From 33639129b1a29e9a611ba9471af7094326af4efa Mon Sep 17 00:00:00 2001 From: Cccc_ Date: Sat, 6 Jun 2026 23:54:11 +0800 Subject: [PATCH] init --- .dockerignore | 10 + .gitignore | 42 + .vscode/settings.json | 3 + Dockerfile.backend | 15 + Dockerfile.frontend | 13 + README.md | 59 + backend/.env.example | 11 + backend/app/__init__.py | 1 + backend/app/api/deps.py | 35 + backend/app/api/routes_admin.py | 84 + backend/app/api/routes_auth.py | 32 + backend/app/api/routes_student.py | 161 ++ backend/app/core/__init__.py | 1 + backend/app/core/database.py | 19 + backend/app/core/ratelimit.py | 42 + backend/app/core/schemas.py | 160 ++ backend/app/core/security.py | 17 + backend/app/core/settings.py | 28 + backend/app/main.py | 96 + backend/app/services/diagnosis.py | 379 +++ backend/app/services/repository.py | 662 +++++ cli.py | 593 ++++ docker-compose.yml | 25 + frontend/.env.example | 2 + frontend/eslint.config.js | 43 + frontend/index.html | 17 + frontend/nginx.conf | 30 + frontend/package.json | 34 + frontend/pnpm-lock.yaml | 2388 +++++++++++++++++ frontend/pnpm-workspace.yaml | 2 + frontend/src/App.vue | 4 + frontend/src/assets/ref/empty-state.png | Bin 0 -> 14216 bytes frontend/src/assets/ref/ouc-logo.png | Bin 0 -> 15290 bytes frontend/src/components/AppShell.vue | 73 + frontend/src/components/StatusBadge.vue | 9 + frontend/src/components/TimelineList.vue | 23 + frontend/src/env.d.ts | 8 + frontend/src/lib/api.ts | 31 + frontend/src/lib/auth.ts | 31 + frontend/src/lib/errors.ts | 51 + frontend/src/lib/status.ts | 2 + frontend/src/main.ts | 74 + frontend/src/router/index.ts | 76 + frontend/src/style.css | 1656 ++++++++++++ frontend/src/types.ts | 122 + frontend/src/views/AdminOrdersView.vue | 396 +++ frontend/src/views/LoginView.vue | 312 +++ frontend/src/views/StudentHomeView.vue | 170 ++ frontend/src/views/StudentOrderDetailView.vue | 368 +++ frontend/src/views/StudentOrdersView.vue | 126 + frontend/src/views/StudentReportView.vue | 705 +++++ frontend/tsconfig.app.json | 25 + frontend/tsconfig.json | 8 + frontend/tsconfig.node.json | 14 + frontend/vite.config.ts | 32 + pyproject.toml | 30 + scripts/_seed.py | 321 +++ uv.lock | 638 +++++ 58 files changed, 10309 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 .vscode/settings.json create mode 100644 Dockerfile.backend create mode 100644 Dockerfile.frontend create mode 100644 README.md create mode 100644 backend/.env.example create mode 100644 backend/app/__init__.py create mode 100644 backend/app/api/deps.py create mode 100644 backend/app/api/routes_admin.py create mode 100644 backend/app/api/routes_auth.py create mode 100644 backend/app/api/routes_student.py create mode 100644 backend/app/core/__init__.py create mode 100644 backend/app/core/database.py create mode 100644 backend/app/core/ratelimit.py create mode 100644 backend/app/core/schemas.py create mode 100644 backend/app/core/security.py create mode 100644 backend/app/core/settings.py create mode 100644 backend/app/main.py create mode 100644 backend/app/services/diagnosis.py create mode 100644 backend/app/services/repository.py create mode 100755 cli.py create mode 100644 docker-compose.yml create mode 100644 frontend/.env.example create mode 100644 frontend/eslint.config.js create mode 100644 frontend/index.html create mode 100644 frontend/nginx.conf create mode 100644 frontend/package.json create mode 100644 frontend/pnpm-lock.yaml create mode 100644 frontend/pnpm-workspace.yaml create mode 100644 frontend/src/App.vue create mode 100644 frontend/src/assets/ref/empty-state.png create mode 100644 frontend/src/assets/ref/ouc-logo.png create mode 100644 frontend/src/components/AppShell.vue create mode 100644 frontend/src/components/StatusBadge.vue create mode 100644 frontend/src/components/TimelineList.vue create mode 100644 frontend/src/env.d.ts create mode 100644 frontend/src/lib/api.ts create mode 100644 frontend/src/lib/auth.ts create mode 100644 frontend/src/lib/errors.ts create mode 100644 frontend/src/lib/status.ts create mode 100644 frontend/src/main.ts create mode 100644 frontend/src/router/index.ts create mode 100644 frontend/src/style.css create mode 100644 frontend/src/types.ts create mode 100644 frontend/src/views/AdminOrdersView.vue create mode 100644 frontend/src/views/LoginView.vue create mode 100644 frontend/src/views/StudentHomeView.vue create mode 100644 frontend/src/views/StudentOrderDetailView.vue create mode 100644 frontend/src/views/StudentOrdersView.vue create mode 100644 frontend/src/views/StudentReportView.vue create mode 100644 frontend/tsconfig.app.json create mode 100644 frontend/tsconfig.json create mode 100644 frontend/tsconfig.node.json create mode 100644 frontend/vite.config.ts create mode 100644 pyproject.toml create mode 100644 scripts/_seed.py create mode 100644 uv.lock diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..a0ff305 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +.venv/ +.runtime/ +__pycache__/ +*.pyc +node_modules/ +frontend/dist/ +backend/data/ +backend/uploads/ +*.sqlite3 +.env diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..29d5404 --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +# Dependencies +frontend/node_modules/ +frontend/dist/ +frontend/.vite/ +frontend/.tsbuildinfo/ +backend/.venv/ +.venv/ + +# Python +__pycache__/ +backend/__pycache__/ +backend/**/*.pyc +*.pyc + +# Runtime & cache +.runtime/ +.ruff_cache/ +.playwright-cli/ +output/ + +# Build artifacts +*.tsbuildinfo + +# Environment (contains secrets) +backend/.env +.env + +# Database & uploads +backend/data/ +backend/uploads/ +*.sqlite3 + +# Tooling +.agents/ +.opencode/ +.codex/ +openspec + +# Reference materials +/ref/ +第一小组*.docx +第一小组*.pptx diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..bf3e171 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.analysis.extraPaths": ["backend"] +} diff --git a/Dockerfile.backend b/Dockerfile.backend new file mode 100644 index 0000000..abf480c --- /dev/null +++ b/Dockerfile.backend @@ -0,0 +1,15 @@ +FROM python:3.14-slim + +WORKDIR /app +ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 + +RUN pip install --no-cache-dir uv + +COPY pyproject.toml uv.lock ./ +RUN uv sync --frozen --no-dev + +COPY backend/ ./backend/ + +WORKDIR /app/backend +EXPOSE 8000 +CMD ["uv", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/Dockerfile.frontend b/Dockerfile.frontend new file mode 100644 index 0000000..dd3227a --- /dev/null +++ b/Dockerfile.frontend @@ -0,0 +1,13 @@ +FROM node:lts-alpine AS builder + +WORKDIR /app +COPY frontend/package.json frontend/pnpm-lock.yaml ./ +RUN corepack enable && pnpm install --frozen-lockfile +COPY frontend/ ./ +RUN pnpm build + +FROM nginx:stable-alpine +COPY frontend/nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=builder /app/dist /usr/share/nginx/html +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..54e35ba --- /dev/null +++ b/README.md @@ -0,0 +1,59 @@ +# 交互式透明化宿舍报修系统 + +课程项目 —— 带 AI 诊断引导的宿舍报修流程。 + +## 快速开始 + +```bash +./cli.py setup # 安装依赖 +./cli.py env # 创建 .env(可选,未配置 DeepSeek 则使用关键词诊断) +./cli.py start # 启动前后端 +``` + +## 常用命令 + +```bash +./cli.py start # 启动服务 +./cli.py stop # 停止服务 +./cli.py status # 查看状态 +./cli.py logs backend -f # 实时日志 +./cli.py seed # 写入演示数据 +./cli.py docker up # Docker 部署 +``` + +## 服务地址 + +| 服务 | 地址 | +| --- | ---- | +| 前端 | | +| 后端 API | | +| API 文档 | | + +## 演示账号 + +运行 `./cli.py seed` 写入演示数据: + +| 角色 | 用户名 | 密码 | +|------|--------|------| +| 学生 | `student01` | `Student123` | +| 管理员 | `admin01` | `Admin123` | + +## 环境变量 + +`./cli.py env` 会从 `.env.example` 复制模板。主要配置项见 `backend/.env.example`。 + +关键变量:`DEEPSEEK_API_KEY` — 配置后启用 AI 诊断,留空则降级为关键词匹配。 + +## 主流程 + +1. 学生用自然语言描述故障 → AI/关键词诊断追问 → 生成结构化草稿 → 确认提交 +2. 管理员审核工单、分配人员、更新状态 +3. 学生确认完工并评价 + +## 技术栈 + +``` +后端 FastAPI + SQLite + DeepSeek API +前端 Vue 3 + Element Plus + Vite +管理 ./cli.py 统一入口 | Docker Compose 可选 +``` diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..17353ba --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,11 @@ +# 数据库文件路径(相对于 backend/ 目录),默认 dorm_repair.sqlite3 +# DORM_REPAIR_DB=dorm_repair.sqlite3 + +# 前端地址,用于 CORS +# FRONTEND_ORIGIN=http://localhost:5173 + +# DeepSeek API 配置(填入 Key 后启用 AI 智能诊断,否则自动降级到关键词匹配) +DEEPSEEK_API_KEY= +# DEEPSEEK_BASE_URL=https://api.deepseek.com +# DEEPSEEK_MODEL=deepseek-chat +# DEEPSEEK_TIMEOUT=30 diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/backend/app/__init__.py @@ -0,0 +1 @@ + diff --git a/backend/app/api/deps.py b/backend/app/api/deps.py new file mode 100644 index 0000000..a29a955 --- /dev/null +++ b/backend/app/api/deps.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +from typing import Annotated + +from fastapi import Depends, Header, HTTPException, status + +from app.core.database import get_connection +from app.core.schemas import UserProfile +from app.services import repository + + +def get_bearer_token(authorization: Annotated[str | None, Header()] = None) -> str: + if not authorization or not authorization.startswith("Bearer "): + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="missing_token") + return authorization.replace("Bearer ", "", 1).strip() + + +def get_current_user(token: Annotated[str, Depends(get_bearer_token)]) -> UserProfile: + with get_connection() as connection: + row = repository.get_user_by_token(connection, token) + if row is None: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid_token") + return repository.profile_from_row(row) + + +def require_student(user: Annotated[UserProfile, Depends(get_current_user)]) -> UserProfile: + if user.role != "student": + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="student_only") + return user + + +def require_admin(user: Annotated[UserProfile, Depends(get_current_user)]) -> UserProfile: + if user.role != "admin": + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="admin_only") + return user diff --git a/backend/app/api/routes_admin.py b/backend/app/api/routes_admin.py new file mode 100644 index 0000000..1e9088d --- /dev/null +++ b/backend/app/api/routes_admin.py @@ -0,0 +1,84 @@ +from __future__ import annotations + +from typing import Annotated + +from fastapi import APIRouter, Depends, HTTPException, Query, status + +from app.api.deps import require_admin +from app.core.database import get_connection +from app.core.schemas import AdminOrderUpdateRequest, OrderDetailOut, OrderSummaryOut, UserProfile +from app.services import repository + +router = APIRouter(prefix="/api/admin", tags=["admin"]) + + +@router.get("/orders", response_model=list[OrderSummaryOut]) +def list_orders( + _: Annotated[UserProfile, Depends(require_admin)], + status_text: str | None = Query(default=None, alias="status"), + category: str | None = Query(default=None), + urgency: str | None = Query(default=None), +) -> list[dict[str, str]]: + with get_connection() as connection: + return repository.list_orders(connection, status=status_text, category=category, urgency=urgency) + + +@router.get("/orders/{order_id}", response_model=OrderDetailOut) +def get_order(order_id: int, _: Annotated[UserProfile, Depends(require_admin)]) -> dict[str, str]: + with get_connection() as connection: + detail = repository.get_order_detail(connection, order_id) + if detail is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="order_not_found") + return detail + + +@router.patch("/orders/{order_id}") +def update_order( + order_id: int, + payload: AdminOrderUpdateRequest, + admin: Annotated[UserProfile, Depends(require_admin)], +) -> dict[str, str]: + with get_connection() as connection: + try: + repository.update_order(connection, order_id, admin, payload) + except KeyError as error: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(error)) from error + except ValueError as error: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(error)) from error + return {"status": "ok"} + + +@router.get("/stats") +def get_stats(_: Annotated[UserProfile, Depends(require_admin)]) -> dict[str, object]: + with get_connection() as connection: + return repository.get_stats(connection) + + +@router.post("/orders/{order_id}/accept-rework") +def accept_rework( + order_id: int, + admin: Annotated[UserProfile, Depends(require_admin)], +) -> dict[str, str]: + with get_connection() as connection: + try: + repository.accept_rework(connection, order_id, admin) + except KeyError as error: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(error)) from error + except ValueError as error: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(error)) from error + return {"status": "ok"} + + +@router.post("/orders/{order_id}/reject-rework") +def reject_rework( + order_id: int, + admin: Annotated[UserProfile, Depends(require_admin)], +) -> dict[str, str]: + with get_connection() as connection: + try: + repository.reject_rework(connection, order_id, admin) + except KeyError as error: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(error)) from error + except ValueError as error: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(error)) from error + return {"status": "ok"} diff --git a/backend/app/api/routes_auth.py b/backend/app/api/routes_auth.py new file mode 100644 index 0000000..a7f9d42 --- /dev/null +++ b/backend/app/api/routes_auth.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +from typing import Annotated + +from fastapi import APIRouter, Depends, HTTPException, status + +from app.api.deps import get_current_user +from app.core.database import get_connection +from app.core.schemas import LoginRequest, LoginResponse, UserProfile +from app.core.security import verify_password +from app.services import repository + +router = APIRouter(prefix="/api/auth", tags=["auth"]) + + +@router.post("/login", response_model=LoginResponse) +def login(payload: LoginRequest) -> LoginResponse: + with get_connection() as connection: + row = connection.execute( + "SELECT id, username, password_hash, role, display_name FROM users WHERE username = ?", + (payload.username,), + ).fetchone() + if row is None or not verify_password(payload.password, row["password_hash"]): + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid_credentials") + token = repository.create_session(connection, row["id"]) + user = repository.profile_from_row(row) + return LoginResponse(token=token, user=user) + + +@router.get("/me", response_model=UserProfile) +def me(user: Annotated[UserProfile, Depends(get_current_user)]) -> UserProfile: + return user diff --git a/backend/app/api/routes_student.py b/backend/app/api/routes_student.py new file mode 100644 index 0000000..510c4f3 --- /dev/null +++ b/backend/app/api/routes_student.py @@ -0,0 +1,161 @@ +from __future__ import annotations + +from typing import Annotated + +from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status + +from app.api.deps import require_student +from app.core.database import get_connection +from app.core.ratelimit import check_diagnosis_rate_limit +from app.core.schemas import ( + DiagnosisAnswerRequest, + DiagnosisResponse, + DiagnosisStartRequest, + FeedbackRequest, + OrderCreateRequest, + OrderDetailOut, + OrderSummaryOut, + ReworkRequest, + SavedAddressOut, + UserProfile, +) +from app.services import repository +from app.services.diagnosis import get_diagnosis_provider + +router = APIRouter(prefix="/api/student", tags=["student"]) + + +@router.post("/diagnosis/start", response_model=DiagnosisResponse) +async def start_diagnosis( + payload: DiagnosisStartRequest, + _: Annotated[UserProfile, Depends(require_student)], + _rl: None = Depends(check_diagnosis_rate_limit), +) -> DiagnosisResponse: + provider = get_diagnosis_provider() + return await provider.start(payload.message) + + +@router.post("/diagnosis/answer", response_model=DiagnosisResponse) +async def answer_diagnosis( + payload: DiagnosisAnswerRequest, + _: Annotated[UserProfile, Depends(require_student)], + _rl: None = Depends(check_diagnosis_rate_limit), +) -> DiagnosisResponse: + try: + provider = get_diagnosis_provider() + return await provider.answer(payload.session_id, payload.answers) + except KeyError as error: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=error.args[0]) from error + + +@router.get("/addresses", response_model=list[SavedAddressOut]) +def list_addresses(user: Annotated[UserProfile, Depends(require_student)]) -> list[dict[str, object]]: + with get_connection() as connection: + return repository.list_addresses(connection, user.id) + + +@router.delete("/addresses/{address_id}") +def delete_address(address_id: int, user: Annotated[UserProfile, Depends(require_student)]) -> dict[str, str]: + with get_connection() as connection: + if not repository.delete_address(connection, address_id, user.id): + raise HTTPException(status_code=404, detail="地址不存在") + return {"message": "已删除"} + + +@router.post("/orders") +def create_order(payload: OrderCreateRequest, user: Annotated[UserProfile, Depends(require_student)]) -> dict[str, int]: + with get_connection() as connection: + order_id = repository.create_order(connection, user, payload) + return {"order_id": order_id} + + +@router.post("/orders/{order_id}/attachments") +def upload_attachments( + order_id: int, + user: Annotated[UserProfile, Depends(require_student)], + files: Annotated[list[UploadFile], File(...)], +) -> dict[str, str]: + with get_connection() as connection: + order = repository.get_order_detail(connection, order_id, student_id=user.id) + if order is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="order_not_found") + for file in files: + try: + repository.save_attachment(connection, order_id, file) + except ValueError as error: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(error)) from error + return {"status": "ok"} + + +@router.post("/orders/{order_id}/cancel") +def cancel_order( + order_id: int, + user: Annotated[UserProfile, Depends(require_student)], +) -> dict[str, str]: + with get_connection() as connection: + try: + repository.cancel_order(connection, order_id, user) + except KeyError as error: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=error.args[0]) from error + except ValueError as error: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error.args[0]) from error + return {"status": "ok"} + + +@router.get("/orders", response_model=list[OrderSummaryOut]) +def list_orders(user: Annotated[UserProfile, Depends(require_student)]) -> list[dict[str, str]]: + with get_connection() as connection: + return repository.list_orders(connection, student_id=user.id) + + +@router.get("/orders/{order_id}", response_model=OrderDetailOut) +def get_order(order_id: int, user: Annotated[UserProfile, Depends(require_student)]) -> dict[str, str]: + with get_connection() as connection: + detail = repository.get_order_detail(connection, order_id, student_id=user.id) + if detail is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="order_not_found") + return detail + + +@router.post("/orders/{order_id}/confirm") +def confirm_order(order_id: int, user: Annotated[UserProfile, Depends(require_student)]) -> dict[str, str]: + with get_connection() as connection: + try: + repository.confirm_order(connection, order_id, user) + except KeyError as error: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=error.args[0]) from error + except ValueError as error: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error.args[0]) from error + return {"status": "ok"} + + +@router.post("/orders/{order_id}/feedback") +def submit_feedback( + order_id: int, + payload: FeedbackRequest, + user: Annotated[UserProfile, Depends(require_student)], +) -> dict[str, str]: + with get_connection() as connection: + try: + repository.add_feedback(connection, order_id, user, payload) + except KeyError as error: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=error.args[0]) from error + except ValueError as error: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error.args[0]) from error + return {"status": "ok"} + + +@router.post("/orders/{order_id}/rework") +def submit_rework( + order_id: int, + payload: ReworkRequest, + user: Annotated[UserProfile, Depends(require_student)], +) -> dict[str, str]: + with get_connection() as connection: + try: + repository.request_rework(connection, order_id, user, payload) + except KeyError as error: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=error.args[0]) from error + except ValueError as error: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error.args[0]) from error + return {"status": "ok"} diff --git a/backend/app/core/__init__.py b/backend/app/core/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/backend/app/core/__init__.py @@ -0,0 +1 @@ + diff --git a/backend/app/core/database.py b/backend/app/core/database.py new file mode 100644 index 0000000..7a87cd1 --- /dev/null +++ b/backend/app/core/database.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +import sqlite3 +from collections.abc import Generator +from contextlib import contextmanager + +from .settings import DATABASE_PATH, ensure_runtime_dirs + + +@contextmanager +def get_connection() -> Generator[sqlite3.Connection, None, None]: + ensure_runtime_dirs() + connection = sqlite3.connect(DATABASE_PATH) + connection.row_factory = sqlite3.Row + connection.execute("PRAGMA foreign_keys = ON") + try: + yield connection + finally: + connection.close() diff --git a/backend/app/core/ratelimit.py b/backend/app/core/ratelimit.py new file mode 100644 index 0000000..900700d --- /dev/null +++ b/backend/app/core/ratelimit.py @@ -0,0 +1,42 @@ +"""Simple in-memory rate limiter per user.""" + +from __future__ import annotations + +import time +from collections import defaultdict +from typing import Annotated + +from fastapi import Depends, HTTPException, status + +from app.api.deps import require_student +from app.core.schemas import UserProfile + + +class RateLimiter: + def __init__(self, max_requests: int, window_seconds: int) -> None: + self._max = max_requests + self._window = window_seconds + self._buckets: dict[int, list[float]] = defaultdict(list) + + def check(self, user_id: int) -> None: + now = time.time() + cutoff = now - self._window + bucket = self._buckets[user_id] + # purge expired entries + while bucket and bucket[0] < cutoff: + bucket.pop(0) + if len(bucket) >= self._max: + raise HTTPException( + status_code=status.HTTP_429_TOO_MANY_REQUESTS, + detail=f"请求过于频繁,请 {self._window // 60} 分钟后重试", + ) + bucket.append(now) + + +_diagnosis_limiter = RateLimiter(max_requests=5, window_seconds=60) + + +def check_diagnosis_rate_limit( + user: Annotated[UserProfile, Depends(require_student)], +) -> None: + _diagnosis_limiter.check(user.id) diff --git a/backend/app/core/schemas.py b/backend/app/core/schemas.py new file mode 100644 index 0000000..e46d470 --- /dev/null +++ b/backend/app/core/schemas.py @@ -0,0 +1,160 @@ +from __future__ import annotations + +from typing import Any, Literal + +from pydantic import BaseModel, Field + +Role = Literal["student", "admin"] +OrderStatus = Literal["已提交", "待处理", "处理中", "待上门", "已完成", "已确认", "返工申请中", "已取消"] +Urgency = Literal["低", "中", "高", "紧急"] + + +class LoginRequest(BaseModel): + username: str = Field(min_length=1, max_length=50) + password: str = Field(min_length=1, max_length=100) + + +class UserProfile(BaseModel): + id: int + username: str + display_name: str + role: Role + + +class LoginResponse(BaseModel): + token: str + user: UserProfile + + +class DiagnosisStartRequest(BaseModel): + message: str = Field(min_length=2, max_length=300) + + +class DiagnosisQuestionAnswer(BaseModel): + question_id: str + answer: str = Field(min_length=1, max_length=200) + + +class DiagnosisAnswerRequest(BaseModel): + session_id: str + answers: list[DiagnosisQuestionAnswer] + + +class DiagnosisQuestion(BaseModel): + id: str + prompt: str + + +class DiagnosisDraft(BaseModel): + category: str + urgency: Urgency + summary: str + safety_risk: bool + suggested_worker: str + notes: list[str] + + +class DiagnosisResponse(BaseModel): + session_id: str + stage: Literal["questions", "draft"] + initial_message: str + suggested_categories: list[str] + questions: list[DiagnosisQuestion] + draft: DiagnosisDraft | None = None + + +class SavedAddressOut(BaseModel): + id: int + campus: str + building: str + room: str + last_used_at: str + + +class OrderCreateRequest(BaseModel): + campus: str = Field(min_length=1, max_length=50) + building: str = Field(min_length=1, max_length=50) + room: str = Field(min_length=1, max_length=50) + category: str = Field(min_length=1, max_length=50) + raw_description: str = Field(min_length=2, max_length=300) + structured_summary: str = Field(min_length=2, max_length=400) + urgency: Urgency + expected_date: str | None = Field(default=None, max_length=20) + expected_time_segment: str | None = Field(default=None, max_length=50) + diagnosis_session_id: str | None = Field(default=None, max_length=80) + allow_room_entry: bool = False + + +class FeedbackRequest(BaseModel): + rating: int = Field(ge=1, le=5) + comment: str = Field(min_length=1, max_length=300) + + +class ReworkRequest(BaseModel): + reason: str = Field(min_length=2, max_length=300) + + +class AdminOrderUpdateRequest(BaseModel): + status: OrderStatus | None = None + assignee_name: str | None = Field(default=None, max_length=50) + expected_arrival_at: str | None = Field(default=None, max_length=50) + admin_note: str | None = Field(default=None, max_length=300) + + +class OrderEventOut(BaseModel): + id: int + actor_role: str + actor_name: str + event_type: str + title: str + detail: str | None + from_status: str | None + to_status: str | None + created_at: str + + +class AttachmentOut(BaseModel): + id: int + file_name: str + file_path: str + mime_type: str + created_at: str + + +class OrderSummaryOut(BaseModel): + id: int + order_no: str + campus: str + building: str + room: str + category: str + status: str + urgency: str + submission_time: str + expected_repair_time: str | None + assignee_name: str | None + + +class OrderDetailOut(BaseModel): + id: int + order_no: str + campus: str + building: str + room: str + category: str + status: str + urgency: str + raw_description: str + structured_summary: str + allow_room_entry: bool + expected_date: str | None + expected_time_segment: str | None + assignee_name: str | None + expected_arrival_at: str | None + admin_note: str | None + rework_reason: str | None + created_at: str + updated_at: str + attachments: list[AttachmentOut] + events: list[OrderEventOut] + feedback: dict[str, Any] | None diff --git a/backend/app/core/security.py b/backend/app/core/security.py new file mode 100644 index 0000000..ddc1169 --- /dev/null +++ b/backend/app/core/security.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +import secrets + +import bcrypt + + +def hash_password(password: str) -> str: + return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode() + + +def verify_password(password: str, password_hash: str) -> bool: + return bcrypt.checkpw(password.encode("utf-8"), password_hash.encode()) + + +def generate_token() -> str: + return secrets.token_urlsafe(32) diff --git a/backend/app/core/settings.py b/backend/app/core/settings.py new file mode 100644 index 0000000..f952a8a --- /dev/null +++ b/backend/app/core/settings.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +import os +from pathlib import Path + +from dotenv import load_dotenv + +BASE_DIR = Path(__file__).resolve().parents[2] +load_dotenv(BASE_DIR / ".env") + +DATA_DIR = BASE_DIR / "data" +UPLOADS_DIR = BASE_DIR / "uploads" +DATABASE_PATH = DATA_DIR / os.getenv("DORM_REPAIR_DB", "dorm_repair.sqlite3") +FRONTEND_ORIGIN = os.getenv("FRONTEND_ORIGIN", "http://localhost:5173") +FRONTEND_ORIGINS = [ + origin.strip() + for origin in os.getenv("FRONTEND_ORIGINS", f"{FRONTEND_ORIGIN},http://127.0.0.1:5173").split(",") + if origin.strip() +] +DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY", "") +DEEPSEEK_BASE_URL = os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com") +DEEPSEEK_MODEL = os.getenv("DEEPSEEK_MODEL", "deepseek-chat") +DEEPSEEK_TIMEOUT = float(os.getenv("DEEPSEEK_TIMEOUT", "30.0")) + + +def ensure_runtime_dirs() -> None: + DATA_DIR.mkdir(parents=True, exist_ok=True) + UPLOADS_DIR.mkdir(parents=True, exist_ok=True) diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..f2f7147 --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,96 @@ +from __future__ import annotations + +import logging + +from fastapi import FastAPI, HTTPException, Request +from fastapi.exceptions import RequestValidationError +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse +from fastapi.staticfiles import StaticFiles + +from app.api.routes_admin import router as admin_router +from app.api.routes_auth import router as auth_router +from app.api.routes_student import router as student_router +from app.core.database import get_connection +from app.core.settings import FRONTEND_ORIGINS, UPLOADS_DIR, ensure_runtime_dirs +from app.services.repository import init_db + +logger = logging.getLogger(__name__) + +ERROR_DETAIL_MAP: dict[str, str] = { + "missing_token": "缺少认证令牌", + "invalid_token": "认证已过期,请重新登录", + "invalid_credentials": "用户名或密码错误", + "student_only": "此功能仅限学生使用", + "admin_only": "此功能仅限管理员使用", + "order_not_found": "工单不存在或已被删除", + "session_not_found": "诊断会话已过期,请重新描述故障", + "order_not_completed": "维修尚未完成,无法执行此操作", + "order_not_ready_for_feedback": "当前状态不可评价", + "order_cannot_cancel": "当前状态无法取消,仅已提交或待处理的工单可取消", + "order_not_in_rework": "当前工单不在返工申请状态", + "unsupported_file_type": "仅支持上传图片文件(jpg, png, gif, webp)", + "file_too_large": "文件大小超过限制(最大10MB)", + "validation_error": "请求参数有误", +} + +ensure_runtime_dirs() + +with get_connection() as conn: + init_db(conn) + conn.execute("DELETE FROM sessions WHERE expires_at < datetime('now')") + conn.commit() + +app = FastAPI(title="Dorm Repair API", version="0.1.0") + +app.add_middleware( + CORSMiddleware, + allow_origins=FRONTEND_ORIGINS, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +app.include_router(auth_router) +app.include_router(student_router) +app.include_router(admin_router) +app.mount("/uploads", StaticFiles(directory=UPLOADS_DIR), name="uploads") + + +@app.exception_handler(HTTPException) +async def http_exception_handler(request: Request, exc: HTTPException) -> JSONResponse: + error_code = str(exc.detail) if not isinstance(exc.detail, dict) else exc.detail.get("error_code", str(exc.detail)) + detail = ERROR_DETAIL_MAP.get(error_code, str(exc.detail)) + return JSONResponse( + status_code=exc.status_code, + content={"detail": detail, "error_code": error_code}, + ) + + +@app.exception_handler(RequestValidationError) +async def validation_exception_handler(request: Request, exc: RequestValidationError) -> JSONResponse: + return JSONResponse( + status_code=422, + content={"detail": "请求参数有误", "error_code": "validation_error"}, + ) + + +@app.exception_handler(Exception) +async def unhandled_exception_handler(request: Request, exc: Exception) -> JSONResponse: + logger.exception("Unhandled exception: %s", exc) + return JSONResponse( + status_code=500, + content={"detail": "服务器内部错误", "error_code": "internal_error"}, + ) + + +@app.on_event("shutdown") +async def shutdown() -> None: + from app.services.diagnosis import close_diagnosis_provider + + await close_diagnosis_provider() + + +@app.get("/api/health") +def health() -> dict[str, str]: + return {"status": "ok"} diff --git a/backend/app/services/diagnosis.py b/backend/app/services/diagnosis.py new file mode 100644 index 0000000..ceec0a8 --- /dev/null +++ b/backend/app/services/diagnosis.py @@ -0,0 +1,379 @@ +from __future__ import annotations + +import json +import logging +import time +from dataclasses import dataclass, field +from typing import Any, Literal +from uuid import uuid4 + +import httpx + +from app.core.schemas import DiagnosisDraft, DiagnosisQuestion, DiagnosisQuestionAnswer, DiagnosisResponse +from app.core.settings import DEEPSEEK_API_KEY, DEEPSEEK_BASE_URL, DEEPSEEK_MODEL, DEEPSEEK_TIMEOUT + +logger = logging.getLogger(__name__) +SESSION_TTL = 3600 # 1 hour + +CLASSIFY_SYSTEM_PROMPT = """你是宿舍维修故障诊断助手。根据学生故障描述,输出 JSON 对象。 + +分类规则: +- 电路照明: 灯、电、跳闸、插座、焦味、漏电、火花 +- 给排水: 漏水、水龙头、下水、厕所、马桶、管道、洗手池 +- 门窗锁具: 门、锁、窗、把手、柜门 +- 空调设备: 空调、制冷、制热 +- 网络设备: 网络、wifi、网速、断网、网口 +- 家具设施: 床、桌椅、衣柜、抽屉、护栏 +- 其他: 不属于以上类别 + +紧急度规则(urgency): +- 紧急: 火花、漏电、冒烟、大面积漏水、安全威胁 +- 高: 影响基本生活(照明/用水/用电/门禁) +- 中: 普通故障影响使用 +- 低: 无立即影响 + +safety_risk: 火花、漏电、冒烟、焦味 设为 true +suggested_worker: 电工(电路照明)、水暖维修(给排水)、门窗维修(门窗锁具)、空调维修(空调设备)、 + 网络维护(网络设备)、综合维修(其他) +questions: 2-3个有针对性、非模板化的追问,每个含 id(q1/q2/q3) 和 prompt 字段 + +输出格式: +{"category":"...","urgency":"...","safety_risk":false,"suggested_worker":"...","notes":["..."],"questions":[{"id":"q1","prompt":"..."}]}""" + +SUMMARY_SYSTEM_PROMPT = """根据学生原始描述和补充回答,生成维修工单摘要。 +格式:"[category]问题,建议[suggested_worker]处理。[现象];[位置];[影响范围]" +不超过200字,使用中文标点。""" + + +@dataclass +class DiagnosisSession: + session_id: str + category: str + urgency: Literal["低", "中", "高", "紧急"] + suggested_worker: str + safety_risk: bool + initial_message: str + suggested_categories: list[str] + questions: list[DiagnosisQuestion] + notes: list[str] = field(default_factory=list) + created_at: float = field(default_factory=time.time) + + +class AIDiagnosisProvider: + def __init__( + self, + *, + base_url: str = DEEPSEEK_BASE_URL, + api_key: str = DEEPSEEK_API_KEY, + model: str = DEEPSEEK_MODEL, + timeout: float = DEEPSEEK_TIMEOUT, + ) -> None: + self._api_key = api_key + self._client = httpx.AsyncClient( + base_url=base_url.rstrip("/"), + headers={ + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + }, + timeout=httpx.Timeout(timeout), + ) + self._model = model + self._sessions: dict[str, DiagnosisSession] = {} + self._last_sweep = time.time() + + def _sweep_expired_sessions(self) -> None: + now = time.time() + if now - self._last_sweep < 300: # sweep every 5 minutes + return + self._last_sweep = now + expired = [sid for sid, s in self._sessions.items() if now - s.created_at > SESSION_TTL] + for sid in expired: + del self._sessions[sid] + if expired: + logger.info("Swept %d expired diagnosis sessions", len(expired)) + + async def start(self, message: str) -> DiagnosisResponse: + self._sweep_expired_sessions() + if not self._api_key: + return self._start_local(message) + try: + result = await self._call_classify(message) + category = result["category"] + urgency = result["urgency"] + worker = result["suggested_worker"] + safety_risk = result["safety_risk"] + ai_notes = result.get("notes", []) + questions = [DiagnosisQuestion(id=q["id"], prompt=q["prompt"]) for q in result.get("questions", [])] + if not questions: + questions = [ + DiagnosisQuestion(id="location", prompt="问题具体出现在宿舍的什么位置或设备上?"), + DiagnosisQuestion(id="symptom", prompt="请补充最明显的故障现象,方便维修人员带对工具。"), + ] + except (httpx.HTTPError, json.JSONDecodeError, KeyError): + return self._start_local(message) + + session_id = uuid4().hex + suggested_categories = [category] + if category != "其他": + suggested_categories.append("其他") + session = DiagnosisSession( + session_id=session_id, + category=category, + urgency=urgency, + suggested_worker=worker, + safety_risk=safety_risk, + initial_message=message, + suggested_categories=suggested_categories, + questions=questions, + notes=ai_notes, + ) + self._sessions[session_id] = session + return DiagnosisResponse( + session_id=session_id, + stage="questions", + initial_message=message, + suggested_categories=suggested_categories, + questions=questions, + draft=None, + ) + + async def answer(self, session_id: str, answers: list[DiagnosisQuestionAnswer]) -> DiagnosisResponse: + session = self._sessions.get(session_id) + if session is None: + raise KeyError("session_not_found") + + if time.time() - session.created_at > SESSION_TTL: + del self._sessions[session_id] + raise KeyError("session_not_found") + + session.created_at = time.time() + + if not self._api_key: + return self._answer_local(session, answers) + try: + summary = await self._call_summary(session, answers) + except (httpx.HTTPError, json.JSONDecodeError): + return self._answer_local(session, answers) + + notes = list(session.notes) + if session.safety_risk: + notes.append("建议尽快断电或停止继续使用相关设备。") + answer_map = {item.question_id: item.answer.strip() for item in answers} + for question in session.questions: + value = answer_map.get(question.id) + if value: + notes.append(f"{question.prompt}:{value}") + + draft = DiagnosisDraft( + category=session.category, + urgency=session.urgency, + summary=summary, + safety_risk=session.safety_risk, + suggested_worker=session.suggested_worker, + notes=notes, + ) + return DiagnosisResponse( + session_id=session.session_id, + stage="draft", + initial_message=session.initial_message, + suggested_categories=session.suggested_categories, + questions=[], + draft=draft, + ) + + def _start_local(self, message: str) -> DiagnosisResponse: + category, urgency, worker, safety_risk, questions = self._local_classify(message) + session_id = uuid4().hex + suggested_categories = [category] + if category != "其他": + suggested_categories.append("其他") + session = DiagnosisSession( + session_id=session_id, + category=category, + urgency=urgency, + suggested_worker=worker, + safety_risk=safety_risk, + initial_message=message, + suggested_categories=suggested_categories, + questions=questions, + ) + self._sessions[session_id] = session + return DiagnosisResponse( + session_id=session_id, + stage="questions", + initial_message=message, + suggested_categories=suggested_categories, + questions=questions, + draft=None, + ) + + def _answer_local(self, session: DiagnosisSession, answers: list[DiagnosisQuestionAnswer]) -> DiagnosisResponse: + answer_map = {item.question_id: item.answer.strip() for item in answers} + notes = [] + if session.safety_risk: + notes.append("建议尽快断电或停止继续使用相关设备。") + if "location" in answer_map: + notes.append(f"具体位置:{answer_map['location']}") + if "impact" in answer_map: + notes.append(f"影响范围:{answer_map['impact']}") + if "symptom" in answer_map: + notes.append(f"补充现象:{answer_map['symptom']}") + + summary = self._local_build_summary(session, answer_map) + draft = DiagnosisDraft( + category=session.category, + urgency=session.urgency, + summary=summary, + safety_risk=session.safety_risk, + suggested_worker=session.suggested_worker, + notes=notes, + ) + return DiagnosisResponse( + session_id=session.session_id, + stage="draft", + initial_message=session.initial_message, + suggested_categories=session.suggested_categories, + questions=[], + draft=draft, + ) + + def _local_classify( + self, message: str + ) -> tuple[str, Literal["低", "中", "高", "紧急"], str, bool, list[DiagnosisQuestion]]: + text = message.lower() + category = "其他" + urgency: Literal["低", "中", "高", "紧急"] = "中" + worker = "综合维修" + safety_risk = False + + if any(keyword in text for keyword in ["灯", "电", "跳闸", "插座", "焦味"]): + category = "电路照明" + worker = "电工" + urgency = "高" + if any(keyword in text for keyword in ["漏水", "水龙头", "下水", "厕所", "马桶"]): + category = "给排水" + worker = "水暖维修" + urgency = "高" + if any(keyword in text for keyword in ["门", "锁", "窗", "把手"]): + category = "门窗锁具" + worker = "门窗维修" + urgency = "中" + if any(keyword in text for keyword in ["空调", "制冷", "制热"]): + category = "空调设备" + worker = "空调维修" + urgency = "中" + if any(keyword in text for keyword in ["网络", "wifi", "网速", "断网"]): + category = "网络设备" + worker = "网络维护" + urgency = "中" + if any(keyword in text for keyword in ["烟", "火花", "焦味", "漏电"]): + urgency = "紧急" + safety_risk = True + + questions = [ + DiagnosisQuestion(id="location", prompt="问题具体出现在宿舍的什么位置或设备上?"), + DiagnosisQuestion(id="impact", prompt="这个问题目前影响范围有多大?比如是否影响整间宿舍使用。"), + DiagnosisQuestion(id="symptom", prompt="请补充最明显的故障现象,方便维修人员带对工具。"), + ] + return category, urgency, worker, safety_risk, questions + + def _local_build_summary(self, session: DiagnosisSession, answers: dict[str, str]) -> str: + parts = [session.initial_message.strip()] + for key in ("location", "impact", "symptom"): + value = answers.get(key) + if value: + parts.append(value) + joined = ";".join(parts) + return f"{session.category}问题,建议{session.suggested_worker}处理。{joined}" + + async def _call_classify(self, message: str) -> dict[str, Any]: + response = await self._client.post( + "/v1/chat/completions", + json={ + "model": self._model, + "messages": [ + {"role": "system", "content": CLASSIFY_SYSTEM_PROMPT}, + {"role": "user", "content": message}, + ], + "response_format": {"type": "json_object"}, + "temperature": 0.1, + }, + ) + response.raise_for_status() + body = response.json() + content = body["choices"][0]["message"]["content"] + result = json.loads(content) + _validate_classify_result(result) + return result + + async def _call_summary(self, session: DiagnosisSession, answers: list[DiagnosisQuestionAnswer]) -> str: + answer_texts = "\n".join(f"- {item.question_id}: {item.answer.strip()}" for item in answers) + response = await self._client.post( + "/v1/chat/completions", + json={ + "model": self._model, + "messages": [ + {"role": "system", "content": SUMMARY_SYSTEM_PROMPT}, + { + "role": "user", + "content": ( + f"原始描述:{session.initial_message}\n" + f"故障类别:{session.category}\n" + f"负责工种:{session.suggested_worker}\n" + f"补充回答:\n{answer_texts}" + ), + }, + ], + "temperature": 0.1, + }, + ) + response.raise_for_status() + body = response.json() + return body["choices"][0]["message"]["content"].strip() + + +def _validate_classify_result(result: dict[str, Any]) -> None: + valid_categories = {"电路照明", "给排水", "门窗锁具", "空调设备", "网络设备", "家具设施", "其他"} + valid_urgencies = {"低", "中", "高", "紧急"} + + category = result.get("category", "") + if category not in valid_categories: + result["category"] = "其他" + + urgency = result.get("urgency", "") + if urgency not in valid_urgencies: + result["urgency"] = "中" + + if not isinstance(result.get("safety_risk"), bool): + result["safety_risk"] = False + + worker = result.get("suggested_worker", "") + if not worker: + result["suggested_worker"] = "综合维修" + + questions = result.get("questions") + if not isinstance(questions, list): + result["questions"] = [] + else: + result["questions"] = [q for q in questions if isinstance(q, dict) and "id" in q and "prompt" in q] + + notes = result.get("notes") + if not isinstance(notes, list): + result["notes"] = [] + + +_dp: AIDiagnosisProvider | None = None + + +def get_diagnosis_provider() -> AIDiagnosisProvider: + global _dp + if _dp is None: + _dp = AIDiagnosisProvider() + return _dp + + +async def close_diagnosis_provider() -> None: + global _dp + if _dp is not None: + await _dp._client.aclose() + _dp = None diff --git a/backend/app/services/repository.py b/backend/app/services/repository.py new file mode 100644 index 0000000..6338186 --- /dev/null +++ b/backend/app/services/repository.py @@ -0,0 +1,662 @@ +from __future__ import annotations + +import sqlite3 +from datetime import datetime, timedelta +from pathlib import Path +from typing import Any +from uuid import uuid4 + +from fastapi import UploadFile + +from app.core.schemas import AdminOrderUpdateRequest, FeedbackRequest, OrderCreateRequest, ReworkRequest, UserProfile +from app.core.security import generate_token +from app.core.settings import UPLOADS_DIR + +STATUS_NEW = "已提交" +DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S" + + +def now_text() -> str: + return datetime.now().strftime(DATETIME_FORMAT) + + +def init_db(connection: sqlite3.Connection) -> None: + connection.executescript( + """ + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + role TEXT NOT NULL, + display_name TEXT NOT NULL, + created_at TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS sessions ( + token TEXT PRIMARY KEY, + user_id INTEGER NOT NULL, + created_at TEXT NOT NULL, + expires_at TEXT NOT NULL, + FOREIGN KEY(user_id) REFERENCES users(id) + ); + + CREATE TABLE IF NOT EXISTS repair_orders ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + order_no TEXT UNIQUE NOT NULL, + student_id INTEGER NOT NULL, + campus TEXT NOT NULL, + building TEXT NOT NULL, + room TEXT NOT NULL, + category TEXT NOT NULL, + raw_description TEXT NOT NULL, + structured_summary TEXT NOT NULL, + urgency TEXT NOT NULL, + status TEXT NOT NULL, + allow_room_entry INTEGER NOT NULL DEFAULT 0, + assignee_name TEXT, + expected_date TEXT, + expected_time_segment TEXT, + expected_arrival_at TEXT, + admin_note TEXT, + rework_reason TEXT, + diagnosis_session_id TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + FOREIGN KEY(student_id) REFERENCES users(id) + ); + + CREATE TABLE IF NOT EXISTS saved_addresses ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + student_id INTEGER NOT NULL, + campus TEXT NOT NULL, + building TEXT NOT NULL, + room TEXT NOT NULL, + last_used_at TEXT NOT NULL, + UNIQUE(student_id, campus, building, room), + FOREIGN KEY(student_id) REFERENCES users(id) + ); + + CREATE TABLE IF NOT EXISTS attachments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + order_id INTEGER NOT NULL, + file_name TEXT NOT NULL, + file_path TEXT NOT NULL, + mime_type TEXT NOT NULL, + created_at TEXT NOT NULL, + FOREIGN KEY(order_id) REFERENCES repair_orders(id) + ); + + CREATE TABLE IF NOT EXISTS order_events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + order_id INTEGER NOT NULL, + actor_role TEXT NOT NULL, + actor_name TEXT NOT NULL, + event_type TEXT NOT NULL, + title TEXT NOT NULL, + detail TEXT, + from_status TEXT, + to_status TEXT, + created_at TEXT NOT NULL, + FOREIGN KEY(order_id) REFERENCES repair_orders(id) + ); + + CREATE TABLE IF NOT EXISTS feedback ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + order_id INTEGER UNIQUE NOT NULL, + rating INTEGER NOT NULL, + comment TEXT NOT NULL, + created_at TEXT NOT NULL, + FOREIGN KEY(order_id) REFERENCES repair_orders(id) + ); + """ + ) + connection.commit() + + +def create_session(connection: sqlite3.Connection, user_id: int) -> str: + token = generate_token() + created_at = now_text() + expires_at = (datetime.now() + timedelta(days=7)).strftime(DATETIME_FORMAT) + connection.execute( + "INSERT INTO sessions (token, user_id, created_at, expires_at) VALUES (?, ?, ?, ?)", + (token, user_id, created_at, expires_at), + ) + connection.commit() + return token + + +def get_user_by_token(connection: sqlite3.Connection, token: str) -> sqlite3.Row | None: + return connection.execute( + """ + SELECT users.id, users.username, users.display_name, users.role + FROM sessions + JOIN users ON users.id = sessions.user_id + WHERE sessions.token = ? AND sessions.expires_at >= ? + """, + (token, now_text()), + ).fetchone() + + +def profile_from_row(row: sqlite3.Row) -> UserProfile: + return UserProfile(id=row["id"], username=row["username"], display_name=row["display_name"], role=row["role"]) + + +def build_order_number(connection: sqlite3.Connection) -> str: + count = connection.execute("SELECT COUNT(*) AS count FROM repair_orders").fetchone()["count"] + 1 + return f"DR{datetime.now().strftime('%Y%m%d')}{count:03d}" + + +def create_event( + connection: sqlite3.Connection, + order_id: int, + actor_role: str, + actor_name: str, + event_type: str, + title: str, + detail: str | None, + from_status: str | None, + to_status: str | None, +) -> None: + connection.execute( + """ + INSERT INTO order_events ( + order_id, actor_role, actor_name, event_type, title, detail, from_status, to_status, created_at + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + order_id, + actor_role, + actor_name, + event_type, + title, + detail, + from_status, + to_status, + now_text(), + ), + ) + + +def upsert_saved_address( + connection: sqlite3.Connection, + student_id: int, + campus: str, + building: str, + room: str, +) -> None: + connection.execute( + """ + INSERT INTO saved_addresses (student_id, campus, building, room, last_used_at) + VALUES (?, ?, ?, ?, ?) + ON CONFLICT(student_id, campus, building, room) + DO UPDATE SET last_used_at = excluded.last_used_at + """, + (student_id, campus, building, room, now_text()), + ) + + +def create_order(connection: sqlite3.Connection, student: UserProfile, payload: OrderCreateRequest) -> int: + order_no = build_order_number(connection) + created_at = now_text() + cursor = connection.execute( + """ + INSERT INTO repair_orders ( + order_no, student_id, campus, building, room, category, raw_description, structured_summary, + urgency, status, allow_room_entry, assignee_name, expected_date, expected_time_segment, + expected_arrival_at, admin_note, rework_reason, diagnosis_session_id, created_at, updated_at + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, NULL, NULL, NULL, ?, ?, ?) + """, + ( + order_no, + student.id, + payload.campus, + payload.building, + payload.room, + payload.category, + payload.raw_description, + payload.structured_summary, + payload.urgency, + STATUS_NEW, + int(payload.allow_room_entry), + payload.expected_date, + payload.expected_time_segment, + payload.diagnosis_session_id, + created_at, + created_at, + ), + ) + order_id = cursor.lastrowid + create_event( + connection, + order_id, + "student", + student.display_name, + "created", + "学生提交报修", + payload.raw_description, + None, + STATUS_NEW, + ) + upsert_saved_address(connection, student.id, payload.campus, payload.building, payload.room) + connection.commit() + return order_id + + +def list_addresses(connection: sqlite3.Connection, student_id: int) -> list[dict[str, Any]]: + rows = connection.execute( + """ + SELECT id, campus, building, room, last_used_at + FROM saved_addresses + WHERE student_id = ? + ORDER BY last_used_at DESC + """, + (student_id,), + ).fetchall() + return [dict(row) for row in rows] + + +def delete_address(connection: sqlite3.Connection, address_id: int, student_id: int) -> bool: + cursor = connection.execute( + "DELETE FROM saved_addresses WHERE id = ? AND student_id = ?", + (address_id, student_id), + ) + connection.commit() + return cursor.rowcount > 0 + + +def list_orders( + connection: sqlite3.Connection, + *, + student_id: int | None = None, + status: str | None = None, + category: str | None = None, + urgency: str | None = None, +) -> list[dict[str, Any]]: + query = """ + SELECT + id, + order_no, + campus, + building, + room, + category, + status, + urgency, + created_at AS submission_time, + COALESCE( + expected_arrival_at, + expected_date || ' ' || COALESCE(expected_time_segment, '') + ) AS expected_repair_time, + assignee_name + FROM repair_orders + WHERE 1 = 1 + """ + params: list[Any] = [] + if student_id is not None: + query += " AND student_id = ?" + params.append(student_id) + if status: + query += " AND status = ?" + params.append(status) + if category: + query += " AND category = ?" + params.append(category) + if urgency: + query += " AND urgency = ?" + params.append(urgency) + query += " ORDER BY updated_at DESC" + rows = connection.execute(query, params).fetchall() + return [dict(row) for row in rows] + + +def _get_order_row(connection: sqlite3.Connection, order_id: int) -> sqlite3.Row | None: + return connection.execute("SELECT * FROM repair_orders WHERE id = ?", (order_id,)).fetchone() + + +def get_order_detail( + connection: sqlite3.Connection, + order_id: int, + *, + student_id: int | None = None, +) -> dict[str, Any] | None: + row = _get_order_row(connection, order_id) + if row is None: + return None + if student_id is not None and row["student_id"] != student_id: + return None + + attachments = connection.execute( + "SELECT id, file_name, file_path, mime_type, created_at FROM attachments WHERE order_id = ? ORDER BY id ASC", + (order_id,), + ).fetchall() + events = connection.execute( + """ + SELECT id, actor_role, actor_name, event_type, title, detail, from_status, to_status, created_at + FROM order_events + WHERE order_id = ? + ORDER BY id ASC + """, + (order_id,), + ).fetchall() + feedback_row = connection.execute( + "SELECT rating, comment, created_at FROM feedback WHERE order_id = ?", + (order_id,), + ).fetchone() + detail = dict(row) + detail["attachments"] = [dict(item) for item in attachments] + detail["events"] = [dict(item) for item in events] + detail["feedback"] = dict(feedback_row) if feedback_row else None + return detail + + +_ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".webp"} +_MAX_UPLOAD_SIZE = 10 * 1024 * 1024 # 10 MB + +_VALID_TRANSITIONS: dict[str, set[str]] = { + "已提交": {"待处理", "已取消"}, + "待处理": {"处理中", "已取消"}, + "处理中": {"待上门", "已完成"}, + "待上门": {"处理中", "已完成"}, + "已完成": {"已确认", "返工申请中"}, + "已确认": set(), + "返工申请中": {"处理中"}, + "已取消": set(), +} + + +def _detect_image_type(header: bytes) -> str | None: + if header[:3] == b"\xff\xd8\xff": + return ".jpg" + if header[:8] == b"\x89PNG\r\n\x1a\n": + return ".png" + if header[:4] == b"GIF8": + return ".gif" + if header[:4] == b"RIFF" and len(header) >= 12 and header[8:12] == b"WEBP": + return ".webp" + return None + + +def _validate_status_transition(from_status: str, to_status: str) -> None: + allowed = _VALID_TRANSITIONS.get(from_status, set()) + if to_status not in allowed: + raise ValueError(f"不允许的状态转换:{from_status} → {to_status}") + + +def save_attachment(connection: sqlite3.Connection, order_id: int, file: UploadFile) -> None: + ext = Path(file.filename or "upload").suffix.lower() or ".jpg" + if ext not in _ALLOWED_EXTENSIONS: + raise ValueError("unsupported_file_type") + + raw = file.file.read() + if len(raw) > _MAX_UPLOAD_SIZE: + raise ValueError("file_too_large") + + detected = _detect_image_type(raw[:12]) + if detected is None: + raise ValueError("unsupported_file_type") + + relative_dir = Path(str(order_id)) + target_dir = UPLOADS_DIR / relative_dir + target_dir.mkdir(parents=True, exist_ok=True) + file_name = f"{uuid4().hex}{detected}" + target_path = target_dir / file_name + target_path.write_bytes(raw) + + connection.execute( + """ + INSERT INTO attachments (order_id, file_name, file_path, mime_type, created_at) + VALUES (?, ?, ?, ?, ?) + """, + (order_id, file.filename or file_name, str(relative_dir / file_name), f"image/{detected[1:]}", now_text()), + ) + connection.commit() + + +def confirm_order(connection: sqlite3.Connection, order_id: int, student: UserProfile) -> None: + row = _get_order_row(connection, order_id) + if row is None or row["student_id"] != student.id: + raise KeyError("order_not_found") + if row["status"] != "已完成": + raise ValueError("order_not_completed") + previous = row["status"] + connection.execute( + "UPDATE repair_orders SET status = ?, updated_at = ? WHERE id = ?", + ("已确认", now_text(), order_id), + ) + create_event( + connection, + order_id, + "student", + student.display_name, + "confirmed", + "学生确认维修完成", + None, + previous, + "已确认", + ) + connection.commit() + + +def request_rework(connection: sqlite3.Connection, order_id: int, student: UserProfile, payload: ReworkRequest) -> None: + row = _get_order_row(connection, order_id) + if row is None or row["student_id"] != student.id: + raise KeyError("order_not_found") + if row["status"] != "已完成": + raise ValueError("order_not_completed") + previous = row["status"] + connection.execute( + "UPDATE repair_orders SET status = ?, rework_reason = ?, updated_at = ? WHERE id = ?", + ("返工申请中", payload.reason, now_text(), order_id), + ) + create_event( + connection, + order_id, + "student", + student.display_name, + "rework_requested", + "学生申请返工", + payload.reason, + previous, + "返工申请中", + ) + connection.commit() + + +def add_feedback(connection: sqlite3.Connection, order_id: int, student: UserProfile, payload: FeedbackRequest) -> None: + row = _get_order_row(connection, order_id) + if row is None or row["student_id"] != student.id: + raise KeyError("order_not_found") + if row["status"] not in {"已完成", "已确认"}: + raise ValueError("order_not_ready_for_feedback") + connection.execute( + "INSERT OR REPLACE INTO feedback (order_id, rating, comment, created_at) VALUES (?, ?, ?, ?)", + (order_id, payload.rating, payload.comment, now_text()), + ) + create_event( + connection, + order_id, + "student", + student.display_name, + "feedback_added", + "学生提交评价", + payload.comment, + None, + None, + ) + connection.commit() + + +def update_order( + connection: sqlite3.Connection, order_id: int, admin: UserProfile, payload: AdminOrderUpdateRequest +) -> None: + row = _get_order_row(connection, order_id) + if row is None: + raise KeyError("order_not_found") + + previous_status = row["status"] + assignee_name = payload.assignee_name if payload.assignee_name is not None else row["assignee_name"] + expected_arrival_at = ( + payload.expected_arrival_at if payload.expected_arrival_at is not None else row["expected_arrival_at"] + ) + admin_note = payload.admin_note if payload.admin_note is not None else row["admin_note"] + status = payload.status if payload.status is not None else row["status"] + + if payload.status is not None and payload.status != row["status"]: + _validate_status_transition(row["status"], payload.status) + + connection.execute( + """ + UPDATE repair_orders + SET status = ?, assignee_name = ?, expected_arrival_at = ?, admin_note = ?, updated_at = ? + WHERE id = ? + """, + (status, assignee_name, expected_arrival_at, admin_note, now_text(), order_id), + ) + detail_parts = [] + if assignee_name: + detail_parts.append(f"负责人:{assignee_name}") + if expected_arrival_at: + detail_parts.append(f"预计上门:{expected_arrival_at}") + if admin_note: + detail_parts.append(f"备注:{admin_note}") + detail = ";".join(detail_parts) or None + create_event( + connection, + order_id, + "admin", + admin.display_name, + "admin_updated", + "管理员更新工单", + detail, + previous_status, + status, + ) + connection.commit() + + +def accept_rework(connection: sqlite3.Connection, order_id: int, admin: UserProfile) -> None: + row = _get_order_row(connection, order_id) + if row is None: + raise KeyError("order_not_found") + if row["status"] != "返工申请中": + raise ValueError("order_not_in_rework") + previous = row["status"] + connection.execute( + "UPDATE repair_orders SET status = ?, rework_reason = NULL, updated_at = ? WHERE id = ?", + ("处理中", now_text(), order_id), + ) + create_event( + connection, + order_id, + "admin", + admin.display_name, + "rework_accepted", + "管理员接受返工申请,重新安排处理", + None, + previous, + "处理中", + ) + connection.commit() + + +def reject_rework(connection: sqlite3.Connection, order_id: int, admin: UserProfile) -> None: + row = _get_order_row(connection, order_id) + if row is None: + raise KeyError("order_not_found") + if row["status"] != "返工申请中": + raise ValueError("order_not_in_rework") + previous = row["status"] + connection.execute( + "UPDATE repair_orders SET status = ?, rework_reason = NULL, updated_at = ? WHERE id = ?", + ("已完成", now_text(), order_id), + ) + create_event( + connection, + order_id, + "admin", + admin.display_name, + "rework_rejected", + "管理员拒绝返工申请,维持已完成状态", + None, + previous, + "已完成", + ) + connection.commit() + + +def cancel_order(connection: sqlite3.Connection, order_id: int, student: UserProfile) -> None: + row = _get_order_row(connection, order_id) + if row is None or row["student_id"] != student.id: + raise KeyError("order_not_found") + if row["status"] not in {"已提交", "待处理"}: + raise ValueError("order_cannot_cancel") + connection.execute( + "UPDATE repair_orders SET status = ?, updated_at = ? WHERE id = ?", + ("已取消", now_text(), order_id), + ) + create_event( + connection, + order_id, + "student", + student.display_name, + "status_updated", + "学生取消了报修工单", + None, + row["status"], + "已取消", + ) + connection.commit() + + +def get_stats(connection: sqlite3.Connection) -> dict[str, object]: + cursor = connection.cursor() + + cursor.execute( + """ + SELECT category, COUNT(*) AS cnt + FROM repair_orders + GROUP BY category + ORDER BY cnt DESC + """ + ) + category_distribution = [{"category": row["category"], "count": row["cnt"]} for row in cursor.fetchall()] + + cursor.execute( + """ + SELECT building, COUNT(*) AS cnt + FROM repair_orders + GROUP BY building + ORDER BY cnt DESC + """ + ) + building_distribution = [{"building": row["building"], "count": row["cnt"]} for row in cursor.fetchall()] + + cursor.execute( + """ + SELECT AVG( + (julianday(events.first_done_at) - julianday(orders.created_at)) * 24 + ) AS avg_hours + FROM repair_orders orders + JOIN ( + SELECT order_id, MIN(created_at) AS first_done_at + FROM order_events + WHERE to_status = '已完成' + GROUP BY order_id + ) events ON events.order_id = orders.id + """ + ) + avg_row = cursor.fetchone() + avg_hours = round(avg_row["avg_hours"], 1) if avg_row and avg_row["avg_hours"] is not None else 0 + + cursor.execute("SELECT AVG(rating) AS avg_rating FROM feedback") + rating_row = cursor.fetchone() + avg_rating = round(rating_row["avg_rating"], 1) if rating_row and rating_row["avg_rating"] is not None else 0 + + return { + "category_distribution": category_distribution, + "building_distribution": building_distribution, + "avg_processing_hours": avg_hours, + "avg_rating": avg_rating, + } diff --git a/cli.py b/cli.py new file mode 100755 index 0000000..b77d976 --- /dev/null +++ b/cli.py @@ -0,0 +1,593 @@ +#!/usr/bin/env python3 +"""宿舍报修系统 —— 统一项目管理 CLI + +用法: + ./cli.py setup 安装全部依赖 + ./cli.py start [backend|frontend|all] 启动服务 + ./cli.py stop [backend|frontend|all] 停止服务 + ./cli.py restart [backend|frontend|all] 重启服务 + ./cli.py status 查看服务状态 + ./cli.py logs [backend|frontend] [-f] 查看日志 + ./cli.py seed [--force] [--db PATH] 写入演示数据 + ./cli.py lint 运行代码检查 + ./cli.py typecheck TypeScript 类型检查 + ./cli.py build 生产构建(前端) + ./cli.py clean [--runtime|--db|--deps|--all] 清理临时文件 + ./cli.py env 创建 .env 配置 + ./cli.py docker [up|down|build] Docker 快捷操作 +""" + +from __future__ import annotations + +import argparse +import json +import os +import signal +import socket +import subprocess +import sys +import time +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +IS_WINDOWS = sys.platform == "win32" + +# Auto-activate virtualenv if present +_VENV = Path(__file__).resolve().parent / ".venv" +_VENV_PYTHON = _VENV / ("Scripts" if IS_WINDOWS else "bin") / ("python.exe" if IS_WINDOWS else "python") +if _VENV_PYTHON.is_file() and Path(sys.executable).resolve() != _VENV_PYTHON.resolve(): + os.execv(str(_VENV_PYTHON), [str(_VENV_PYTHON), *sys.argv]) + +PROJECT_ROOT = Path(__file__).resolve().parent +BACKEND_DIR = PROJECT_ROOT / "backend" +FRONTEND_DIR = PROJECT_ROOT / "frontend" +RUNTIME_DIR = PROJECT_ROOT / ".runtime" / "dev-services" +STATE_PATH = RUNTIME_DIR / "state.json" +DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S" + +# ── Color helpers ────────────────────────────────────────────── +_BOLD = "\033[1m" +_GREEN = "\033[32m" +_YELLOW = "\033[33m" +_RED = "\033[31m" +_CYAN = "\033[36m" +_RESET = "\033[0m" + + +def _green(s: str) -> str: + return f"{_GREEN}{s}{_RESET}" + + +def _yellow(s: str) -> str: + return f"{_YELLOW}{s}{_RESET}" + + +def _red(s: str) -> str: + return f"{_RED}{s}{_RESET}" + + +def _cyan(s: str) -> str: + return f"{_CYAN}{s}{_RESET}" + + +def _bold(s: str) -> str: + return f"{_BOLD}{s}{_RESET}" + + +# ── Configuration ────────────────────────────────────────────── + + +@dataclass(frozen=True) +class Config: + backend_host: str = "127.0.0.1" + backend_port: int = 8000 + frontend_host: str = "127.0.0.1" + frontend_port: int = 5173 + default_db: Path = BACKEND_DIR / "data" / "dorm_repair.sqlite3" + + @property + def backend_url(self) -> str: + return f"http://{self.backend_host}:{self.backend_port}" + + @property + def frontend_url(self) -> str: + return f"http://{self.frontend_host}:{self.frontend_port}" + + @property + def backend_health_url(self) -> str: + return f"{self.backend_url}/api/health" + + +CONFIG = Config() + + +# ── Service definitions ──────────────────────────────────────── + + +@dataclass(frozen=True) +class Service: + name: str + command: list[str] + cwd: Path + url: str + host: str + port: int + env: dict[str, str] | None = None + + @property + def log_path(self) -> Path: + return RUNTIME_DIR / f"{self.name}.log" + + +SERVICES = { + "backend": Service( + name="backend", + command=[ + "uv", + "run", + "uvicorn", + "app.main:app", + "--reload", + "--host", + CONFIG.backend_host, + "--port", + str(CONFIG.backend_port), + ], + cwd=BACKEND_DIR, + url=CONFIG.backend_health_url, + host=CONFIG.backend_host, + port=CONFIG.backend_port, + env={"UV_CACHE_DIR": str(RUNTIME_DIR / "uv-cache")}, + ), + "frontend": Service( + name="frontend", + command=[ + "pnpm", + "dev", + "--host", + CONFIG.frontend_host, + "--port", + str(CONFIG.frontend_port), + "--strictPort", + ], + cwd=FRONTEND_DIR, + url=CONFIG.frontend_url + "/", + host=CONFIG.frontend_host, + port=CONFIG.frontend_port, + ), +} + + +# ── State management ─────────────────────────────────────────── + + +def _load_state() -> dict[str, Any]: + if not STATE_PATH.exists(): + return {} + try: + return json.loads(STATE_PATH.read_text(encoding="utf-8")) + except json.JSONDecodeError: + return {} + + +def _save_state(state: dict[str, Any]) -> None: + RUNTIME_DIR.mkdir(parents=True, exist_ok=True) + STATE_PATH.write_text(json.dumps(state, indent=2, ensure_ascii=False), encoding="utf-8") + + +# ── Process helpers ──────────────────────────────────────────── + + +def _pid_running(pid: int) -> bool: + if IS_WINDOWS: + result = subprocess.run( + ["tasklist", "/FI", f"PID eq {pid}", "/NH"], + capture_output=True, + text=True, + ) + return str(pid) in result.stdout + try: + os.kill(pid, 0) + except ProcessLookupError: + return False + except PermissionError: + return True + return True + + +def _service_pid(state: dict[str, Any], svc: Service) -> int | None: + raw = state.get(svc.name, {}).get("pid") + if not isinstance(raw, int): + return None + return raw if _pid_running(raw) else None + + +def _port_open(host: str, port: int) -> bool: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.settimeout(0.2) + return sock.connect_ex((host, port)) == 0 + + +# ── Service lifecycle ────────────────────────────────────────── + + +def _start_service(svc: Service, state: dict[str, Any]) -> None: + pid = _service_pid(state, svc) + if pid is not None: + print(f" {svc.name}: 已在运行 pid={pid}") + return + + if _port_open(svc.host, svc.port): + print(f" {_yellow(svc.name)}: 端口 {svc.host}:{svc.port} 已被占用") + return + + RUNTIME_DIR.mkdir(parents=True, exist_ok=True) + log_file = svc.log_path.open("ab") + popen_kwargs: dict[str, Any] = { + "cwd": svc.cwd, + "env": {**os.environ, **(svc.env or {})}, + "stdin": subprocess.DEVNULL, + "stdout": log_file, + "stderr": subprocess.STDOUT, + } + if IS_WINDOWS and hasattr(subprocess, "CREATE_NEW_PROCESS_GROUP"): + popen_kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP + else: + popen_kwargs["start_new_session"] = True + proc = subprocess.Popen(svc.command, **popen_kwargs) + state[svc.name] = { + "pid": proc.pid, + "command": svc.command, + "cwd": str(svc.cwd), + "log": str(svc.log_path), + "url": svc.url, + "started_at": time.strftime(DATETIME_FORMAT), + } + print(f" {_green(svc.name)}: 已启动 pid={proc.pid}") + + +def _stop_service(svc: Service, state: dict[str, Any]) -> None: + pid = _service_pid(state, svc) + if pid is None: + print(f" {svc.name}: 未运行") + state.pop(svc.name, None) + return + + print(f" {svc.name}: 正在停止 pid={pid}") + + if IS_WINDOWS: + subprocess.run(["taskkill", "/PID", str(pid), "/T"], capture_output=True) + else: + try: + os.killpg(pid, signal.SIGTERM) + except ProcessLookupError: + state.pop(svc.name, None) + return + + deadline = time.time() + 8 + while time.time() < deadline: + if not _pid_running(pid): + state.pop(svc.name, None) + print(f" {svc.name}: 已停止") + return + time.sleep(0.2) + + print(f" {_yellow(svc.name)}: 未响应,强制终止") + if IS_WINDOWS: + subprocess.run(["taskkill", "/F", "/PID", str(pid), "/T"], capture_output=True) + else: + try: + os.killpg(pid, signal.SIGKILL) + except ProcessLookupError: + pass + state.pop(svc.name, None) + + +def _status_service(svc: Service, state: dict[str, Any]) -> None: + pid = _service_pid(state, svc) + if pid is None: + suffix = " (端口被占用)" if _port_open(svc.host, svc.port) else "" + print(f" {svc.name}: 未运行{suffix}") + return + print(f" {_green(svc.name)}: 运行中 pid={pid} url={svc.url}") + + +def _pick_services(names: list[str]) -> list[Service]: + if not names or "all" in names: + return list(SERVICES.values()) + selected: list[Service] = [] + for name in names: + svc = SERVICES.get(name) + if svc is None: + choices = ", ".join(["all", *SERVICES]) + raise SystemExit(f"未知服务 '{name}',可选: {choices}") + selected.append(svc) + return selected + + +# ── Commands ─────────────────────────────────────────────────── + + +def cmd_setup() -> int: + """安装后端和前端依赖。""" + print(_bold("安装后端依赖 (uv sync) ...")) + subprocess.run(["uv", "sync"], cwd=BACKEND_DIR, check=True) + print(_bold("安装前端依赖 (pnpm install) ...")) + subprocess.run(["pnpm", "install"], cwd=FRONTEND_DIR, check=True) + print(_green("安装完成。")) + return 0 + + +def cmd_start(services: list[str]) -> int: + state = _load_state() + svcs = _pick_services(services) + for svc in svcs: + _start_service(svc, state) + _save_state(state) + if svcs: + _print_urls(svcs) + return 0 + + +def _print_urls(svcs: list[Service]) -> None: + print() + for svc in svcs: + label = "后端" if svc.name == "backend" else "前端" + print(f" {label}: {_cyan(svc.url)}") + print() + + +def cmd_stop(services: list[str]) -> int: + state = _load_state() + for svc in reversed(_pick_services(services)): + _stop_service(svc, state) + _save_state(state) + return 0 + + +def cmd_restart(services: list[str]) -> int: + state = _load_state() + svcs = _pick_services(services) + for svc in reversed(svcs): + _stop_service(svc, state) + for svc in svcs: + _start_service(svc, state) + _save_state(state) + if svcs: + _print_urls(svcs) + return 0 + + +def cmd_status() -> int: + state = _load_state() + for svc in SERVICES.values(): + _status_service(svc, state) + return 0 + + +def cmd_logs(service: str, follow: bool) -> int: + svc = SERVICES.get(service) + if svc is None: + raise SystemExit(f"未知服务 '{service}',可选: backend, frontend") + log_path = svc.log_path + if not log_path.exists(): + print(f"日志文件不存在: {log_path}") + return 0 + + if follow: + _follow_log(log_path) + else: + content = log_path.read_text(encoding="utf-8", errors="replace") + for line in content.splitlines()[-50:]: + print(line) + return 0 + + +def _follow_log(path: Path) -> None: + if IS_WINDOWS: + with path.open("r", encoding="utf-8", errors="replace") as f: + f.seek(0, 2) + while True: + line = f.readline() + if line: + print(line, end="") + else: + time.sleep(0.5) + else: + subprocess.run(["tail", "-n", "50", "-f", str(path)]) + + +def cmd_seed(force: bool, db_path: str | None) -> int: + """Seed demo data via standalone seed script.""" + db = db_path or str(CONFIG.default_db) + seed_script = PROJECT_ROOT / "scripts" / "_seed.py" + print(_bold("Seeding demo data ...")) + subprocess.run( + ["uv", "run", "python", str(seed_script), "--db", db] + (["--force"] if force else []), + cwd=BACKEND_DIR, + check=True, + ) + print(f" student01 / Student123 {_cyan('(student)')}") + print(f" admin01 / Admin123 {_cyan('(admin)')}") + print(" 8 sample repair orders covering all statuses") + return 0 + + +def cmd_lint() -> int: + print(_bold("Backend lint (ruff) ...")) + subprocess.run(["uv", "run", "ruff", "check", "backend"], check=False) + subprocess.run(["uv", "run", "ruff", "format", "--check", "backend"], check=False) + print(_bold("Frontend lint (eslint) ...")) + subprocess.run(["pnpm", "lint"], cwd=FRONTEND_DIR, check=False) + return 0 + + +def cmd_typecheck() -> int: + print(_bold("TypeScript type check ...")) + subprocess.run(["pnpm", "typecheck"], cwd=FRONTEND_DIR, check=False) + return 0 + + +def cmd_build() -> int: + print(_bold("Building frontend ...")) + subprocess.run(["pnpm", "build"], cwd=FRONTEND_DIR, check=True) + print(_green(f"Output: {FRONTEND_DIR / 'dist'}/")) + return 0 + + +def cmd_clean(runtime: bool, db: bool, deps: bool, all_: bool) -> int: + if all_: + runtime = db = deps = True + + if runtime: + if RUNTIME_DIR.exists(): + import shutil + + shutil.rmtree(RUNTIME_DIR) + print(f"Removed {RUNTIME_DIR}") + else: + print("No runtime dir to clean.") + + if db: + db_path = CONFIG.default_db + if db_path.exists(): + db_path.unlink() + print(f"Removed {db_path}") + else: + print("No database to clean.") + + if deps: + for d in [BACKEND_DIR / ".venv", FRONTEND_DIR / "node_modules"]: + if d.exists(): + import shutil + + shutil.rmtree(d) + print(f"Removed {d}") + + print(_green("Clean complete.")) + return 0 + + +def cmd_env() -> int: + for template, target in [ + (BACKEND_DIR / ".env.example", BACKEND_DIR / ".env"), + (FRONTEND_DIR / ".env.example", FRONTEND_DIR / ".env"), + ]: + if not template.exists(): + print(f"Template not found: {template}") + continue + if target.exists(): + print(f"{_yellow('Skipped')} {target} (already exists)") + continue + target.write_text(template.read_text(encoding="utf-8"), encoding="utf-8") + print(f"Created {target} from {template.name}") + print(_green("Environment files ready. Edit them if needed:")) + print(f" {BACKEND_DIR / '.env'}") + print(f" {FRONTEND_DIR / '.env'}") + return 0 + + +def cmd_docker(action: str) -> int: + compose_file = PROJECT_ROOT / "docker-compose.yml" + if not compose_file.exists(): + raise SystemExit("docker-compose.yml not found.") + if action == "up": + subprocess.run(["docker", "compose", "up", "-d"], check=True) + elif action == "down": + subprocess.run(["docker", "compose", "down"], check=True) + elif action == "build": + subprocess.run(["docker", "compose", "build"], check=True) + else: + raise SystemExit(f"Unknown docker action: {action}") + return 0 + + +# ── Argument parsing ─────────────────────────────────────────── + + +def _parse_args(argv: list[str] | None = None) -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="宿舍报修系统 —— 项目管理 CLI", + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + sub = parser.add_subparsers(dest="command", required=True) + + sub.add_parser("setup", help="安装全部依赖(uv sync + pnpm install)") + + for cmd_name in ("start", "stop", "restart"): + label = {"start": "启动", "stop": "停止", "restart": "重启"}[cmd_name] + p = sub.add_parser(cmd_name, help=f"{label}服务") + p.add_argument( + "services", + nargs="*", + default=["all"], + help="目标服务:all, backend, frontend(默认: all)", + ) + + sub.add_parser("status", help="查看服务状态") + + log_p = sub.add_parser("logs", help="查看服务日志") + log_p.add_argument("service", choices=["backend", "frontend"], help="服务名") + log_p.add_argument("-f", "--follow", action="store_true", help="实时跟踪日志") + + seed_p = sub.add_parser("seed", help="写入演示数据") + seed_p.add_argument("--force", action="store_true", help="覆盖已有数据") + seed_p.add_argument("--db", type=str, default=None, help="SQLite 数据库路径") + + sub.add_parser("lint", help="运行代码检查(ruff + eslint)") + sub.add_parser("typecheck", help="TypeScript 类型检查(vue-tsc)") + sub.add_parser("build", help="生产构建(前端)") + + clean_p = sub.add_parser("clean", help="清理临时文件") + clean_p.add_argument("--runtime", action="store_true", help="删除 .runtime/") + clean_p.add_argument("--db", action="store_true", help="删除 SQLite 数据库") + clean_p.add_argument("--deps", action="store_true", help="删除 .venv 和 node_modules") + clean_p.add_argument("--all", action="store_true", help="清理全部") + + sub.add_parser("env", help="从 .env.example 创建 .env 配置文件") + + docker_p = sub.add_parser("docker", help="Docker 快捷操作") + docker_p.add_argument("action", choices=["up", "down", "build"], help="Docker 操作") + + return parser.parse_args(argv) + + +# ── Main ─────────────────────────────────────────────────────── + + +def main(argv: list[str] | None = None) -> int: + args = _parse_args(argv) + cmd = args.command + + if cmd == "setup": + return cmd_setup() + elif cmd == "start": + return cmd_start(args.services) + elif cmd == "stop": + return cmd_stop(args.services) + elif cmd == "restart": + return cmd_restart(args.services) + elif cmd == "status": + return cmd_status() + elif cmd == "logs": + return cmd_logs(args.service, args.follow) + elif cmd == "seed": + return cmd_seed(args.force, args.db) + elif cmd == "lint": + return cmd_lint() + elif cmd == "typecheck": + return cmd_typecheck() + elif cmd == "build": + return cmd_build() + elif cmd == "clean": + return cmd_clean(args.runtime, args.db, args.deps, args.all) + elif cmd == "env": + return cmd_env() + elif cmd == "docker": + return cmd_docker(args.action) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7254488 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,25 @@ +version: "3.8" + +services: + backend: + build: + context: . + dockerfile: Dockerfile.backend + ports: + - "8000:8000" + volumes: + - ./backend/data:/app/backend/data + - ./backend/uploads:/app/backend/uploads + env_file: + - backend/.env + restart: unless-stopped + + frontend: + build: + context: . + dockerfile: Dockerfile.frontend + ports: + - "80:80" + depends_on: + - backend + restart: unless-stopped diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..707a303 --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,2 @@ +# 后端 API 地址 +VITE_API_BASE_URL=http://127.0.0.1:8000 diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..96cb492 --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,43 @@ +import js from '@eslint/js' +import tseslint from 'typescript-eslint' +import vue from 'eslint-plugin-vue' +import vueParser from 'vue-eslint-parser' + +export default [ + { + ignores: ['dist/**', 'node_modules/**', '.vite/**', '*.tsbuildinfo'], + }, + { + files: ['**/*.{js,ts,vue}'], + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + globals: { + File: 'readonly', + FormData: 'readonly', + KeyboardEvent: 'readonly', + console: 'readonly', + document: 'readonly', + localStorage: 'readonly', + setTimeout: 'readonly', + }, + }, + }, + js.configs.recommended, + ...tseslint.configs.recommended, + ...vue.configs['flat/essential'], + { + files: ['**/*.vue'], + languageOptions: { + parser: vueParser, + parserOptions: { + parser: tseslint.parser, + ecmaVersion: 'latest', + sourceType: 'module', + }, + }, + rules: { + 'vue/multi-word-component-names': 'off', + }, + }, +] diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..ea89282 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,17 @@ + + + + + + 宿舍报修系统 + + + + +
+ + + diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..473e7dc --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,30 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } + + location /api/ { + proxy_pass http://backend:8000/api/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /uploads/ { + proxy_pass http://backend:8000/uploads/; + } + + location /docs { + proxy_pass http://backend:8000/docs; + } + + location /openapi.json { + proxy_pass http://backend:8000/openapi.json; + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..603cec5 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,34 @@ +{ + "name": "dorm-repair-frontend", + "private": true, + "version": "0.0.0", + "description": "交互式透明化宿舍报修系统 - Vue 3 前端", + "packageManager": "pnpm@11.5.2", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc -b && vite build", + "lint": "eslint src vite.config.ts eslint.config.js", + "preview": "vite preview", + "typecheck": "vue-tsc -b" + }, + "dependencies": { + "@element-plus/icons-vue": "^2.3.2", + "axios": "^1.17.0", + "element-plus": "^2.14.1", + "vue": "^3.5.35", + "vue-router": "^5.1.0" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/node": "^25.9.2", + "@vitejs/plugin-vue": "^6.0.7", + "eslint": "^10.4.1", + "eslint-plugin-vue": "^10.9.2", + "typescript": "^6.0.3", + "typescript-eslint": "^8.60.1", + "vite": "^8.0.16", + "vue-eslint-parser": "^10.4.1", + "vue-tsc": "^3.3.3" + } +} diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml new file mode 100644 index 0000000..e7b928e --- /dev/null +++ b/frontend/pnpm-lock.yaml @@ -0,0 +1,2388 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@element-plus/icons-vue': + specifier: ^2.3.2 + version: 2.3.2(vue@3.5.35(typescript@6.0.3)) + axios: + specifier: ^1.17.0 + version: 1.17.0 + element-plus: + specifier: ^2.14.1 + version: 2.14.1(vue@3.5.35(typescript@6.0.3)) + vue: + specifier: ^3.5.35 + version: 3.5.35(typescript@6.0.3) + vue-router: + specifier: ^5.1.0 + version: 5.1.0(@vue/compiler-sfc@3.5.35)(vite@8.0.16(@types/node@25.9.2)(yaml@2.9.0))(vue@3.5.35(typescript@6.0.3)) + devDependencies: + '@eslint/js': + specifier: ^10.0.1 + version: 10.0.1(eslint@10.4.1) + '@types/node': + specifier: ^25.9.2 + version: 25.9.2 + '@vitejs/plugin-vue': + specifier: ^6.0.7 + version: 6.0.7(vite@8.0.16(@types/node@25.9.2)(yaml@2.9.0))(vue@3.5.35(typescript@6.0.3)) + eslint: + specifier: ^10.4.1 + version: 10.4.1 + eslint-plugin-vue: + specifier: ^10.9.2 + version: 10.9.2(@typescript-eslint/parser@8.60.1(eslint@10.4.1)(typescript@6.0.3))(eslint@10.4.1)(vue-eslint-parser@10.4.1(eslint@10.4.1)) + typescript: + specifier: ^6.0.3 + version: 6.0.3 + typescript-eslint: + specifier: ^8.60.1 + version: 8.60.1(eslint@10.4.1)(typescript@6.0.3) + vite: + specifier: ^8.0.16 + version: 8.0.16(@types/node@25.9.2)(yaml@2.9.0) + vue-eslint-parser: + specifier: ^10.4.1 + version: 10.4.1(eslint@10.4.1) + vue-tsc: + specifier: ^3.3.3 + version: 3.3.3(typescript@6.0.3) + +packages: + + '@babel/generator@8.0.0-rc.6': + resolution: {integrity: sha512-6mIzgVK8DgEzvIapoQwhXTMnnkuE4STQmVv9H03i/tZ2ml8oev3TRvZJgTenK2Bsq0YWNtzOrFdTyNzCMFtjJQ==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@8.0.0-rc.6': + resolution: {integrity: sha512-BCkFy+zN6kXQed3YOT7aJl93NfDSzQc3pBfsvTVPs9gU9X3V0aefEF5kwBT0E+mDWH9QgKaZstYUQN9VdQZT4g==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@8.0.0-rc.6': + resolution: {integrity: sha512-nVJ+1JcCgntv8d78rRo++o2wuODT0Irknx2BF8Np4Ft2CRgjLqIs4qzSZ8b66yGbBdMWGmZBO9WEZv1hhNiSpg==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/parser@7.29.3': + resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/parser@8.0.0-rc.6': + resolution: {integrity: sha512-rOS8IpdO7mQELkTPlCsTgPejO0bFuZdEDCGQJouYbYf9e1FLTym7Fei2pEjq8q7MWbX0ravcd7QQYKs1TxOuog==} + engines: {node: ^22.18.0 || >=24.11.0} + hasBin: true + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@babel/types@8.0.0-rc.6': + resolution: {integrity: sha512-p7/ABylAYlexb31wtRdIfH9L9A0Z2T/9H6zAqzqndkY2PLkvNNc580wGhp/gGKN4Sp9sQvSkhc6Oga8/O+wTyw==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@ctrl/tinycolor@4.2.0': + resolution: {integrity: sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==} + engines: {node: '>=14'} + + '@element-plus/icons-vue@2.3.2': + resolution: {integrity: sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==} + peerDependencies: + vue: ^3.2.0 + + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.23.5': + resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/config-helpers@0.6.0': + resolution: {integrity: sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/core@1.2.1': + resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/js@10.0.1': + resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: ^10.0.0 + peerDependenciesMeta: + eslint: + optional: true + + '@eslint/object-schema@3.0.5': + resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/plugin-kit@0.7.2': + resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@oxc-project/types@0.133.0': + resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} + + '@rolldown/binding-android-arm64@1.0.3': + resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.3': + resolution: {integrity: sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.3': + resolution: {integrity: sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.3': + resolution: {integrity: sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.3': + resolution: {integrity: sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.3': + resolution: {integrity: sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.0.3': + resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.0.3': + resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.0.3': + resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.0.3': + resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.0.3': + resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.0.3': + resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.3': + resolution: {integrity: sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.3': + resolution: {integrity: sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.3': + resolution: {integrity: sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@sxzz/popperjs-es@2.11.8': + resolution: {integrity: sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==} + + '@tybys/wasm-util@0.10.2': + resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/jsesc@2.5.1': + resolution: {integrity: sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/lodash-es@4.17.12': + resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} + + '@types/lodash@4.17.24': + resolution: {integrity: sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==} + + '@types/node@25.9.2': + resolution: {integrity: sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw==} + + '@types/web-bluetooth@0.0.21': + resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} + + '@typescript-eslint/eslint-plugin@8.60.1': + resolution: {integrity: sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.60.1 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.60.1': + resolution: {integrity: sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.60.1': + resolution: {integrity: sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.60.1': + resolution: {integrity: sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.60.1': + resolution: {integrity: sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.60.1': + resolution: {integrity: sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.60.1': + resolution: {integrity: sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.60.1': + resolution: {integrity: sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.60.1': + resolution: {integrity: sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.60.1': + resolution: {integrity: sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitejs/plugin-vue@6.0.7': + resolution: {integrity: sha512-km+p+XdSz9Sxm5rqUbqcSfZYaAniKxWBj1KURl+Jr7UaPvvX7BmaWMdP69I5rrFDeQGyxAG7NXdc57vz+snhWg==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + vue: ^3.2.25 + + '@volar/language-core@2.4.28': + resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==} + + '@volar/source-map@2.4.28': + resolution: {integrity: sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==} + + '@volar/typescript@2.4.28': + resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==} + + '@vue-macros/common@3.1.2': + resolution: {integrity: sha512-h9t4ArDdniO9ekYHAD95t9AZcAbb19lEGK+26iAjUODOIJKmObDNBSe4+6ELQAA3vtYiFPPBtHh7+cQCKi3Dng==} + engines: {node: '>=20.19.0'} + peerDependencies: + vue: ^2.7.0 || ^3.2.25 + peerDependenciesMeta: + vue: + optional: true + + '@vue/compiler-core@3.5.34': + resolution: {integrity: sha512-s9cLyK5mLcvZ4Agva5QgRsQyLKvts9WbU9DB6NqiZkkGEdwmcEiylj5Jbwkp680drF/NNCV8OlAJSe+yMLxaJw==} + + '@vue/compiler-core@3.5.35': + resolution: {integrity: sha512-BUmHaR1J+O+CKZ9uJucdVTEr1LHsdyvv7vG3eNRhK3CczEHeMd/LtsHAuD7PbrxvI2envCY2v7HI1vC1aBRzKw==} + + '@vue/compiler-dom@3.5.34': + resolution: {integrity: sha512-EbF/T++k0e2MMZlJsBhzK8Sgwt0HcIPOhzn1CTB/lv6sQcyk+OWf8YeiLxZp3ro7MbbLcAfAJ6sEvjFWuNgUCw==} + + '@vue/compiler-dom@3.5.35': + resolution: {integrity: sha512-k+bprkXxuqhVajgTx5mUHuir7TwQzUKOWR40ng1ncAqQRPnrLngGGgqVEEhOnTMlc8btHYVKmrP8s5Qyg0hvYA==} + + '@vue/compiler-sfc@3.5.34': + resolution: {integrity: sha512-D/ihr6uZeIt6r+pVZf46RWT1fAsLFMbUP7k8G1VkiiWexriED9GrX3echHd4Abbt17zjlfiFJ8z7a3BxZOPNjg==} + + '@vue/compiler-sfc@3.5.35': + resolution: {integrity: sha512-G5VPMcXTSywXBgtFOZOnHKBxKSrwXUcvY1iaF5/hRcy7t0J6CH/d8ha9F4nzi00Fax1eLV0QHM7v4mQu68jydw==} + + '@vue/compiler-ssr@3.5.34': + resolution: {integrity: sha512-cDtTHKibkThKGHH1SP+WdccquNRYQDFH6rRjQCqT9G2ltFAfoR5pUftpab/z+aM5mW9HLLVQW7hfKKQe/1GBeQ==} + + '@vue/compiler-ssr@3.5.35': + resolution: {integrity: sha512-rGhAeXgdM7/ffTJGXT69rCCdTmjDewnFuUZfBQQHTdcEBeWdT5HCGY60y2ytLJr9/Dsu7IntUi5z/w0h6Rjnzw==} + + '@vue/devtools-api@8.1.2': + resolution: {integrity: sha512-vA0O112YqyDuNA1s7Yb2gCgToQ/OxOWiFDO5ThLCcDy0ldHnSd1dUTaSYhOldbqoNgumE4dxtGAoAaSUKUD1Zg==} + + '@vue/devtools-kit@8.1.2': + resolution: {integrity: sha512-f75/upc+GCyjXErpgPGz4582ujS0L/adAltGy+tqXMGUJpgAcfGr6CxnnhpZY8BHuMYt6KpbF8uaFrrQG66rGQ==} + + '@vue/devtools-shared@8.1.2': + resolution: {integrity: sha512-X9RyVFYAdkBe4IUf5v48TxBF/6QPmF8CmWrDAjXzfUHrgQ/HGfTC1A6TqgXqZ03ye66l3AD51BAGD69IvKM9sw==} + + '@vue/language-core@3.3.3': + resolution: {integrity: sha512-X6p+7nfY7vVT6dQwUJ+v0Jfq/lwIfhL2jMi91dQ3ln4hnlGXlxsDu/FNkeyHYgvYtyQy18ZX76IZy7X4diDbiQ==} + + '@vue/reactivity@3.5.35': + resolution: {integrity: sha512-tVc+SsHConvh/Lz64qq1pP3rYArBmK42xonovEcxY74SQtvctZodG/zhq54P5dr38cVuw25d27cPNRdlMidpGQ==} + + '@vue/runtime-core@3.5.35': + resolution: {integrity: sha512-A/xFNX9loIcWDygeQuNCfKuh0CoYBzxhqEMNah5TSFg9Z53DrFYEN2qi5CU9necjM1OWYegYREUTHmXTmhfXtg==} + + '@vue/runtime-dom@3.5.35': + resolution: {integrity: sha512-odrJ1C391dbGnyDRh8U+rnP7J2amIEzfmRk5vXy7xi3aZhEXofTvpi0T4HJb6jlNqQZTNPR5MPHSB3RHNkIORA==} + + '@vue/server-renderer@3.5.35': + resolution: {integrity: sha512-NkebSOYdB97wi8OQcO3HqzZSlymJi/aWsN/7h74OSVhRTm6qGs3Jp3e0rCXynmWwSlKeRrnlIug+ilYoHBmQDA==} + peerDependencies: + vue: 3.5.35 + + '@vue/shared@3.5.34': + resolution: {integrity: sha512-24uqU4OIiX29ryC3MeWid/Xf2fa2EFRUVLb77nRhk+UrTVrh/XiGtFAFmJBAtBRbjwNdsPRP+jj/OL27Eg1NDA==} + + '@vue/shared@3.5.35': + resolution: {integrity: sha512-zSbjL7gRXwks2ZQLRGCajBtBXEOXW9Ddhn/HvSdrGkE2dqGnumzW8XtusRrxrE9LvqtiqDXQ+A60Hp6mvdYxfA==} + + '@vueuse/core@14.3.0': + resolution: {integrity: sha512-aHfz47g0ZhMtTVHmIzMVpJy8ePhhOy68GY5bv110+5DVtZ+W7BsOx+m61UNQqfrWyPztIHIanWa3E2tib3NFIw==} + peerDependencies: + vue: ^3.5.0 + + '@vueuse/metadata@14.3.0': + resolution: {integrity: sha512-BwxmbAzwAVF50+MW57GXOUEV61nFBGnlBvrTqj49PqWJu3uw7hdu72ztXeZ33RdZtDY6kO+bfCAE1PCn88Tktw==} + + '@vueuse/shared@14.3.0': + resolution: {integrity: sha512-bZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCg==} + peerDependencies: + vue: ^3.5.0 + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + alien-signals@3.2.1: + resolution: {integrity: sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==} + + ast-kit@2.2.0: + resolution: {integrity: sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==} + engines: {node: '>=20.19.0'} + + ast-walker-scope@0.9.0: + resolution: {integrity: sha512-IJdzo2vLiElBxKzwS36VsCue/62d6IdWjnPB2v3nuPKeWGynp6FF/CYoLa5i/3jXH/z97ZDdsXz6abpgM6w07A==} + engines: {node: '>=20.19.0'} + + async-validator@4.2.5: + resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.17.0: + resolution: {integrity: sha512-J8SwNxprqqpbfenehxWYXE7CW+wM1BB4w3+N+g+/Wx40xM4rsLrfPmHHxSWIxJLYDgSY/HqlFPIYb2/S3rxafw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + birpc@2.9.0: + resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + engines: {node: 18 || 20 || >=22} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + confbox@0.2.4: + resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + dayjs@1.11.20: + resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + element-plus@2.14.1: + resolution: {integrity: sha512-UFnm1+BckNi+azkKJ7L32q1uXs9ekr99Z9pWTQPeDR05jqEWUwQq51ro4kZMVrANbjknX3Z7ukCZwTi2T6Tr9A==} + peerDependencies: + vue: ^3.3.7 + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-plugin-vue@10.9.2: + resolution: {integrity: sha512-4g7ZP3pYcuqd7Zp0pzUKcos0W+RkjBz4EGdhJ92FcYk6v03Ti/GK5NwjgsjxHK+98eXDbHeK7VtX1az7/8doZA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@stylistic/eslint-plugin': ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 + '@typescript-eslint/parser': ^7.0.0 || ^8.0.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + vue-eslint-parser: ^10.3.0 + peerDependenciesMeta: + '@stylistic/eslint-plugin': + optional: true + '@typescript-eslint/parser': + optional: true + + eslint-scope@9.1.2: + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@10.4.1: + resolution: {integrity: sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + exsolve@1.0.8: + resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.3: + resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} + engines: {node: '>= 0.4'} + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + local-pkg@1.1.2: + resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==} + engines: {node: '>=14'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash-es@4.18.1: + resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} + + lodash-unified@1.0.3: + resolution: {integrity: sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==} + peerDependencies: + '@types/lodash-es': '*' + lodash: '*' + lodash-es: '*' + + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + + magic-string-ast@1.0.3: + resolution: {integrity: sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA==} + engines: {node: '>=20.19.0'} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + memoize-one@6.0.0: + resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + mlly@1.8.2: + resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + normalize-wheel-es@1.2.0: + resolution: {integrity: sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + perfect-debounce@2.1.0: + resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + pkg-types@2.3.1: + resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==} + + postcss-selector-parser@7.1.1: + resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} + engines: {node: '>=4'} + + postcss@8.5.14: + resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} + engines: {node: ^10 || ^12 || >=14} + + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} + + rolldown@1.0.3: + resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + scule@1.3.0: + resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} + + semver@7.8.1: + resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typescript-eslint@8.60.1: + resolution: {integrity: sha512-6m5hkkRAp8lKvhVpcprAIn5KkehQEh+47oHH2VGnExEh7dhNxXlg6GPAOIu6TxbVQxhebrJDvjl3020ooiWCMA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.4: + resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} + + undici-types@7.24.6: + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + + unplugin-utils@0.3.1: + resolution: {integrity: sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==} + engines: {node: '>=20.19.0'} + + unplugin@3.0.0: + resolution: {integrity: sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==} + engines: {node: ^20.19.0 || >=22.12.0} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vite@8.0.16: + resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.18 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + + vue-component-type-helpers@3.3.1: + resolution: {integrity: sha512-pu58kqxmVyEH6VfNYW1UyEfR3XAnJ27ZXT3yzXxxpjLxVzAbyC35Zk/nm/RMs7ijWnJNSd9fWkeex2OhUsx3MA==} + + vue-eslint-parser@10.4.1: + resolution: {integrity: sha512-Gk6gRDj0n/fkRa3C3l0bBheoBckUq/Rs0F/TvMWIS6nzzx67amAViMe9CkNgsP2tXyQONvGiHQESHwFtZ3aYDA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + + vue-router@5.1.0: + resolution: {integrity: sha512-HAbiLzLEHQwxPgvsbOJDAwtavszEgLwri6XfyrsPECIFez8+59xc9LofWVdc/HEaSRT822lJ8H9Ns38VVond5g==} + peerDependencies: + '@pinia/colada': '>=0.21.2' + '@vue/compiler-sfc': ^3.5.34 + pinia: ^3.0.4 + vite: ^7.0.0 || ^8.0.0 + vue: ^3.5.34 + peerDependenciesMeta: + '@pinia/colada': + optional: true + '@vue/compiler-sfc': + optional: true + pinia: + optional: true + vite: + optional: true + + vue-tsc@3.3.3: + resolution: {integrity: sha512-SWUEG7YRUeDJHT7Xsuhf02elYX2gxPzzAII7OxDAh4KNOr4QHQ0Lls0YfnaO5GNd560CwVa2HTfdqmA5MqvRqQ==} + hasBin: true + peerDependencies: + typescript: '>=5.0.0' + + vue@3.5.35: + resolution: {integrity: sha512-cx89fnr+0kVGHiNFG6y6s0bdjypJRFNZn6x3WPstNdQR1bi1mbB7h4v5IBGTsPJU3nK1+0Iqj3Zf+hZWMieR4Q==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + xml-name-validator@4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@babel/generator@8.0.0-rc.6': + dependencies: + '@babel/parser': 8.0.0-rc.6 + '@babel/types': 8.0.0-rc.6 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + '@types/jsesc': 2.5.1 + jsesc: 3.1.0 + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-string-parser@8.0.0-rc.6': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-identifier@8.0.0-rc.6': {} + + '@babel/parser@7.29.3': + dependencies: + '@babel/types': 7.29.0 + + '@babel/parser@8.0.0-rc.6': + dependencies: + '@babel/types': 8.0.0-rc.6 + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@babel/types@8.0.0-rc.6': + dependencies: + '@babel/helper-string-parser': 8.0.0-rc.6 + '@babel/helper-validator-identifier': 8.0.0-rc.6 + + '@ctrl/tinycolor@4.2.0': {} + + '@element-plus/icons-vue@2.3.2(vue@3.5.35(typescript@6.0.3))': + dependencies: + vue: 3.5.35(typescript@6.0.3) + + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@10.4.1)': + dependencies: + eslint: 10.4.1 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.23.5': + dependencies: + '@eslint/object-schema': 3.0.5 + debug: 4.4.3 + minimatch: 10.2.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.6.0': + dependencies: + '@eslint/core': 1.2.1 + + '@eslint/core@1.2.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/js@10.0.1(eslint@10.4.1)': + optionalDependencies: + eslint: 10.4.1 + + '@eslint/object-schema@3.0.5': {} + + '@eslint/plugin-kit@0.7.2': + dependencies: + '@eslint/core': 1.2.1 + levn: 0.4.1 + + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/utils@0.2.11': {} + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.2 + optional: true + + '@oxc-project/types@0.133.0': {} + + '@rolldown/binding-android-arm64@1.0.3': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.3': + optional: true + + '@rolldown/binding-darwin-x64@1.0.3': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.3': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.3': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.3': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.3': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.3': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.3': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.3': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.3': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.3': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.3': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.3': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.3': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@sxzz/popperjs-es@2.11.8': {} + + '@tybys/wasm-util@0.10.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/esrecurse@4.3.1': {} + + '@types/estree@1.0.9': {} + + '@types/jsesc@2.5.1': {} + + '@types/json-schema@7.0.15': {} + + '@types/lodash-es@4.17.12': + dependencies: + '@types/lodash': 4.17.24 + + '@types/lodash@4.17.24': {} + + '@types/node@25.9.2': + dependencies: + undici-types: 7.24.6 + + '@types/web-bluetooth@0.0.21': {} + + '@typescript-eslint/eslint-plugin@8.60.1(@typescript-eslint/parser@8.60.1(eslint@10.4.1)(typescript@6.0.3))(eslint@10.4.1)(typescript@6.0.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.60.1(eslint@10.4.1)(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.60.1 + '@typescript-eslint/type-utils': 8.60.1(eslint@10.4.1)(typescript@6.0.3) + '@typescript-eslint/utils': 8.60.1(eslint@10.4.1)(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.60.1 + eslint: 10.4.1 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.60.1(eslint@10.4.1)(typescript@6.0.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.60.1 + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/typescript-estree': 8.60.1(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.60.1 + debug: 4.4.3 + eslint: 10.4.1 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.60.1(typescript@6.0.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.60.1(typescript@6.0.3) + '@typescript-eslint/types': 8.60.1 + debug: 4.4.3 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.60.1': + dependencies: + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/visitor-keys': 8.60.1 + + '@typescript-eslint/tsconfig-utils@8.60.1(typescript@6.0.3)': + dependencies: + typescript: 6.0.3 + + '@typescript-eslint/type-utils@8.60.1(eslint@10.4.1)(typescript@6.0.3)': + dependencies: + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/typescript-estree': 8.60.1(typescript@6.0.3) + '@typescript-eslint/utils': 8.60.1(eslint@10.4.1)(typescript@6.0.3) + debug: 4.4.3 + eslint: 10.4.1 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.60.1': {} + + '@typescript-eslint/typescript-estree@8.60.1(typescript@6.0.3)': + dependencies: + '@typescript-eslint/project-service': 8.60.1(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.60.1(typescript@6.0.3) + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/visitor-keys': 8.60.1 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.8.1 + tinyglobby: 0.2.16 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.60.1(eslint@10.4.1)(typescript@6.0.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1) + '@typescript-eslint/scope-manager': 8.60.1 + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/typescript-estree': 8.60.1(typescript@6.0.3) + eslint: 10.4.1 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.60.1': + dependencies: + '@typescript-eslint/types': 8.60.1 + eslint-visitor-keys: 5.0.1 + + '@vitejs/plugin-vue@6.0.7(vite@8.0.16(@types/node@25.9.2)(yaml@2.9.0))(vue@3.5.35(typescript@6.0.3))': + dependencies: + '@rolldown/pluginutils': 1.0.1 + vite: 8.0.16(@types/node@25.9.2)(yaml@2.9.0) + vue: 3.5.35(typescript@6.0.3) + + '@volar/language-core@2.4.28': + dependencies: + '@volar/source-map': 2.4.28 + + '@volar/source-map@2.4.28': {} + + '@volar/typescript@2.4.28': + dependencies: + '@volar/language-core': 2.4.28 + path-browserify: 1.0.1 + vscode-uri: 3.1.0 + + '@vue-macros/common@3.1.2(vue@3.5.35(typescript@6.0.3))': + dependencies: + '@vue/compiler-sfc': 3.5.34 + ast-kit: 2.2.0 + local-pkg: 1.1.2 + magic-string-ast: 1.0.3 + unplugin-utils: 0.3.1 + optionalDependencies: + vue: 3.5.35(typescript@6.0.3) + + '@vue/compiler-core@3.5.34': + dependencies: + '@babel/parser': 7.29.3 + '@vue/shared': 3.5.34 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-core@3.5.35': + dependencies: + '@babel/parser': 7.29.3 + '@vue/shared': 3.5.35 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.34': + dependencies: + '@vue/compiler-core': 3.5.34 + '@vue/shared': 3.5.34 + + '@vue/compiler-dom@3.5.35': + dependencies: + '@vue/compiler-core': 3.5.35 + '@vue/shared': 3.5.35 + + '@vue/compiler-sfc@3.5.34': + dependencies: + '@babel/parser': 7.29.3 + '@vue/compiler-core': 3.5.34 + '@vue/compiler-dom': 3.5.34 + '@vue/compiler-ssr': 3.5.34 + '@vue/shared': 3.5.34 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.14 + source-map-js: 1.2.1 + + '@vue/compiler-sfc@3.5.35': + dependencies: + '@babel/parser': 7.29.3 + '@vue/compiler-core': 3.5.35 + '@vue/compiler-dom': 3.5.35 + '@vue/compiler-ssr': 3.5.35 + '@vue/shared': 3.5.35 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.15 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.34': + dependencies: + '@vue/compiler-dom': 3.5.34 + '@vue/shared': 3.5.34 + + '@vue/compiler-ssr@3.5.35': + dependencies: + '@vue/compiler-dom': 3.5.35 + '@vue/shared': 3.5.35 + + '@vue/devtools-api@8.1.2': + dependencies: + '@vue/devtools-kit': 8.1.2 + + '@vue/devtools-kit@8.1.2': + dependencies: + '@vue/devtools-shared': 8.1.2 + birpc: 2.9.0 + hookable: 5.5.3 + perfect-debounce: 2.1.0 + + '@vue/devtools-shared@8.1.2': {} + + '@vue/language-core@3.3.3': + dependencies: + '@volar/language-core': 2.4.28 + '@vue/compiler-dom': 3.5.34 + '@vue/shared': 3.5.34 + alien-signals: 3.2.1 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + picomatch: 4.0.4 + + '@vue/reactivity@3.5.35': + dependencies: + '@vue/shared': 3.5.35 + + '@vue/runtime-core@3.5.35': + dependencies: + '@vue/reactivity': 3.5.35 + '@vue/shared': 3.5.35 + + '@vue/runtime-dom@3.5.35': + dependencies: + '@vue/reactivity': 3.5.35 + '@vue/runtime-core': 3.5.35 + '@vue/shared': 3.5.35 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.35(vue@3.5.35(typescript@6.0.3))': + dependencies: + '@vue/compiler-ssr': 3.5.35 + '@vue/shared': 3.5.35 + vue: 3.5.35(typescript@6.0.3) + + '@vue/shared@3.5.34': {} + + '@vue/shared@3.5.35': {} + + '@vueuse/core@14.3.0(vue@3.5.35(typescript@6.0.3))': + dependencies: + '@types/web-bluetooth': 0.0.21 + '@vueuse/metadata': 14.3.0 + '@vueuse/shared': 14.3.0(vue@3.5.35(typescript@6.0.3)) + vue: 3.5.35(typescript@6.0.3) + + '@vueuse/metadata@14.3.0': {} + + '@vueuse/shared@14.3.0(vue@3.5.35(typescript@6.0.3))': + dependencies: + vue: 3.5.35(typescript@6.0.3) + + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + agent-base@6.0.2: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + alien-signals@3.2.1: {} + + ast-kit@2.2.0: + dependencies: + '@babel/parser': 7.29.3 + pathe: 2.0.3 + + ast-walker-scope@0.9.0: + dependencies: + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + ast-kit: 2.2.0 + + async-validator@4.2.5: {} + + asynckit@0.4.0: {} + + axios@1.17.0: + dependencies: + follow-redirects: 1.16.0 + form-data: 4.0.5 + https-proxy-agent: 5.0.1 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + - supports-color + + balanced-match@4.0.4: {} + + birpc@2.9.0: {} + + boolbase@1.0.0: {} + + brace-expansion@5.0.6: + dependencies: + balanced-match: 4.0.4 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + confbox@0.1.8: {} + + confbox@0.2.4: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cssesc@3.0.0: {} + + csstype@3.2.3: {} + + dayjs@1.11.20: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + delayed-stream@1.0.0: {} + + detect-libc@2.1.2: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + element-plus@2.14.1(vue@3.5.35(typescript@6.0.3)): + dependencies: + '@ctrl/tinycolor': 4.2.0 + '@element-plus/icons-vue': 2.3.2(vue@3.5.35(typescript@6.0.3)) + '@floating-ui/dom': 1.7.6 + '@popperjs/core': '@sxzz/popperjs-es@2.11.8' + '@types/lodash': 4.17.24 + '@types/lodash-es': 4.17.12 + '@vueuse/core': 14.3.0(vue@3.5.35(typescript@6.0.3)) + async-validator: 4.2.5 + dayjs: 1.11.20 + lodash: 4.18.1 + lodash-es: 4.18.1 + lodash-unified: 1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.18.1)(lodash@4.18.1) + memoize-one: 6.0.0 + normalize-wheel-es: 1.2.0 + vue: 3.5.35(typescript@6.0.3) + vue-component-type-helpers: 3.3.1 + + entities@7.0.1: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.3 + + escape-string-regexp@4.0.0: {} + + eslint-plugin-vue@10.9.2(@typescript-eslint/parser@8.60.1(eslint@10.4.1)(typescript@6.0.3))(eslint@10.4.1)(vue-eslint-parser@10.4.1(eslint@10.4.1)): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1) + eslint: 10.4.1 + natural-compare: 1.4.0 + nth-check: 2.1.1 + postcss-selector-parser: 7.1.1 + semver: 7.8.1 + vue-eslint-parser: 10.4.1(eslint@10.4.1) + xml-name-validator: 4.0.0 + optionalDependencies: + '@typescript-eslint/parser': 8.60.1(eslint@10.4.1)(typescript@6.0.3) + + eslint-scope@9.1.2: + dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.9 + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@10.4.1: + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.23.5 + '@eslint/config-helpers': 0.6.0 + '@eslint/core': 1.2.1 + '@eslint/plugin-kit': 0.7.2 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + minimatch: 10.2.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@11.2.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 5.0.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@2.0.2: {} + + esutils@2.0.3: {} + + exsolve@1.0.8: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + + flatted@3.4.2: {} + + follow-redirects@1.16.0: {} + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.3 + mime-types: 2.1.35 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + 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.3 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + gopd@1.2.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.3: + dependencies: + function-bind: 1.1.2 + + hookable@5.5.3: {} + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + imurmurhash@0.1.4: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + isexe@2.0.0: {} + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + local-pkg@1.1.2: + dependencies: + mlly: 1.8.2 + pkg-types: 2.3.1 + quansync: 0.2.11 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash-es@4.18.1: {} + + lodash-unified@1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.18.1)(lodash@4.18.1): + dependencies: + '@types/lodash-es': 4.17.12 + lodash: 4.18.1 + lodash-es: 4.18.1 + + lodash@4.18.1: {} + + magic-string-ast@1.0.3: + dependencies: + magic-string: 0.30.21 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + math-intrinsics@1.1.0: {} + + memoize-one@6.0.0: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.6 + + mlly@1.8.2: + dependencies: + acorn: 8.16.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.4 + + ms@2.1.3: {} + + muggle-string@0.4.1: {} + + nanoid@3.3.12: {} + + natural-compare@1.4.0: {} + + normalize-wheel-es@1.2.0: {} + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + path-browserify@1.0.1: {} + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + pathe@2.0.3: {} + + perfect-debounce@2.1.0: {} + + picocolors@1.1.1: {} + + picomatch@4.0.4: {} + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.2 + pathe: 2.0.3 + + pkg-types@2.3.1: + dependencies: + confbox: 0.2.4 + exsolve: 1.0.8 + pathe: 2.0.3 + + postcss-selector-parser@7.1.1: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss@8.5.14: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postcss@8.5.15: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + proxy-from-env@2.1.0: {} + + punycode@2.3.1: {} + + quansync@0.2.11: {} + + readdirp@5.0.0: {} + + rolldown@1.0.3: + dependencies: + '@oxc-project/types': 0.133.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.3 + '@rolldown/binding-darwin-arm64': 1.0.3 + '@rolldown/binding-darwin-x64': 1.0.3 + '@rolldown/binding-freebsd-x64': 1.0.3 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.3 + '@rolldown/binding-linux-arm64-gnu': 1.0.3 + '@rolldown/binding-linux-arm64-musl': 1.0.3 + '@rolldown/binding-linux-ppc64-gnu': 1.0.3 + '@rolldown/binding-linux-s390x-gnu': 1.0.3 + '@rolldown/binding-linux-x64-gnu': 1.0.3 + '@rolldown/binding-linux-x64-musl': 1.0.3 + '@rolldown/binding-openharmony-arm64': 1.0.3 + '@rolldown/binding-wasm32-wasi': 1.0.3 + '@rolldown/binding-win32-arm64-msvc': 1.0.3 + '@rolldown/binding-win32-x64-msvc': 1.0.3 + + scule@1.3.0: {} + + semver@7.8.1: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + source-map-js@1.2.1: {} + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + ts-api-utils@2.5.0(typescript@6.0.3): + dependencies: + typescript: 6.0.3 + + tslib@2.8.1: + optional: true + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typescript-eslint@8.60.1(eslint@10.4.1)(typescript@6.0.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.60.1(@typescript-eslint/parser@8.60.1(eslint@10.4.1)(typescript@6.0.3))(eslint@10.4.1)(typescript@6.0.3) + '@typescript-eslint/parser': 8.60.1(eslint@10.4.1)(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.60.1(typescript@6.0.3) + '@typescript-eslint/utils': 8.60.1(eslint@10.4.1)(typescript@6.0.3) + eslint: 10.4.1 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + typescript@6.0.3: {} + + ufo@1.6.4: {} + + undici-types@7.24.6: {} + + unplugin-utils@0.3.1: + dependencies: + pathe: 2.0.3 + picomatch: 4.0.4 + + unplugin@3.0.0: + dependencies: + '@jridgewell/remapping': 2.3.5 + picomatch: 4.0.4 + webpack-virtual-modules: 0.6.2 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + util-deprecate@1.0.2: {} + + vite@8.0.16(@types/node@25.9.2)(yaml@2.9.0): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.15 + rolldown: 1.0.3 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 25.9.2 + fsevents: 2.3.3 + yaml: 2.9.0 + + vscode-uri@3.1.0: {} + + vue-component-type-helpers@3.3.1: {} + + vue-eslint-parser@10.4.1(eslint@10.4.1): + dependencies: + debug: 4.4.3 + eslint: 10.4.1 + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 + esquery: 1.7.0 + semver: 7.8.1 + transitivePeerDependencies: + - supports-color + + vue-router@5.1.0(@vue/compiler-sfc@3.5.35)(vite@8.0.16(@types/node@25.9.2)(yaml@2.9.0))(vue@3.5.35(typescript@6.0.3)): + dependencies: + '@babel/generator': 8.0.0-rc.6 + '@vue-macros/common': 3.1.2(vue@3.5.35(typescript@6.0.3)) + '@vue/devtools-api': 8.1.2 + ast-walker-scope: 0.9.0 + chokidar: 5.0.0 + json5: 2.2.3 + local-pkg: 1.1.2 + magic-string: 0.30.21 + mlly: 1.8.2 + muggle-string: 0.4.1 + pathe: 2.0.3 + picomatch: 4.0.4 + scule: 1.3.0 + tinyglobby: 0.2.16 + unplugin: 3.0.0 + unplugin-utils: 0.3.1 + vue: 3.5.35(typescript@6.0.3) + yaml: 2.9.0 + optionalDependencies: + '@vue/compiler-sfc': 3.5.35 + vite: 8.0.16(@types/node@25.9.2)(yaml@2.9.0) + + vue-tsc@3.3.3(typescript@6.0.3): + dependencies: + '@volar/typescript': 2.4.28 + '@vue/language-core': 3.3.3 + typescript: 6.0.3 + + vue@3.5.35(typescript@6.0.3): + dependencies: + '@vue/compiler-dom': 3.5.35 + '@vue/compiler-sfc': 3.5.35 + '@vue/runtime-dom': 3.5.35 + '@vue/server-renderer': 3.5.35(vue@3.5.35(typescript@6.0.3)) + '@vue/shared': 3.5.35 + optionalDependencies: + typescript: 6.0.3 + + webpack-virtual-modules@0.6.2: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + xml-name-validator@4.0.0: {} + + yaml@2.9.0: {} + + yocto-queue@0.1.0: {} diff --git a/frontend/pnpm-workspace.yaml b/frontend/pnpm-workspace.yaml new file mode 100644 index 0000000..7b97578 --- /dev/null +++ b/frontend/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +minimumReleaseAgeExclude: + - '@types/node@25.9.2' diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..4be8965 --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,4 @@ + + diff --git a/frontend/src/assets/ref/empty-state.png b/frontend/src/assets/ref/empty-state.png new file mode 100644 index 0000000000000000000000000000000000000000..8b453d0aff3de64493d0ebff609dfb608ce08c9c GIT binary patch literal 14216 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 +import { computed } from 'vue' +import { useRouter } from 'vue-router' +import { SwitchButton, UserFilled } from '@element-plus/icons-vue' + +import oucLogoUrl from '@/assets/ref/ouc-logo.png' +import { clearSession, getUser } from '@/lib/auth' + +const props = defineProps<{ + accent?: 'student' | 'admin' +}>() + +const router = useRouter() +const user = computed(() => getUser()) +const roleText = computed(() => (props.accent === 'admin' ? '后勤管理端' : '学生服务端')) + +function goHome() { + router.push(props.accent === 'admin' ? '/admin/orders' : '/student/home') +} + +function logout() { + clearSession() + router.push('/login') +} + + + diff --git a/frontend/src/components/StatusBadge.vue b/frontend/src/components/StatusBadge.vue new file mode 100644 index 0000000..a0ec391 --- /dev/null +++ b/frontend/src/components/StatusBadge.vue @@ -0,0 +1,9 @@ + + + diff --git a/frontend/src/components/TimelineList.vue b/frontend/src/components/TimelineList.vue new file mode 100644 index 0000000..0d5587e --- /dev/null +++ b/frontend/src/components/TimelineList.vue @@ -0,0 +1,23 @@ + + + diff --git a/frontend/src/env.d.ts b/frontend/src/env.d.ts new file mode 100644 index 0000000..ab333dd --- /dev/null +++ b/frontend/src/env.d.ts @@ -0,0 +1,8 @@ +/// + +declare module '*.vue' { + import type { DefineComponent } from 'vue' + + const component: DefineComponent, Record, unknown> + export default component +} diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts new file mode 100644 index 0000000..ef54ea2 --- /dev/null +++ b/frontend/src/lib/api.ts @@ -0,0 +1,31 @@ +import axios from 'axios' + +import { clearSession, getToken } from '@/lib/auth' + +const baseURL = import.meta.env.VITE_API_BASE_URL as string || 'http://127.0.0.1:8000' + +const api = axios.create({ baseURL }) + +api.interceptors.request.use((config) => { + const token = getToken() + if (token) { + config.headers.Authorization = `Bearer ${token}` + } + return config +}) + +api.interceptors.response.use( + (response) => response, + (error) => { + if (error.response?.status === 401) { + clearSession() + } + return Promise.reject(error) + }, +) + +export function getUploadUrl(filePath: string): string { + return `${baseURL}/uploads/${filePath}` +} + +export default api diff --git a/frontend/src/lib/auth.ts b/frontend/src/lib/auth.ts new file mode 100644 index 0000000..1dceb68 --- /dev/null +++ b/frontend/src/lib/auth.ts @@ -0,0 +1,31 @@ +import type { UserProfile } from '@/types' + +const TOKEN_KEY = 'dorm-repair-token' +const USER_KEY = 'dorm-repair-user' + +export function getToken(): string | null { + return localStorage.getItem(TOKEN_KEY) +} + +export function getUser(): UserProfile | null { + const raw = localStorage.getItem(USER_KEY) + if (!raw) { + return null + } + try { + return JSON.parse(raw) as UserProfile + } catch { + clearSession() + return null + } +} + +export function saveSession(token: string, user: UserProfile): void { + localStorage.setItem(TOKEN_KEY, token) + localStorage.setItem(USER_KEY, JSON.stringify(user)) +} + +export function clearSession(): void { + localStorage.removeItem(TOKEN_KEY) + localStorage.removeItem(USER_KEY) +} diff --git a/frontend/src/lib/errors.ts b/frontend/src/lib/errors.ts new file mode 100644 index 0000000..79afc41 --- /dev/null +++ b/frontend/src/lib/errors.ts @@ -0,0 +1,51 @@ +const ERROR_MESSAGES: Record = { + missing_token: '缺少认证令牌', + invalid_token: '认证已过期,请重新登录', + invalid_credentials: '用户名或密码错误', + student_only: '此功能仅限学生使用', + admin_only: '此功能仅限管理员使用', + order_not_found: '工单不存在或已被删除', + session_not_found: '诊断会话已过期,请重新描述故障', + order_not_completed: '维修尚未完成,无法执行此操作', + order_not_ready_for_feedback: '当前状态不可评价', + order_cannot_cancel: '当前状态无法取消,仅已提交或待处理的工单可取消', + unsupported_file_type: '仅支持上传图片文件(jpg, png, gif, webp)', + file_too_large: '文件大小超过限制(最大10MB)', + order_not_in_rework: '当前工单不在返工申请状态', + validation_error: '请求参数有误', + internal_error: '服务器内部错误', + unauthorized: '未登录或登录已过期', + forbidden: '无权访问此资源', +} + +const STATUS_MESSAGES: Record = { + 400: '请求有误', + 401: '未登录或登录已过期', + 403: '无权访问', + 404: '请求的资源不存在', + 422: '请求参数有误', + 500: '服务器内部错误', +} + +export function getErrorMessage(error: unknown): string { + if (error && typeof error === 'object' && 'response' in error) { + const axiosError = error as { response?: { data?: { detail?: string; error_code?: string }; status?: number } } + const data = axiosError.response?.data + if (data?.error_code && ERROR_MESSAGES[data.error_code]) { + return ERROR_MESSAGES[data.error_code] + } + if (data?.detail) { + return data.detail + } + const status = axiosError.response?.status + if (status && STATUS_MESSAGES[status]) { + return STATUS_MESSAGES[status] + } + } + if (error instanceof Error) { + if (error.message === 'Network Error' || error.message.includes('ERR_NETWORK')) { + return '网络连接失败,请检查网络后重试' + } + } + return '操作失败,请稍后重试' +} diff --git a/frontend/src/lib/status.ts b/frontend/src/lib/status.ts new file mode 100644 index 0000000..0f6c066 --- /dev/null +++ b/frontend/src/lib/status.ts @@ -0,0 +1,2 @@ +export const ACTIVE_STATUSES = new Set(['已提交', '待处理', '处理中', '待上门', '返工申请中']) +export const CLOSED_STATUSES = new Set(['已完成', '已确认', '已取消']) diff --git a/frontend/src/main.ts b/frontend/src/main.ts new file mode 100644 index 0000000..0fd5934 --- /dev/null +++ b/frontend/src/main.ts @@ -0,0 +1,74 @@ +import { createApp } from 'vue' +import { + ElAvatar, + ElButton, + ElCard, + ElDropdown, + ElDropdownItem, + ElDropdownMenu, + ElEmpty, + ElIcon, + ElInput, + ElOption, + ElRate, + ElSelect, + ElSkeleton, + ElTable, + ElTableColumn, + ElTag, + ElTooltip, + ElUpload, + vLoading, +} from 'element-plus' +import 'element-plus/theme-chalk/base.css' +import 'element-plus/theme-chalk/el-avatar.css' +import 'element-plus/theme-chalk/el-button.css' +import 'element-plus/theme-chalk/el-card.css' +import 'element-plus/theme-chalk/el-dropdown.css' +import 'element-plus/theme-chalk/el-dropdown-menu.css' +import 'element-plus/theme-chalk/el-empty.css' +import 'element-plus/theme-chalk/el-icon.css' +import 'element-plus/theme-chalk/el-input.css' +import 'element-plus/theme-chalk/el-loading.css' +import 'element-plus/theme-chalk/el-message.css' +import 'element-plus/theme-chalk/el-message-box.css' +import 'element-plus/theme-chalk/el-option.css' +import 'element-plus/theme-chalk/el-overlay.css' +import 'element-plus/theme-chalk/el-popper.css' +import 'element-plus/theme-chalk/el-rate.css' +import 'element-plus/theme-chalk/el-select.css' +import 'element-plus/theme-chalk/el-skeleton.css' +import 'element-plus/theme-chalk/el-table.css' +import 'element-plus/theme-chalk/el-table-column.css' +import 'element-plus/theme-chalk/el-tag.css' +import 'element-plus/theme-chalk/el-tooltip.css' +import 'element-plus/theme-chalk/el-upload.css' + +import App from './App.vue' +import router from './router' +import './style.css' + +const app = createApp(App) + +app + .use(router) + .use(ElAvatar) + .use(ElButton) + .use(ElCard) + .use(ElDropdown) + .use(ElDropdownItem) + .use(ElDropdownMenu) + .use(ElEmpty) + .use(ElIcon) + .use(ElInput) + .use(ElOption) + .use(ElRate) + .use(ElSelect) + .use(ElSkeleton) + .use(ElTable) + .use(ElTableColumn) + .use(ElTag) + .use(ElTooltip) + .use(ElUpload) + .directive('loading', vLoading) + .mount('#app') diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts new file mode 100644 index 0000000..6bd56eb --- /dev/null +++ b/frontend/src/router/index.ts @@ -0,0 +1,76 @@ +import { createRouter, createWebHistory } from 'vue-router' + +import LoginView from '@/views/LoginView.vue' +import StudentHomeView from '@/views/StudentHomeView.vue' +import StudentReportView from '@/views/StudentReportView.vue' +import StudentOrdersView from '@/views/StudentOrdersView.vue' +import StudentOrderDetailView from '@/views/StudentOrderDetailView.vue' +import AdminOrdersView from '@/views/AdminOrdersView.vue' +import { getUser } from '@/lib/auth' + +const router = createRouter({ + history: createWebHistory(), + routes: [ + { + path: '/', + redirect: '/login', + }, + { + path: '/login', + name: 'login', + component: LoginView, + }, + { + path: '/student/home', + name: 'student-home', + component: StudentHomeView, + meta: { requiresAuth: true, role: 'student' }, + }, + { + path: '/student/report', + name: 'student-report', + component: StudentReportView, + meta: { requiresAuth: true, role: 'student' }, + }, + { + path: '/student/orders', + name: 'student-orders', + component: StudentOrdersView, + meta: { requiresAuth: true, role: 'student' }, + }, + { + path: '/student/orders/:id', + name: 'student-order-detail', + component: StudentOrderDetailView, + meta: { requiresAuth: true, role: 'student' }, + }, + { + path: '/admin/orders', + name: 'admin-orders', + component: AdminOrdersView, + meta: { requiresAuth: true, role: 'admin' }, + }, + ], +}) + +router.beforeEach((to) => { + const user = getUser() + const requiresAuth = Boolean(to.meta.requiresAuth) + const expectedRole = to.meta.role as 'student' | 'admin' | undefined + + if (requiresAuth && !user) { + return '/login' + } + if (to.path === '/login' && user) { + return user.role === 'admin' ? '/admin/orders' : '/student/home' + } + if (expectedRole && user?.role !== expectedRole) { + if (!user) { + return '/login' + } + return user.role === 'admin' ? '/admin/orders' : '/student/home' + } + return true +}) + +export default router diff --git a/frontend/src/style.css b/frontend/src/style.css new file mode 100644 index 0000000..bf2cc07 --- /dev/null +++ b/frontend/src/style.css @@ -0,0 +1,1656 @@ +:root { + font-family: "Noto Sans SC", "Microsoft YaHei", Arial, sans-serif; + color: #1f2937; + background: #f4f7fb; + --blue: #004098; + --blue-dark: #00337a; + --blue-light: #e6ecf5; + --cyan: #00a0e9; + --green: #12a88a; + --orange: #d97706; + --red: #c43f4a; + --text: #1f2937; + --muted: #475569; + --border: #d9e2ec; + --card: #ffffff; + --bg: #f4f7fb; + --el-color-primary: #004098; + --el-color-primary-light-3: #4d79b7; + --el-color-primary-light-5: #80a0cc; + --el-color-primary-light-7: #b3c6e0; + --el-color-primary-light-8: #ccd9ea; + --el-color-primary-light-9: #e6ecf5; + --el-color-primary-dark-2: #00337a; + --el-border-radius-base: 8px; + --el-font-family: "Noto Sans SC", "Microsoft YaHei", Arial, sans-serif; + + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.03), 0 1px 4px rgba(0, 64, 152, 0.03); + --shadow-md: 0 2px 8px rgba(0, 0, 0, 0.04), 0 4px 16px rgba(0, 64, 152, 0.05); + --shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.06), 0 8px 24px rgba(0, 64, 152, 0.06); + --fs-xl: 28px; + --fs-lg: 22px; + --fs-base: 16px; + --fs-sm: 14px; + --fs-xs: 12px; + --spacing-xs: 8px; + --spacing-sm: 12px; + --spacing-md: 16px; + --spacing-lg: 20px; + --spacing-xl: 24px; + --spacing-2xl: 32px; +} + +* { + box-sizing: border-box; +} + +html, +body, +#app { + margin: 0; + min-height: 100%; +} + +body { + background: var(--bg); +} + +a { + color: inherit; + text-decoration: none; +} + +button, +input, +textarea, +select { + font: inherit; +} + +button { + appearance: none; +} + +/* Element Plus common tweaks */ +.el-card { + border: 1px solid var(--border); + border-radius: 12px; + box-shadow: var(--shadow-sm); +} + +.card, +.flat-section, +.banner { + box-shadow: var(--shadow-sm); +} + +.admin-command, +.portal-profile-card { + box-shadow: var(--shadow-md); +} + +.order-card { + transition: box-shadow 0.2s ease, transform 0.2s ease; +} + +.el-card__header { + padding: 16px 20px; +} + +.el-card__body { + padding: var(--spacing-lg); +} + +.el-input__wrapper, +.el-textarea__inner, +.el-select__wrapper { + border-radius: 8px; + box-shadow: 0 0 0 1px var(--border) inset; +} + +.el-input__wrapper.is-focus, +.el-select__wrapper.is-focused, +.el-textarea__inner:focus { + box-shadow: 0 0 0 1px var(--blue) inset; +} + +.el-button { + transition: background-color 0.2s ease, border-color 0.2s ease, transform 0.2s ease; +} + +.el-button:not(.is-disabled):active, +.button:active, +.text-button:active { + transform: translateY(1px); +} + +.el-button:focus-visible { + outline: 3px solid rgba(0, 64, 152, 0.22); + outline-offset: 2px; +} + +/* App shell */ +.portal-shell { + min-height: 100vh; + background: #f4f7fb; +} + +.portal-topbar { + position: sticky; + top: 0; + z-index: 100; + height: 52px; + background: #fff; + border-top: 3px solid var(--blue); + border-bottom: 1px solid var(--border); +} + +.portal-topbar__inner { + display: flex; + align-items: center; + justify-content: space-between; + gap: 28px; + height: 100%; + max-width: 1200px; + margin: 0 auto; + padding: 0 var(--spacing-xl); +} + +.portal-brand { + display: flex; + align-items: center; + gap: var(--spacing-sm); + border: 0; + background: transparent; + color: var(--text); + cursor: pointer; +} + +.portal-brand img { + width: 176px; + max-width: 36vw; + padding: 4px 8px; + border-radius: 6px; +} + +.portal-brand span { + font-size: 18px; + font-weight: 700; +} + +.portal-topbar__tools { + display: flex; + align-items: center; + gap: var(--spacing-xs); +} + +.portal-role-chip { + padding: 7px 12px; + border: 1px solid var(--blue); + border-radius: 999px; + color: var(--blue); + font-size: 13px; + background: var(--blue-light); +} + +.portal-user { + display: flex; + align-items: center; + gap: var(--spacing-xs); + border: 0; + background: transparent; + color: var(--text); + cursor: pointer; +} + +.portal-content { + max-width: 1200px; + margin: 0 auto; + padding: var(--spacing-xl) var(--spacing-xl) 48px; +} + +.page-title { + margin: 0 0 4px; + font-size: var(--fs-xl); + font-weight: 700; + color: var(--text); +} + +.page-subtitle { + margin: 0 0 16px; + font-size: var(--fs-sm); + color: var(--muted); +} + +.portal-footer { + margin-top: var(--spacing-2xl); + padding: 26px 0; + background: var(--blue-dark); + color: rgba(255, 255, 255, 0.86); +} + +.portal-footer__inner { + display: flex; + align-items: center; + gap: var(--spacing-xl); + max-width: 1200px; + margin: 0 auto; + padding: 0 var(--spacing-xl); + font-size: 13px; +} + +.portal-footer img { + width: 120px; + padding: 6px; + border-radius: 6px; + background: #fff; +} + +.portal-footer p { + margin: 3px 0; +} + +/* Student home */ +.portal-home { + position: relative; +} + +.portal-overview-strip { + display: grid; + grid-template-columns: 1fr; + gap: var(--spacing-md); + margin-bottom: 18px; +} + +.portal-profile-card__main { + display: flex; + align-items: flex-start; + gap: var(--spacing-md); +} + +.portal-profile-card__main .el-avatar { + flex-shrink: 0; +} + +.portal-profile-card { + border-left: 5px solid var(--blue); +} + +.portal-profile-card__text { + min-width: 0; +} + +.portal-profile-card h1 { + margin: 4px 0; + color: var(--text); + font-size: var(--fs-lg); +} + +.portal-profile-card span, +.portal-profile-card p { + margin: 0; + color: var(--muted); +} + +.portal-profile-card__actions { + display: flex; + flex-wrap: wrap; + gap: var(--spacing-xs); + margin-top: 16px; +} + +.portal-stat-card { + text-align: center; +} + +.portal-stat-card__icon { + display: grid; + width: 48px; + height: 48px; + place-items: center; + margin: 0 auto 10px; + border-radius: 50%; + background: var(--blue-light); + color: var(--blue); + font-size: 24px; +} + +.portal-stat-card span { + display: block; + color: var(--muted); +} + +.portal-stat-card strong { + display: block; + margin-top: 6px; + color: var(--text); + font-size: var(--fs-xl); +} + +.portal-card-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--spacing-md); + margin-bottom: 16px; +} + +.portal-card-header span { + color: var(--muted); + font-size: 13px; +} + +.portal-card-header h2 { + margin: 4px 0 0; + color: var(--text); + font-size: var(--fs-lg); +} + +.portal-stat-card, +.portal-orders-card, +.portal-recent-card, +.portal-profile-card { + background: #fff; +} + +.portal-stat-card .el-card__body { + padding: var(--spacing-md) 12px; +} + +.portal-stats-row { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: var(--spacing-xs); + margin-bottom: 18px; +} + +.portal-stats-row .el-card { + background: #fbfdff; +} + +.portal-workspace { + display: grid; + grid-template-columns: minmax(0, 2fr) minmax(280px, 0.85fr); + gap: var(--spacing-md); +} + +.portal-order-table { + cursor: pointer; +} + +.portal-order-main { + display: grid; + gap: 4px; +} + +.portal-order-main span, +.portal-muted { + color: var(--muted); + font-size: 13px; +} + +.portal-recent-list { + display: grid; + gap: var(--spacing-xs); +} + +.portal-recent-list button { + display: flex; + align-items: center; + justify-content: space-between; + min-height: 42px; + padding: 0 6px; + border: 0; + border-bottom: 1px solid #eef2f7; + background: #fff; + cursor: pointer; +} + +/* Common page blocks */ +.stack { + display: grid; + gap: var(--spacing-md); +} + +.card { + padding: var(--spacing-lg); + border: 1px solid var(--border); + border-radius: 12px; + background: #fff; +} + +.card--subtle { + background: #f8fbff; +} + +.card--list { + background: var(--bg); + box-shadow: none; +} + +.section-heading { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--spacing-md); + margin-bottom: var(--spacing-sm); +} + +.section-heading h2, +.card h2, +.card h3 { + margin: 0 0 var(--spacing-md); +} + +.service-summary { + gap: var(--spacing-md); +} + +.section-heading__meta, +.muted { + color: var(--muted); +} + +.responsibility-tag { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 3px 12px; + border-radius: 999px; + background: var(--blue-light); + color: var(--blue); + font-size: 13px; + font-weight: 600; +} + +.responsibility-tag[data-needs-action="true"] { + background: #fef3c7; + color: #92400e; + animation: pulse-dot 2s ease infinite; +} + +@keyframes pulse-dot { + 0%, 100% { box-shadow: 0 0 0 0 rgba(146, 64, 14, 0.2); } + 50% { box-shadow: 0 0 0 6px rgba(146, 64, 14, 0); } +} + +.button, +.text-button { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 44px; + padding: 0 14px; + border: 1px solid var(--blue); + border-radius: 8px; + background: var(--blue); + color: #fff; + font-weight: 600; + cursor: pointer; + transition: background-color 0.2s ease, border-color 0.2s ease, transform 0.2s ease; +} + +.text-button, +.button--ghost { + background: #fff; + color: var(--blue); +} + +.button:hover, +.text-button:hover, +.address-add-button:hover, +.address-select:hover, +.order-card:hover { + border-color: #9ebde5; + transform: translateY(-1px); +} + +.button:focus-visible, +.text-button:focus-visible, +.address-add-button:focus-visible, +.address-select:focus-visible, +.order-card:focus-visible, +.portal-recent-list button:focus-visible { + outline: 3px solid rgba(0, 64, 152, 0.22); + outline-offset: 2px; +} + +.button-row, +.toolbar__filters, +.local-actions { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--spacing-sm); +} + +.banner { + padding: 12px 14px; + border: 1px solid var(--border); + border-radius: 8px; + background: #f8fbff; +} + +.banner--error { + border-color: #f0b6bd; + background: #fff1f2; + color: var(--red); +} + +.banner--success { + border-color: #bfe8d9; + background: #ecfdf5; + color: #047857; +} + +.banner--warning { + border-color: #f3d49a; + background: #fffbeb; + color: #9a5b00; +} + +.empty-state, +.empty-illustration { + padding: var(--spacing-2xl) 16px; + text-align: center; + color: var(--muted); +} + +.empty-illustration img { + width: 150px; + max-width: 100%; +} + +.field { + display: grid; + gap: var(--spacing-xs); +} + +.field > span, +.tool-label { + color: var(--text); + font-weight: 600; +} + +.form-grid, +.detail-grid, +.repair-type-grid, +.repair-inline-tools, +.repair-derived, +.service-summary, +.admin-metrics { + display: grid; + gap: var(--spacing-sm); +} + +.form-grid, +.detail-grid, +.repair-type-grid, +.repair-inline-tools, +.repair-derived, +.service-summary, +.admin-metrics { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.service-summary, +.text-block, +.timeline, +.order-list { + margin-top: var(--spacing-md); +} + + + + +.text-block { + padding: var(--spacing-sm); + border-radius: 8px; + background: var(--blue-light); +} + +/* Lists and status */ +.status-badge { + display: inline-flex; + align-items: center; + gap: 5px; + min-height: 26px; + padding: 0 10px; + border-radius: 999px; + background: var(--blue-light); + color: var(--blue); + font-size: 13px; + font-weight: 600; +} + +.status-badge::before { + content: ""; + width: 7px; + height: 7px; + border-radius: 50%; + background: currentColor; + flex-shrink: 0; +} + +.status-badge[data-status="已完成"], +.status-badge[data-status="已确认"] { + background: #ecfdf5; + color: #047857; +} + +.status-badge[data-status="处理中"], +.status-badge[data-status="待上门"] { + background: #fef3c7; + color: #92400e; +} + +.status-badge[data-status="待处理"] { + background: #e0e7ff; + color: #3730a3; +} + +.status-badge[data-status="返工申请中"] { + background: #fce7f3; + color: #9d174d; +} + +.status-badge[data-status="已取消"] { + background: #f1f5f9; + color: #64748b; +} + +.order-list { + display: grid; + gap: var(--spacing-sm); +} + +.address-card-list { + display: grid; + gap: var(--spacing-xs); +} + +.order-card { + width: 100%; + min-height: 44px; + padding: 16px; + border: 1px solid var(--border); + border-radius: 10px; + background: #fff; + text-align: left; + cursor: pointer; +} + +.address-select { + display: flex; + align-items: center; + gap: var(--spacing-sm); + width: 100%; + min-height: 44px; + padding: var(--spacing-sm); + border: 1px solid var(--border); + border-radius: 10px; + background: #fff; + text-align: left; + cursor: pointer; + transition: border-color 0.2s, background 0.2s; +} + +.address-select:hover { + border-color: var(--blue); +} + +.address-select__dot { + flex: 0 0 auto; + width: 16px; + height: 16px; + border-radius: 50%; + border: 2px solid var(--border); + transition: border-color 0.2s, background 0.2s; +} + +.address-select[data-active="true"] { + border-color: var(--blue); + background: var(--blue-light); +} + +.address-select[data-active="true"] .address-select__dot { + border-color: var(--blue); + background: var(--blue); + box-shadow: inset 0 0 0 3px #fff; +} + +.address-select__body { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.address-select__body strong { + color: var(--text); + font-size: var(--fs-sm); +} + +.address-select__body span { + color: var(--muted); + font-size: var(--fs-xs); +} + +.address-select__delete { + flex: 0 0 auto; + display: grid; + place-items: center; + width: 24px; + height: 24px; + border: 0; + border-radius: 50%; + background: transparent; + color: var(--muted); + font-size: 16px; + cursor: pointer; + transition: background 0.15s, color 0.15s; +} + +.address-select__delete:hover { + background: #fee; + color: var(--red); +} + +.order-card__row, +.order-card__meta { + display: flex; + gap: var(--spacing-sm); +} + +.order-card__row { + align-items: flex-start; + justify-content: space-between; +} + +.order-card__meta, +.order-card__hint { + color: var(--muted); +} + +.order-card p { + margin: var(--spacing-xs) 0; + color: var(--text); +} + +.order-card__meta { + display: grid; + grid-template-columns: minmax(150px, 1.2fr) minmax(90px, 0.7fr) minmax(150px, 1fr); + padding-top: 10px; + border-top: 1px solid #eef2f7; +} + +.order-card__meta span { + display: flex; + gap: 6px; + align-items: baseline; + min-width: 0; + padding: 0; + background: transparent; + line-height: 1.35; +} + +.order-card__meta b { + color: var(--muted); + font-size: 12px; + font-weight: 600; +} + +.order-card__hint { + margin: var(--spacing-xs) 0 0; +} + +.timeline { + display: grid; + gap: var(--spacing-sm); + margin: 0; + padding: 0; + list-style: none; +} + +.timeline__item { + display: grid; + grid-template-columns: 14px minmax(0, 1fr); + gap: var(--spacing-sm); +} + +.timeline__dot { + width: 10px; + height: 10px; + margin-top: 7px; + border-radius: 50%; + background: var(--blue); +} + +.timeline__row { + display: flex; + justify-content: space-between; + gap: var(--spacing-sm); +} + +.timeline__meta, +.timeline__detail { + margin: 4px 0 0; + color: var(--muted); +} + +/* Repair page */ +.repair-page { + display: grid; + grid-template-columns: 280px minmax(0, 1fr); + gap: 20px; +} + +.repair-side-card { + position: sticky; + top: 68px; + align-self: start; + display: grid; + gap: var(--spacing-md); + padding: var(--spacing-lg); + border: 1px solid var(--border); + border-radius: 14px; + background: #fff; +} + +.repair-side-card span, +.repair-side-card small { + color: var(--muted); +} + +.repair-side-card h2 { + margin: 6px 0 8px; + color: var(--text); + font-size: var(--fs-lg); + line-height: 1.3; +} + +.repair-side-card p { + margin: 0; + color: var(--muted); + line-height: 1.7; +} + +.repair-side-card ol { + display: grid; + gap: var(--spacing-xs); + margin: 0; + padding-left: 18px; + color: var(--text); +} + +.repair-side-card li { + position: relative; + min-height: 44px; + padding-left: 20px; + line-height: 1.6; + cursor: pointer; + transition: color 0.2s; +} + +.repair-side-card li::before { + content: ''; + position: absolute; + left: 0; + top: 6px; + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--border); + transition: background 0.2s; +} + +.repair-side-card li:hover { + color: var(--blue); +} + +.repair-side-card li:hover::before { + background: var(--blue); +} + +.repair-side-card li.done { + color: var(--green); +} + +.repair-side-card li.done::before { + content: '✓'; + width: auto; + height: auto; + top: 0; + border-radius: 0; + background: transparent; + color: var(--green); + font-weight: 700; + font-size: 13px; +} + +.repair-side-card .ready-hint { + color: var(--green); + font-weight: 600; +} + +.repair-form-panel { + display: grid; + gap: var(--spacing-md); +} + +.repair-form-panel > .local-actions { + justify-content: flex-end; + margin-bottom: -4px; +} + +.repair-form-panel > .local-actions .text-button { + min-height: 30px; + padding: 0 6px; + border-color: transparent; + background: transparent; + color: var(--muted); + font-weight: 600; +} + +.repair-form-panel > .local-actions .text-button:hover { + border-color: transparent; + background: transparent; + color: var(--blue); +} + +.submit-strip { + position: sticky; + bottom: 0; + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--spacing-md); + margin-top: var(--spacing-xs); + padding: var(--spacing-md); + border: 1px solid var(--border); + border-radius: 12px; + background: #fff; + z-index: 10; +} + +.submit-strip--ready { + border-color: var(--green); + background: #f0faf5; +} + +.submit-strip__hint { + font-size: var(--fs-sm); + color: var(--muted); +} + +.submit-strip--ready .submit-strip__hint { + color: var(--green); + font-weight: 600; +} + +.flat-section, +.repair-summary-field { + padding: var(--spacing-md); + border: 1px solid var(--border); + border-radius: 12px; + background: #fff; +} + +.flat-section__title h2 { + margin: 0; +} + +.flat-section__title p { + margin: 6px 0 0; + color: var(--muted); +} + +.flat-section { + position: relative; + padding-left: 22px; + scroll-margin-top: 68px; +} + +.flat-section__title { + display: flex; + align-items: flex-start; + gap: var(--spacing-sm); + margin-bottom: 16px; +} + +.flat-section__title > span { + display: inline-grid; + width: 34px; + height: 28px; + place-items: center; + flex: 0 0 auto; + border-radius: 6px; + background: var(--blue); + color: #fff; + font-size: 13px; + font-weight: 700; + transition: background-color 0.25s ease; +} + +.flat-section[data-invalid] { + border-color: #f3bf4c; + background: #fffefa; +} + +.flat-section[data-invalid] .flat-section__title > span { + background: var(--orange); +} + +.flat-section[data-done] .flat-section__title > span { + background: var(--green); +} + +.assistant-action-bar { + display: flex; + align-items: center; + justify-content: flex-end; + gap: var(--spacing-sm); + margin-top: 14px; + padding-top: 14px; + border-top: 1px solid #eef2f7; +} + +.assistant-question-panel, +.inline-address-form { + display: grid; + gap: var(--spacing-sm); + margin-top: var(--spacing-sm); + padding: var(--spacing-md); + border: 1px solid var(--border); + border-radius: 12px; + background: #fff; +} + +.inline-address-form__row { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: var(--spacing-sm); +} + +.inline-address-form__actions { + display: flex; + justify-content: flex-end; + gap: var(--spacing-xs); +} + +.room-entry-choice { + display: grid; + gap: var(--spacing-sm); + margin-top: var(--spacing-md); +} + +.assistant-draft-panel, +.assistant-draft-panel div, +.repair-derived div, +.service-summary div { + display: flex; + flex-direction: column; + gap: 4px; + padding: 12px; + border-radius: 8px; + background: var(--blue-light); +} + + + + + + + + + +.welcome-card { + text-align: center; + padding: 48px 24px; +} + +.welcome-card__content h2 { + margin: 0; + font-size: var(--fs-lg); +} + +.welcome-card__content p { + margin: var(--spacing-xs) 0 0; + color: var(--muted); +} + +.welcome-card__actions { + margin-top: 20px; + display: flex; + justify-content: center; + gap: var(--spacing-sm); +} + +.address-add-button { + width: 100%; + min-height: 44px; + margin-top: var(--spacing-xs); + border: 1px dashed var(--blue); + border-radius: 10px; + background: #f8fbff; + color: var(--blue); + cursor: pointer; + transition: background 0.2s; +} + +.address-add-button:hover { + background: var(--blue-light); +} + +.quantity-control { + display: grid; + grid-template-columns: 44px 1fr 44px; + max-width: 220px; +} + +.quantity-control button { + border: 1px solid var(--border); + min-height: 44px; + background: #fff; + cursor: pointer; +} + +.quantity-control input { + text-align: center; +} + +.service-upload__plus { + font-size: 28px; +} + +.upload-count { + color: var(--muted); +} + +.radio-row { + display: flex; + gap: var(--spacing-xs); + align-items: center; +} + + + +/* Detail page */ +.detail-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: var(--spacing-md); +} + +.detail-header__right { + display: flex; + align-items: center; + gap: var(--spacing-xs); +} + +.progress-track { + display: grid; + grid-template-columns: repeat(6, minmax(0, 1fr)); + gap: var(--spacing-xs); + position: relative; +} + +.progress-track::before { + content: ""; + position: absolute; + top: 24px; + left: calc(8.33% + 12px); + right: calc(8.33% - 12px); + height: 2px; + background: #e2e8f0; + z-index: 0; +} + +.progress-track__step { + display: grid; + gap: 6px; + padding: 12px; + border-radius: 10px; + background: #f2f4f7; + text-align: center; + position: relative; + z-index: 1; +} + +.progress-track__step span { + display: inline-grid; + width: 24px; + height: 24px; + place-items: center; + justify-self: center; + border-radius: 50%; + background: #dde1e7; + color: var(--muted); + font-size: 12px; + font-weight: 700; +} + +.progress-track__step[data-active="true"] { + background: var(--blue-light); + color: var(--blue); + font-weight: 700; +} + +.progress-track__step[data-active="true"] span { + background: var(--blue); + color: #fff; +} + +.progress-track__step[data-current="true"] { + border: 2px solid var(--blue); + background: #fff; + box-shadow: 0 8px 20px rgba(0, 64, 152, 0.1); +} + +.progress-track[data-rework="true"]::after { + content: "↻ 返工"; + display: flex; + align-items: center; + justify-content: center; + padding: 8px; + margin-top: 6px; + border-radius: 8px; + background: #fff7ed; + color: var(--orange); + font-size: 13px; + font-weight: 700; + grid-column: 1 / -1; +} + +.sidebar-toggle { + display: none; + width: 100%; + min-height: 44px; + margin-bottom: 8px; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--blue-light); + color: var(--blue); + font-weight: 600; + cursor: pointer; + font-family: inherit; + font-size: 13px; +} + +.repair-side-card[data-collapsed="true"] .sidebar-content { + display: none; +} + + + +.status-guide { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: var(--spacing-sm); + margin: var(--spacing-sm) 0 var(--spacing-lg); +} + +.status-guide article { + padding: 12px; + border: 1px solid #e2e8f0; + border-radius: 8px; + background: var(--blue-light); +} + +.status-guide span { + color: var(--muted); + font-size: 12px; + font-weight: 700; +} + +.status-guide p { + margin: 6px 0 0; + color: var(--text); +} + +.attachments__grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); + gap: var(--spacing-xs); +} + +.attachments__grid img { + width: 100%; + border-radius: 8px; +} + +/* Admin */ +.admin-command { + display: flex; + justify-content: space-between; + gap: var(--spacing-md); + padding: var(--spacing-md); + border-radius: 12px; + background: #fff; +} + +.admin-metric { + padding: var(--spacing-md); + border: 1px solid var(--border); + border-radius: 12px; + background: #fff; +} + +.admin-metric strong { + display: block; + margin-top: 8px; + font-size: var(--fs-xl); +} + +.service-table.el-table { + border: 1px solid var(--border); + border-radius: 10px; +} + +.table-scroll { + overflow-x: auto; +} + +/* Mobile */ +@media (max-width: 980px) { + .sidebar-toggle { + display: block; + } + + .portal-overview-strip, + .portal-workspace, + .repair-page { + grid-template-columns: 1fr; + } + + .portal-stats-row { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .progress-track { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .repair-side-card { + position: relative; + top: auto; + } +} + +@media (max-width: 640px) { + .portal-topbar__inner { + padding: 0 var(--spacing-sm); + } + + .portal-brand img { + width: 132px; + } + + .portal-brand span, + .portal-role-chip { + display: none; + } + + .repair-side-card { + gap: var(--spacing-xs); + padding: var(--spacing-sm); + } + + .repair-side-card h2 { + margin: 2px 0 4px; + font-size: 20px; + } + + .repair-side-card p, + .repair-side-card ol { + display: none; + } + + .portal-overview-strip, + .form-grid, + .detail-grid, + .repair-type-grid, + .repair-inline-tools, + .repair-derived, + .service-summary, + .admin-metrics, + .assistant-draft-panel, + .status-guide { + grid-template-columns: 1fr; + } + + .progress-track { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .order-card { + padding: var(--spacing-sm); + } + + .order-card__row { + gap: var(--spacing-xs); + } + + .order-card__meta { + grid-template-columns: 1fr; + gap: 6px; + } + + .order-card__meta span { + padding: 0; + } + + .portal-stats-row { + gap: var(--spacing-xs); + } + + .portal-stat-card .el-card__body { + padding: 12px 6px; + } + + .portal-stat-card__icon { + width: 36px; + height: 36px; + margin-bottom: 6px; + font-size: 18px; + } + + .portal-stat-card strong { + font-size: var(--fs-lg); + } + + .portal-card-header, + .section-heading, + .assistant-action-bar, + .admin-command { + align-items: stretch; + flex-direction: column; + } + + .detail-header { + align-items: flex-start; + } + + .portal-footer__inner { + flex-direction: column; + align-items: flex-start; + } + + .inline-address-form__row { + grid-template-columns: 1fr; + } + + .stat-section { + grid-template-columns: 1fr; + } + + .stat-bar-row { + grid-template-columns: 72px 1fr 36px; + gap: var(--spacing-xs); + } +} + +/* Stats bar chart */ +.stat-section { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: var(--spacing-md); +} + +.stat-card { + padding: var(--spacing-md); + border: 1px solid var(--border); + border-radius: 12px; + background: #fff; +} + +.stat-card h3 { + margin: 0 0 14px; + font-size: 15px; + color: var(--muted); +} + +.stat-summary { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: var(--spacing-sm); + margin-bottom: 18px; +} + +.stat-summary__item { + padding: var(--spacing-sm); + border: 1px solid var(--border); + border-radius: 10px; + background: var(--blue-light); + text-align: center; +} + +.stat-summary__item span { + display: block; + color: var(--muted); + font-size: 13px; +} + +.stat-summary__item strong { + display: block; + margin-top: 4px; + font-size: 24px; + color: var(--blue); +} + +.stat-bar-row { + display: grid; + grid-template-columns: 90px 1fr 42px; + align-items: center; + gap: var(--spacing-xs); + margin-bottom: 10px; +} + +.stat-bar-row__label { + font-size: 13px; + font-weight: 600; + color: var(--text); + text-align: right; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.stat-bar-row__track { + height: 18px; + border-radius: 9px; + background: #f1f5f9; + overflow: hidden; +} + +.stat-bar-row__fill { + height: 100%; + border-radius: 9px; + background: var(--blue); + min-width: 4px; + transition: width 0.5s ease; +} + +.stat-bar-row__count { + font-size: 13px; + font-weight: 700; + color: var(--muted); +} + +.stat-empty { + padding: 24px; + text-align: center; + color: var(--muted); + font-size: var(--fs-sm); +} + +/* Print */ +@media print { + .portal-topbar, + .portal-footer, + .button, + .el-button, + .text-button, + .repair-side-card, + .assistant-action-bar, + .admin-command, + .section-heading button, + .banner, + .portal-stats-row, + .card--subtle, + .toolbar__filters, + .admin-metrics, .portal-profile-card__actions, + .welcome-card__actions, + .progress-track[data-rework]::after, + #app > header, + nav { + display: none !important; + } + + body { + background: #fff; + color: #000; + font-size: 12pt; + } + + .portal-content { + width: 100%; + margin: 0; + padding: 0 12mm; + } + + .card, + .flat-section { + box-shadow: none; + border: 1px solid #ccc; + page-break-inside: avoid; + } + + .detail-header, + .detail-grid, + .form-grid, + .service-summary, + .data-list, + .text-block, + .status-guide, + .attachments__grid { + page-break-inside: avoid; + } + + .progress-track__step { + border: 1px solid #ccc; + } + + .progress-track__step[data-current="true"] { + border: 2px solid #000; + } + + .el-table { + font-size: 10pt; + } + + img { + max-width: 100% !important; + } +} diff --git a/frontend/src/types.ts b/frontend/src/types.ts new file mode 100644 index 0000000..e1ce9eb --- /dev/null +++ b/frontend/src/types.ts @@ -0,0 +1,122 @@ +export type Role = 'student' | 'admin' + +export interface UserProfile { + id: number + username: string + display_name: string + role: Role +} + +export interface LoginResponse { + token: string + user: UserProfile +} + +export interface DiagnosisQuestion { + id: string + prompt: string +} + +export interface DiagnosisDraft { + category: string + urgency: '低' | '中' | '高' | '紧急' + summary: string + safety_risk: boolean + suggested_worker: string + notes: string[] +} + +export interface DiagnosisResponse { + session_id: string + stage: 'questions' | 'draft' + initial_message: string + suggested_categories: string[] + questions: DiagnosisQuestion[] + draft: DiagnosisDraft | null +} + +export interface SavedAddress { + id: number + campus: string + building: string + room: string + last_used_at: string +} + +export interface OrderSummary { + id: number + order_no: string + campus: string + building: string + room: string + category: string + status: string + urgency: string + submission_time: string + expected_repair_time: string | null + assignee_name: string | null +} + +export interface Attachment { + id: number + file_name: string + file_path: string + mime_type: string + created_at: string +} + +export interface OrderEvent { + id: number + actor_role: string + actor_name: string + event_type: string + title: string + detail: string | null + from_status: string | null + to_status: string | null + created_at: string +} + +export interface FeedbackData { + rating: number + comment: string + created_at: string +} + +export interface OrderDetail { + id: number + order_no: string + campus: string + building: string + room: string + category: string + status: string + urgency: string + raw_description: string + structured_summary: string + allow_room_entry: boolean + expected_date: string | null + expected_time_segment: string | null + assignee_name: string | null + expected_arrival_at: string | null + admin_note: string | null + rework_reason: string | null + created_at: string + updated_at: string + attachments: Attachment[] + events: OrderEvent[] + feedback: FeedbackData | null +} + +export interface StatItem { + category?: string + building?: string + count: number +} + +export interface AdminStats { + category_distribution: StatItem[] + building_distribution: StatItem[] + avg_processing_hours: number + avg_rating: number +} diff --git a/frontend/src/views/AdminOrdersView.vue b/frontend/src/views/AdminOrdersView.vue new file mode 100644 index 0000000..a5a30b7 --- /dev/null +++ b/frontend/src/views/AdminOrdersView.vue @@ -0,0 +1,396 @@ + + + diff --git a/frontend/src/views/LoginView.vue b/frontend/src/views/LoginView.vue new file mode 100644 index 0000000..0cd618a --- /dev/null +++ b/frontend/src/views/LoginView.vue @@ -0,0 +1,312 @@ + + + + + diff --git a/frontend/src/views/StudentHomeView.vue b/frontend/src/views/StudentHomeView.vue new file mode 100644 index 0000000..286192a --- /dev/null +++ b/frontend/src/views/StudentHomeView.vue @@ -0,0 +1,170 @@ + + + diff --git a/frontend/src/views/StudentOrderDetailView.vue b/frontend/src/views/StudentOrderDetailView.vue new file mode 100644 index 0000000..30c42c3 --- /dev/null +++ b/frontend/src/views/StudentOrderDetailView.vue @@ -0,0 +1,368 @@ + + + diff --git a/frontend/src/views/StudentOrdersView.vue b/frontend/src/views/StudentOrdersView.vue new file mode 100644 index 0000000..f11f00a --- /dev/null +++ b/frontend/src/views/StudentOrdersView.vue @@ -0,0 +1,126 @@ + + + diff --git a/frontend/src/views/StudentReportView.vue b/frontend/src/views/StudentReportView.vue new file mode 100644 index 0000000..255a237 --- /dev/null +++ b/frontend/src/views/StudentReportView.vue @@ -0,0 +1,705 @@ + + + diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json new file mode 100644 index 0000000..6c627f6 --- /dev/null +++ b/frontend/tsconfig.app.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "paths": { + "@/*": ["./src/*"] + }, + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "tsBuildInfoFile": "./.tsbuildinfo/app.tsbuildinfo", + "types": ["vite/client"] + }, + "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"] +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..e891e30 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,8 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} + diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..2e00644 --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "composite": true, + "noEmit": true, + "tsBuildInfoFile": "./.tsbuildinfo/node.tsbuildinfo", + "skipLibCheck": true, + "types": ["node"] + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..503a4f5 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,32 @@ +import { fileURLToPath, URL } from 'node:url' + +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + plugins: [vue()], + build: { + rollupOptions: { + output: { + manualChunks(id) { + if (id.includes('node_modules/element-plus') || id.includes('node_modules/@element-plus')) { + return 'element-plus' + } + if ( + id.includes('node_modules/vue') || + id.includes('node_modules/vue-router') || + id.includes('node_modules/axios') + ) { + return 'vendor' + } + return undefined + }, + }, + }, + }, + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + }, + }, +}) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..61ccb1d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,30 @@ +[project] +name = "dorm-repair-backend" +version = "0.1.0" +description = "交互式透明化宿舍报修系统 —— FastAPI 后端" +requires-python = ">=3.12" +dependencies = [ + "bcrypt>=4,<5", + "fastapi>=0.115,<1", + "uvicorn[standard]>=0.30,<1", + "python-multipart>=0.0.9,<1", + "pydantic>=2.11,<3", + "httpx>=0.28,<1", + "python-dotenv>=1.0,<2", +] + +[dependency-groups] +dev = [ + "ruff>=0.15,<1", +] + +[tool.uv] +package = false + +[tool.ruff] +target-version = "py312" +line-length = 120 +src = ["backend", "cli.py"] + +[tool.ruff.lint] +select = ["E", "F", "I", "UP", "B"] diff --git a/scripts/_seed.py b/scripts/_seed.py new file mode 100644 index 0000000..94352b6 --- /dev/null +++ b/scripts/_seed.py @@ -0,0 +1,321 @@ +#!/usr/bin/env python3 +"""Demo data seed script — called by cli.py, not part of application code.""" + +from __future__ import annotations + +import sqlite3 +import sys +from datetime import datetime +from pathlib import Path + +BACKEND_DIR = Path(__file__).resolve().parent.parent / "backend" +sys.path.insert(0, str(BACKEND_DIR)) + +from app.core.security import hash_password # noqa: E402 +from app.services.repository import create_event, init_db # noqa: E402 + +_now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + +def run(db_path: str, force: bool = False) -> None: + conn = sqlite3.connect(db_path) + conn.row_factory = sqlite3.Row + try: + init_db(conn) + + if force: + conn.executescript(""" + DELETE FROM feedback; + DELETE FROM order_events; + DELETE FROM attachments; + DELETE FROM saved_addresses; + DELETE FROM repair_orders; + DELETE FROM sessions; + DELETE FROM users; + """) + conn.commit() + + existing = conn.execute("SELECT COUNT(*) AS count FROM repair_orders").fetchone()["count"] + if existing > 0: + print("Orders already exist, skipping. Use --force to overwrite.") + return + + conn.executemany( + "INSERT OR IGNORE INTO users " + "(username, password_hash, role, display_name, created_at) " + "VALUES (?, ?, ?, ?, ?)", + [ + ("student01", hash_password("Student123"), "student", "张同学", _now), + ("admin01", hash_password("Admin123"), "admin", "宿管老师", _now), + ], + ) + student = conn.execute("SELECT id FROM users WHERE username = 'student01'").fetchone() + if student is None: + conn.commit() + return + + conn.execute( + "INSERT OR IGNORE INTO saved_addresses " + "(student_id, campus, building, room, last_used_at) " + "VALUES (?, ?, ?, ?, ?)", + (student["id"], "西海岸校区", "听海苑999号楼", "404", _now), + ) + + orders = [ + { + "order_no": "DR20260601001", + "category": "给排水 / 水龙头类", + "raw_description": "卫生间水龙头关不严,一直滴水。", + "structured_summary": "给排水,建议管道处理。卫生间水龙头关不严持续滴水;洗手池位置;影响日常用水。", + "urgency": "中", + "status": "已提交", + "allow_room_entry": 0, + "assignee_name": None, + "expected_date": None, + "expected_time_segment": None, + "expected_arrival_at": None, + "admin_note": None, + "rework_reason": None, + }, + { + "order_no": "DR20260601002", + "category": "电路照明 / 照明故障", + "raw_description": "宿舍顶灯一直闪烁,还有轻微焦味。", + "structured_summary": "电路照明,建议电工处理。顶灯闪烁有焦味;门口位置;影响整间照明;有轻微焦糊味。", + "urgency": "紧急", + "status": "待处理", + "allow_room_entry": 0, + "assignee_name": "王师傅", + "expected_date": "2026-06-02", + "expected_time_segment": "09:00-11:00", + "expected_arrival_at": "明天上午", + "admin_note": "已派电工王师傅,请提前清理顶灯下方区域。", + "rework_reason": None, + }, + { + "order_no": "DR20260601003", + "category": "空调设备 / 制冷制热异常", + "raw_description": "空调开到16度还是不制冷,出风口感觉是自然风。", + "structured_summary": ( + "空调设备,建议空调维修处理。空调16度不制冷;出风口常温风;已持续两天影响休息。" + ), + "urgency": "高", + "status": "处理中", + "allow_room_entry": 1, + "assignee_name": "赵师傅", + "expected_date": "2026-06-02", + "expected_time_segment": "14:00-16:00", + "expected_arrival_at": "今天下午", + "admin_note": "已安排空调维修赵师傅上门检测,可能与制冷剂泄漏有关。", + "rework_reason": None, + }, + { + "order_no": "DR20260601004", + "category": "门窗锁具 / 门锁钥匙", + "raw_description": "宿舍门锁很难拧开,偶尔会卡住。", + "structured_summary": ( + "门窗锁具,建议门窗处理。门锁难拧开偶尔卡住;门口锁芯位置;影响进出;锁芯磨损严重。" + ), + "urgency": "中", + "status": "待上门", + "allow_room_entry": 0, + "assignee_name": "李师傅", + "expected_date": "2026-06-03", + "expected_time_segment": "14:00-16:00", + "expected_arrival_at": "6月3日下午", + "admin_note": "已安排门窗维修李师傅上门更换锁芯,请当天留人在宿舍。", + "rework_reason": None, + }, + { + "order_no": "DR20260528001", + "category": "网络弱电 / 网口异常", + "raw_description": "宿舍网口插上网线没反应,灯不亮。", + "structured_summary": "网络弱电,建议弱电维修处理。网口插网线无反应灯不亮;书桌下方;影响上网学习。", + "urgency": "中", + "status": "返工申请中", + "allow_room_entry": 1, + "assignee_name": "孙师傅", + "expected_date": None, + "expected_time_segment": None, + "expected_arrival_at": None, + "admin_note": "孙师傅已上门维修并更换网口面板,但学生反映问题仍存在,已进入返工流程。", + "rework_reason": "维修后网口仍无法使用,指示灯依然不亮,请重新排查线路问题。", + }, + { + "order_no": "DR20260525003", + "category": "门窗锁具 / 窗户五金", + "raw_description": "宿舍窗户关不严,冬天漏风严重。", + "structured_summary": "门窗锁具,建议门窗处理。窗户关不严有缝隙;推拉窗轨道变形;冬天漏风影响室温。", + "urgency": "中", + "status": "已完成", + "allow_room_entry": 1, + "assignee_name": "李师傅", + "expected_date": "2026-05-24", + "expected_time_segment": "09:00-11:00", + "expected_arrival_at": "5月24日上午", + "admin_note": "已更换窗户密封条并调整轨道,请学生确认。", + "rework_reason": None, + }, + { + "order_no": "DR20260520001", + "category": "家具设施 / 桌椅维修", + "raw_description": "书桌抽屉轨道坏了,拉出来会掉。", + "structured_summary": "家具设施,建议木工维修处理。抽屉轨道损坏拉出脱落;右侧抽屉;影响日常存放。", + "urgency": "低", + "status": "已确认", + "allow_room_entry": 0, + "assignee_name": "刘师傅", + "expected_date": "2026-05-20", + "expected_time_segment": "14:00-16:00", + "expected_arrival_at": "5月20日下午", + "admin_note": "已更换抽屉轨道,学生已确认维修结果。", + "rework_reason": None, + }, + { + "order_no": "DR20260515001", + "category": "给排水 / 下水疏通类", + "raw_description": "洗手池下水很慢,水会积在池子里。", + "structured_summary": "给排水,建议管道疏通处理。洗手池下水缓慢积水;卫生间洗手池;影响日常洗漱。", + "urgency": "中", + "status": "已取消", + "allow_room_entry": 0, + "assignee_name": None, + "expected_date": None, + "expected_time_segment": None, + "expected_arrival_at": None, + "admin_note": None, + "rework_reason": None, + }, + ] + + for o in orders: + cur = conn.execute( + "INSERT INTO repair_orders (" + "order_no, student_id, campus, building, room, category, " + "raw_description, structured_summary, urgency, status, " + "allow_room_entry, assignee_name, expected_date, " + "expected_time_segment, expected_arrival_at, admin_note, " + "rework_reason, diagnosis_session_id, created_at, updated_at" + ") VALUES (" + "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, " + "?, ?, ?, ?, ?, ?, ?, ?, ?, ?" + ")", + ( + o["order_no"], + student["id"], + "西海岸校区", + "听海苑999号楼", + "404", + o["category"], + o["raw_description"], + o["structured_summary"], + o["urgency"], + o["status"], + o["allow_room_entry"], + o["assignee_name"], + o["expected_date"], + o["expected_time_segment"], + o["expected_arrival_at"], + o["admin_note"], + o["rework_reason"], + None, + _now, + _now, + ), + ) + oid = cur.lastrowid + + create_event( + conn, + oid, + "student", + "张同学", + "created", + "学生提交报修", + o["raw_description"], + None, + "已提交", + ) + + if o["status"] != "已提交": + create_event( + conn, + oid, + "admin", + "宿管老师", + "status_updated", + "管理员更新工单状态", + o["admin_note"], + "已提交", + o["status"], + ) + if o["status"] == "待上门": + expected_info = f"预计 {o['expected_date'] or ''} {o['expected_time_segment'] or ''}" + create_event( + conn, + oid, + "admin", + "宿管老师", + "scheduled", + "管理员安排上门时间", + expected_info, + o["status"], + o["status"], + ) + if o["status"] == "返工申请中": + create_event( + conn, + oid, + "student", + "张同学", + "rework_requested", + "学生申请返工", + o["rework_reason"], + "已完成", + o["status"], + ) + if o["status"] == "已确认": + create_event( + conn, + oid, + "student", + "张同学", + "confirmed", + "学生确认维修完成", + "维修结果满意,确认工单完成。", + "已完成", + o["status"], + ) + if o["status"] == "已取消": + create_event( + conn, + oid, + "student", + "张同学", + "cancelled", + "学生取消工单", + "问题已自行解决,取消报修。", + o["status"], + o["status"], + ) + if o["status"] in ("已完成", "已确认"): + conn.execute( + "INSERT INTO feedback (order_id, rating, comment, created_at) VALUES (?, ?, ?, ?)", + (oid, 5, "维修很快,问题已经解决。", _now), + ) + + conn.commit() + print("Seed complete.") + + finally: + conn.close() + + +if __name__ == "__main__": + import argparse + + p = argparse.ArgumentParser() + p.add_argument("--db", type=str, default=str(BACKEND_DIR / "data" / "dorm_repair.sqlite3")) + p.add_argument("--force", action="store_true") + args = p.parse_args() + run(args.db, args.force) diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..935f511 --- /dev/null +++ b/uv.lock @@ -0,0 +1,638 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + +[[package]] +name = "bcrypt" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/5d/6d7433e0f3cd46ce0b43cd65e1db465ea024dbb8216fb2404e919c2ad77b/bcrypt-4.3.0.tar.gz", hash = "sha256:3a3fd2204178b6d2adcf09cb4f6426ffef54762577a7c9b54c159008cb288c18", size = 25697, upload-time = "2025-02-28T01:24:09.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/2c/3d44e853d1fe969d229bd58d39ae6902b3d924af0e2b5a60d17d4b809ded/bcrypt-4.3.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f01e060f14b6b57bbb72fc5b4a83ac21c443c9a2ee708e04a10e9192f90a6281", size = 483719, upload-time = "2025-02-28T01:22:34.539Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e2/58ff6e2a22eca2e2cff5370ae56dba29d70b1ea6fc08ee9115c3ae367795/bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5eeac541cefd0bb887a371ef73c62c3cd78535e4887b310626036a7c0a817bb", size = 272001, upload-time = "2025-02-28T01:22:38.078Z" }, + { url = "https://files.pythonhosted.org/packages/37/1f/c55ed8dbe994b1d088309e366749633c9eb90d139af3c0a50c102ba68a1a/bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59e1aa0e2cd871b08ca146ed08445038f42ff75968c7ae50d2fdd7860ade2180", size = 277451, upload-time = "2025-02-28T01:22:40.787Z" }, + { url = "https://files.pythonhosted.org/packages/d7/1c/794feb2ecf22fe73dcfb697ea7057f632061faceb7dcf0f155f3443b4d79/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:0042b2e342e9ae3d2ed22727c1262f76cc4f345683b5c1715f0250cf4277294f", size = 272792, upload-time = "2025-02-28T01:22:43.144Z" }, + { url = "https://files.pythonhosted.org/packages/13/b7/0b289506a3f3598c2ae2bdfa0ea66969812ed200264e3f61df77753eee6d/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74a8d21a09f5e025a9a23e7c0fd2c7fe8e7503e4d356c0a2c1486ba010619f09", size = 289752, upload-time = "2025-02-28T01:22:45.56Z" }, + { url = "https://files.pythonhosted.org/packages/dc/24/d0fb023788afe9e83cc118895a9f6c57e1044e7e1672f045e46733421fe6/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:0142b2cb84a009f8452c8c5a33ace5e3dfec4159e7735f5afe9a4d50a8ea722d", size = 277762, upload-time = "2025-02-28T01:22:47.023Z" }, + { url = "https://files.pythonhosted.org/packages/e4/38/cde58089492e55ac4ef6c49fea7027600c84fd23f7520c62118c03b4625e/bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:12fa6ce40cde3f0b899729dbd7d5e8811cb892d31b6f7d0334a1f37748b789fd", size = 272384, upload-time = "2025-02-28T01:22:49.221Z" }, + { url = "https://files.pythonhosted.org/packages/de/6a/d5026520843490cfc8135d03012a413e4532a400e471e6188b01b2de853f/bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:5bd3cca1f2aa5dbcf39e2aa13dd094ea181f48959e1071265de49cc2b82525af", size = 277329, upload-time = "2025-02-28T01:22:51.603Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a3/4fc5255e60486466c389e28c12579d2829b28a527360e9430b4041df4cf9/bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:335a420cfd63fc5bc27308e929bee231c15c85cc4c496610ffb17923abf7f231", size = 305241, upload-time = "2025-02-28T01:22:53.283Z" }, + { url = "https://files.pythonhosted.org/packages/c7/15/2b37bc07d6ce27cc94e5b10fd5058900eb8fb11642300e932c8c82e25c4a/bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:0e30e5e67aed0187a1764911af023043b4542e70a7461ad20e837e94d23e1d6c", size = 309617, upload-time = "2025-02-28T01:22:55.461Z" }, + { url = "https://files.pythonhosted.org/packages/5f/1f/99f65edb09e6c935232ba0430c8c13bb98cb3194b6d636e61d93fe60ac59/bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b8d62290ebefd49ee0b3ce7500f5dbdcf13b81402c05f6dafab9a1e1b27212f", size = 335751, upload-time = "2025-02-28T01:22:57.81Z" }, + { url = "https://files.pythonhosted.org/packages/00/1b/b324030c706711c99769988fcb694b3cb23f247ad39a7823a78e361bdbb8/bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef6630e0ec01376f59a006dc72918b1bf436c3b571b80fa1968d775fa02fe7d", size = 355965, upload-time = "2025-02-28T01:22:59.181Z" }, + { url = "https://files.pythonhosted.org/packages/aa/dd/20372a0579dd915dfc3b1cd4943b3bca431866fcb1dfdfd7518c3caddea6/bcrypt-4.3.0-cp313-cp313t-win32.whl", hash = "sha256:7a4be4cbf241afee43f1c3969b9103a41b40bcb3a3f467ab19f891d9bc4642e4", size = 155316, upload-time = "2025-02-28T01:23:00.763Z" }, + { url = "https://files.pythonhosted.org/packages/6d/52/45d969fcff6b5577c2bf17098dc36269b4c02197d551371c023130c0f890/bcrypt-4.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c1949bf259a388863ced887c7861da1df681cb2388645766c89fdfd9004c669", size = 147752, upload-time = "2025-02-28T01:23:02.908Z" }, + { url = "https://files.pythonhosted.org/packages/11/22/5ada0b9af72b60cbc4c9a399fdde4af0feaa609d27eb0adc61607997a3fa/bcrypt-4.3.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:f81b0ed2639568bf14749112298f9e4e2b28853dab50a8b357e31798686a036d", size = 498019, upload-time = "2025-02-28T01:23:05.838Z" }, + { url = "https://files.pythonhosted.org/packages/b8/8c/252a1edc598dc1ce57905be173328eda073083826955ee3c97c7ff5ba584/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:864f8f19adbe13b7de11ba15d85d4a428c7e2f344bac110f667676a0ff84924b", size = 279174, upload-time = "2025-02-28T01:23:07.274Z" }, + { url = "https://files.pythonhosted.org/packages/29/5b/4547d5c49b85f0337c13929f2ccbe08b7283069eea3550a457914fc078aa/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e36506d001e93bffe59754397572f21bb5dc7c83f54454c990c74a468cd589e", size = 283870, upload-time = "2025-02-28T01:23:09.151Z" }, + { url = "https://files.pythonhosted.org/packages/be/21/7dbaf3fa1745cb63f776bb046e481fbababd7d344c5324eab47f5ca92dd2/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:842d08d75d9fe9fb94b18b071090220697f9f184d4547179b60734846461ed59", size = 279601, upload-time = "2025-02-28T01:23:11.461Z" }, + { url = "https://files.pythonhosted.org/packages/6d/64/e042fc8262e971347d9230d9abbe70d68b0a549acd8611c83cebd3eaec67/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7c03296b85cb87db865d91da79bf63d5609284fc0cab9472fdd8367bbd830753", size = 297660, upload-time = "2025-02-28T01:23:12.989Z" }, + { url = "https://files.pythonhosted.org/packages/50/b8/6294eb84a3fef3b67c69b4470fcdd5326676806bf2519cda79331ab3c3a9/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:62f26585e8b219cdc909b6a0069efc5e4267e25d4a3770a364ac58024f62a761", size = 284083, upload-time = "2025-02-28T01:23:14.5Z" }, + { url = "https://files.pythonhosted.org/packages/62/e6/baff635a4f2c42e8788fe1b1633911c38551ecca9a749d1052d296329da6/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:beeefe437218a65322fbd0069eb437e7c98137e08f22c4660ac2dc795c31f8bb", size = 279237, upload-time = "2025-02-28T01:23:16.686Z" }, + { url = "https://files.pythonhosted.org/packages/39/48/46f623f1b0c7dc2e5de0b8af5e6f5ac4cc26408ac33f3d424e5ad8da4a90/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:97eea7408db3a5bcce4a55d13245ab3fa566e23b4c67cd227062bb49e26c585d", size = 283737, upload-time = "2025-02-28T01:23:18.897Z" }, + { url = "https://files.pythonhosted.org/packages/49/8b/70671c3ce9c0fca4a6cc3cc6ccbaa7e948875a2e62cbd146e04a4011899c/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:191354ebfe305e84f344c5964c7cd5f924a3bfc5d405c75ad07f232b6dffb49f", size = 312741, upload-time = "2025-02-28T01:23:21.041Z" }, + { url = "https://files.pythonhosted.org/packages/27/fb/910d3a1caa2d249b6040a5caf9f9866c52114d51523ac2fb47578a27faee/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:41261d64150858eeb5ff43c753c4b216991e0ae16614a308a15d909503617732", size = 316472, upload-time = "2025-02-28T01:23:23.183Z" }, + { url = "https://files.pythonhosted.org/packages/dc/cf/7cf3a05b66ce466cfb575dbbda39718d45a609daa78500f57fa9f36fa3c0/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:33752b1ba962ee793fa2b6321404bf20011fe45b9afd2a842139de3011898fef", size = 343606, upload-time = "2025-02-28T01:23:25.361Z" }, + { url = "https://files.pythonhosted.org/packages/e3/b8/e970ecc6d7e355c0d892b7f733480f4aa8509f99b33e71550242cf0b7e63/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:50e6e80a4bfd23a25f5c05b90167c19030cf9f87930f7cb2eacb99f45d1c3304", size = 362867, upload-time = "2025-02-28T01:23:26.875Z" }, + { url = "https://files.pythonhosted.org/packages/a9/97/8d3118efd8354c555a3422d544163f40d9f236be5b96c714086463f11699/bcrypt-4.3.0-cp38-abi3-win32.whl", hash = "sha256:67a561c4d9fb9465ec866177e7aebcad08fe23aaf6fbd692a6fab69088abfc51", size = 160589, upload-time = "2025-02-28T01:23:28.381Z" }, + { url = "https://files.pythonhosted.org/packages/29/07/416f0b99f7f3997c69815365babbc2e8754181a4b1899d921b3c7d5b6f12/bcrypt-4.3.0-cp38-abi3-win_amd64.whl", hash = "sha256:584027857bc2843772114717a7490a37f68da563b3620f78a849bcb54dc11e62", size = 152794, upload-time = "2025-02-28T01:23:30.187Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c1/3fa0e9e4e0bfd3fd77eb8b52ec198fd6e1fd7e9402052e43f23483f956dd/bcrypt-4.3.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d3efb1157edebfd9128e4e46e2ac1a64e0c1fe46fb023158a407c7892b0f8c3", size = 498969, upload-time = "2025-02-28T01:23:31.945Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d4/755ce19b6743394787fbd7dff6bf271b27ee9b5912a97242e3caf125885b/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08bacc884fd302b611226c01014eca277d48f0a05187666bca23aac0dad6fe24", size = 279158, upload-time = "2025-02-28T01:23:34.161Z" }, + { url = "https://files.pythonhosted.org/packages/9b/5d/805ef1a749c965c46b28285dfb5cd272a7ed9fa971f970435a5133250182/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6746e6fec103fcd509b96bacdfdaa2fbde9a553245dbada284435173a6f1aef", size = 284285, upload-time = "2025-02-28T01:23:35.765Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/698580547a4a4988e415721b71eb45e80c879f0fb04a62da131f45987b96/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:afe327968aaf13fc143a56a3360cb27d4ad0345e34da12c7290f1b00b8fe9a8b", size = 279583, upload-time = "2025-02-28T01:23:38.021Z" }, + { url = "https://files.pythonhosted.org/packages/f2/87/62e1e426418204db520f955ffd06f1efd389feca893dad7095bf35612eec/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d9af79d322e735b1fc33404b5765108ae0ff232d4b54666d46730f8ac1a43676", size = 297896, upload-time = "2025-02-28T01:23:39.575Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c6/8fedca4c2ada1b6e889c52d2943b2f968d3427e5d65f595620ec4c06fa2f/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f1e3ffa1365e8702dc48c8b360fef8d7afeca482809c5e45e653af82ccd088c1", size = 284492, upload-time = "2025-02-28T01:23:40.901Z" }, + { url = "https://files.pythonhosted.org/packages/4d/4d/c43332dcaaddb7710a8ff5269fcccba97ed3c85987ddaa808db084267b9a/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3004df1b323d10021fda07a813fd33e0fd57bef0e9a480bb143877f6cba996fe", size = 279213, upload-time = "2025-02-28T01:23:42.653Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/1e36379e169a7df3a14a1c160a49b7b918600a6008de43ff20d479e6f4b5/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:531457e5c839d8caea9b589a1bcfe3756b0547d7814e9ce3d437f17da75c32b0", size = 284162, upload-time = "2025-02-28T01:23:43.964Z" }, + { url = "https://files.pythonhosted.org/packages/1c/0a/644b2731194b0d7646f3210dc4d80c7fee3ecb3a1f791a6e0ae6bb8684e3/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:17a854d9a7a476a89dcef6c8bd119ad23e0f82557afbd2c442777a16408e614f", size = 312856, upload-time = "2025-02-28T01:23:46.011Z" }, + { url = "https://files.pythonhosted.org/packages/dc/62/2a871837c0bb6ab0c9a88bf54de0fc021a6a08832d4ea313ed92a669d437/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6fb1fd3ab08c0cbc6826a2e0447610c6f09e983a281b919ed721ad32236b8b23", size = 316726, upload-time = "2025-02-28T01:23:47.575Z" }, + { url = "https://files.pythonhosted.org/packages/0c/a1/9898ea3faac0b156d457fd73a3cb9c2855c6fd063e44b8522925cdd8ce46/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e965a9c1e9a393b8005031ff52583cedc15b7884fce7deb8b0346388837d6cfe", size = 343664, upload-time = "2025-02-28T01:23:49.059Z" }, + { url = "https://files.pythonhosted.org/packages/40/f2/71b4ed65ce38982ecdda0ff20c3ad1b15e71949c78b2c053df53629ce940/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:79e70b8342a33b52b55d93b3a59223a844962bef479f6a0ea318ebbcadf71505", size = 363128, upload-time = "2025-02-28T01:23:50.399Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/12f6a58eca6dea4be992d6c681b7ec9410a1d9f5cf368c61437e31daa879/bcrypt-4.3.0-cp39-abi3-win32.whl", hash = "sha256:b4d4e57f0a63fd0b358eb765063ff661328f69a04494427265950c71b992a39a", size = 160598, upload-time = "2025-02-28T01:23:51.775Z" }, + { url = "https://files.pythonhosted.org/packages/a9/cf/45fb5261ece3e6b9817d3d82b2f343a505fd58674a92577923bc500bd1aa/bcrypt-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b", size = 152799, upload-time = "2025-02-28T01:23:53.139Z" }, +] + +[[package]] +name = "certifi" +version = "2026.5.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" }, +] + +[[package]] +name = "click" +version = "8.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/98/518d8e5081007684232226f475082b30087d0f585e8457db087298259f49/click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96", size = 353007, upload-time = "2026-05-22T04:08:37.769Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "dorm-repair-backend" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "bcrypt" }, + { name = "fastapi" }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "python-multipart" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[package.dev-dependencies] +dev = [ + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "bcrypt", specifier = ">=4,<5" }, + { name = "fastapi", specifier = ">=0.115,<1" }, + { name = "httpx", specifier = ">=0.28,<1" }, + { name = "pydantic", specifier = ">=2.11,<3" }, + { name = "python-dotenv", specifier = ">=1.0,<2" }, + { name = "python-multipart", specifier = ">=0.0.9,<1" }, + { name = "uvicorn", extras = ["standard"], specifier = ">=0.30,<1" }, +] + +[package.metadata.requires-dev] +dev = [{ name = "ruff", specifier = ">=0.15,<1" }] + +[[package]] +name = "fastapi" +version = "0.136.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/2d/ff8d91d7b564d464629a0fd50a4489c97fcb836ac230bf3a7269232a9b1f/fastapi-0.136.3.tar.gz", hash = "sha256:e487fae93ad408e6f47641ee4dfe389864fd7bec92e547ea8498fc13f43e83ab", size = 396410, upload-time = "2026-05-23T18:53:15.192Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/82/45359b62a067409bd929ae8a56b8ed13e5a8c8a61194b3c236920999ab83/fastapi-0.136.3-py3-none-any.whl", hash = "sha256:3d2a69bdf04b7e9f3afa292c3bc7a98816bbfafa10bc9b45f3f3700d2f761620", size = 117481, upload-time = "2026-05-23T18:53:16.924Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httptools" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" }, + { url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" }, + { url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440, upload-time = "2025-10-10T03:54:42.452Z" }, + { url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186, upload-time = "2025-10-10T03:54:43.937Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192, upload-time = "2025-10-10T03:54:45.003Z" }, + { url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694, upload-time = "2025-10-10T03:54:45.923Z" }, + { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" }, + { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" }, + { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268, upload-time = "2025-10-10T03:54:49.993Z" }, + { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517, upload-time = "2025-10-10T03:54:51.066Z" }, + { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337, upload-time = "2025-10-10T03:54:52.196Z" }, + { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743, upload-time = "2025-10-10T03:54:53.448Z" }, + { url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" }, + { url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" }, + { url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831, upload-time = "2025-10-10T03:54:57.219Z" }, + { url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631, upload-time = "2025-10-10T03:54:58.219Z" }, + { url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910, upload-time = "2025-10-10T03:54:59.366Z" }, + { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/88/bcf9709822fe69d02c2a6a77956c98ce6ea8ca8767a9aadcedc7eb6a2390/idna-3.16.tar.gz", hash = "sha256:d7a6da03db833450fca25d2358ac9ff06cd624577a4aea3a596d5c0f77b8e03d", size = 203770, upload-time = "2026-05-22T00:16:18.781Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/16/70255075a9859a0e3adb789b68ceb0e210dec03934245fd98d248226572f/idna-3.16-py3-none-any.whl", hash = "sha256:cc246e3a3f89580c3a951b5ad298ca4638078b2cdd4f115654332b5c26daded5", size = 74165, upload-time = "2026-05-22T00:16:16.698Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.29" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/fe/70bd71a6738b09a0bdf6480ca6436b167469ca4578b2a0efbe390b4b0e70/python_multipart-0.0.29.tar.gz", hash = "sha256:643e93849196645e2dbdd81a0f8829a23123ad7f797a84a364c6fb3563f18904", size = 45678, upload-time = "2026-05-17T17:29:47.654Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/cb/769cfc37177252872a45a71f3fbdde9d51b471a3f3c14bfe95dde3407386/python_multipart-0.0.29-py3-none-any.whl", hash = "sha256:2ddcc971cef266225f54f552d8fa10bcfbb1f14446caec199060daac59ff2d69", size = 29640, upload-time = "2026-05-17T17:29:45.69Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/84/6f/a76f7d96e5c962f5b69cee865e49c15c1116897c01990faa8a57edb62e7f/ruff-0.15.15.tar.gz", hash = "sha256:b8dff018130b46d8e5bf0f926ef6b60cf871d6d5ae45fc9334e09632daa741d6", size = 4706985, upload-time = "2026-05-28T14:16:57.784Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/9d/3a45c05b8ab04b4705989de70a79008e27c8003296a0feaee9edc18dd7e9/ruff-0.15.15-py3-none-linux_armv6l.whl", hash = "sha256:cf93e5388f412e1b108b1f8b34a6e036b70fe8aff89393befad96fe48670311b", size = 10710652, upload-time = "2026-05-28T14:16:06.701Z" }, + { url = "https://files.pythonhosted.org/packages/05/66/da974431624bf3b49f6ee1f9543c02d929ff1cba78b0d5a79c38cf21f744/ruff-0.15.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac5a646d1f6a7dadd5d50842dae2c1f9862ac887ef5d1b1375e02def791fde6e", size = 11096615, upload-time = "2026-05-28T14:16:23.313Z" }, + { url = "https://files.pythonhosted.org/packages/8c/09/7443452e5d290230a712103f2fdceeef7184f3ec99a2bd01c8be78aaceb5/ruff-0.15.15-py3-none-macosx_11_0_arm64.whl", hash = "sha256:77d955a431430c66f72dd94e379ad38a16daea3d25094872ac4edf9e797be530", size = 10436683, upload-time = "2026-05-28T14:16:40.974Z" }, + { url = "https://files.pythonhosted.org/packages/53/01/d330c26a57fa4f3943a14424904027428315b700fe4d14a84bb123a649e5/ruff-0.15.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7614ee79c69788cf6cedd568069ade9cecc22a1ad20494efe8d0c9ebb4b622d4", size = 10769064, upload-time = "2026-05-28T14:16:28.905Z" }, + { url = "https://files.pythonhosted.org/packages/1d/85/cc8770f8bdff541b1da8392d1634141fe4a0e3f4ee596605959b7906c27f/ruff-0.15.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3cdb1679e06a1f6b47bc384714ae96f6e2fb65ca441eb78c43d2ca554176ce1f", size = 10511987, upload-time = "2026-05-28T14:16:43.732Z" }, + { url = "https://files.pythonhosted.org/packages/7c/29/8c190c1472b63013583ba391f3342036e02010544c1270455ed8e519bdf3/ruff-0.15.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2728b93d7b23a603ea2c0ac6eb73d760bd38ec9de35f35fb41e18f7a3fee7622", size = 11275100, upload-time = "2026-05-28T14:16:55.244Z" }, + { url = "https://files.pythonhosted.org/packages/9f/6b/7e145ce2cc8e63d6834eca03d83a0e18d121def5c69f91b4cf4011ed4879/ruff-0.15.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be582fcc0db438902c7792b08d6ddf6c9b9e21addaa10092c2c741cfb09e5a45", size = 12176903, upload-time = "2026-05-28T14:16:14.368Z" }, + { url = "https://files.pythonhosted.org/packages/80/a3/d5974637f68e451f7fadf015cf3101d1cd7d8ba5027cffe0b9e3826ebe6b/ruff-0.15.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7aa77465b8ecaf1a27bea098d696f7fed5e1eccbd10b321b682d6de586ae5627", size = 11404550, upload-time = "2026-05-28T14:16:20.138Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1c/e6e5e568f22be4fb05d6244234aba384c06b451252453b821e1a529263cf/ruff-0.15.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48decfa11d740de4889de623be1463308346312f2409a56e24aa280c86162dc4", size = 11382027, upload-time = "2026-05-28T14:16:46.615Z" }, + { url = "https://files.pythonhosted.org/packages/1d/01/170921b49fcd2e8858825593f91cf7146c3e40a5c3e6df763e4bb0484dde/ruff-0.15.15-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a5015088452ca0081387063649ec67f06d3d1d6b8b936a1f836b5e9657ecd48c", size = 11366041, upload-time = "2026-05-28T14:16:26.247Z" }, + { url = "https://files.pythonhosted.org/packages/87/54/a7bad711d7de93254e15e06a4c375b89a03d18de45d3e5dcc86a4472fb1a/ruff-0.15.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5294aab6356c81600fcdea3a62bb1b924dfd5e91767c12318d3f68f86af57cd", size = 10741795, upload-time = "2026-05-28T14:16:17.11Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/38c075963668f8b41c6914ee0f6f318727fbe30ab9145cb29e6df464c5fa/ruff-0.15.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:db5bd4d802415cca656dc1616070b725952d6ae95eb5d4831e49fbd94a38f75f", size = 10511117, upload-time = "2026-05-28T14:16:31.767Z" }, + { url = "https://files.pythonhosted.org/packages/9d/96/6ff689e1f7e375d1d97075eca022f74c2bab59554a432fe4d2e6f091986a/ruff-0.15.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:587a6278ed42059191c1a466e490bd7930fb50bd2e255398bc29616c895a61cb", size = 10994867, upload-time = "2026-05-28T14:16:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c2/5dce0ab9f92a8d534fa62b9bf9caca3eddb8c1a81b616f5e195ada4f0d6e/ruff-0.15.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:df0c1c084f5f4be9812f61518a45c440d3c30d69ce4bf6c5270e66d38338f02a", size = 11482101, upload-time = "2026-05-28T14:16:49.598Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c0/1003b60edd697c649faf61f1a34094b1abb38fb3d1181e3f895781250a08/ruff-0.15.15-py3-none-win32.whl", hash = "sha256:29428ea79694afbe756d45fd59b36f22b6b020dc0443cf7de0173046236964b9", size = 10716774, upload-time = "2026-05-28T14:16:52.337Z" }, + { url = "https://files.pythonhosted.org/packages/02/a8/1269eddd6945a06c23f055ef7848886e37cf9d6a8bebb386a3115f01470c/ruff-0.15.15-py3-none-win_amd64.whl", hash = "sha256:8df0323902e15e24bc4bf246da830573d3cf3352bd0b9a164eab335d111ff4a4", size = 11868463, upload-time = "2026-05-28T14:16:11.333Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b2/920464c907b191e37469d477a1aa8bc048b8f36c4c1610dfa4ab87b39e18/ruff-0.15.15-py3-none-win_arm64.whl", hash = "sha256:3c8ceca6792f38196b8f589bc92eccd03eef286602da92e5dc05cc42ef6441b7", size = 11138498, upload-time = "2026-05-28T14:16:38.425Z" }, +] + +[[package]] +name = "starlette" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/66/4d20cdf39a8d6a51e663b7038e3b828ff211d3891a43a713fe7e4643f3a8/starlette-1.1.0.tar.gz", hash = "sha256:e83c7fe0ddecd8719c5b840080325aec0260acec86e9832899e377b91d65e90f", size = 2660060, upload-time = "2026-05-23T16:55:41.376Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/79/920b8e0a8b20f793e8d64855095cb8febabf6175b8550b6f7a547d813891/starlette-1.1.0-py3-none-any.whl", hash = "sha256:7f0dfd38e428aad5cb6f9f667f0ca1d2d8ca3f3385dccac8305f79ec98458382", size = 72899, upload-time = "2026-05-23T16:55:39.201Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.48.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e6/bf/f6544ba992ddb9a6077343a576f9844f7f8f06ab819aefd00206e9255f18/uvicorn-0.48.0.tar.gz", hash = "sha256:a5504207195d08c2511bf9125ede5ac4a4b71725d519e758d01dcf0bc2d31c37", size = 91074, upload-time = "2026-05-24T12:08:41.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/be/72532be3da7acc5fdfbccdb95215cd04f995a0886532a5b423f929cda4cc/uvicorn-0.48.0-py3-none-any.whl", hash = "sha256:48097851328b87ec36117d3d575234519eb58c2b22d79666e9bbc6c49a761dad", size = 71410, upload-time = "2026-05-24T12:08:40.258Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/41/5e1a4bb12aac5f1493fa1bdc11154eca3b258ca4eba65d39c473fe19d8e9/watchfiles-1.2.0.tar.gz", hash = "sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838", size = 108252, upload-time = "2026-05-18T04:32:04.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/2f/e42c992d2afda3108ea1c02acecc991b9f31d05c14adc2a7cee9ee211fc4/watchfiles-1.2.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:bc13eb17538be00c874699dc0abe4ee2bc8d50bb1166a6b9e175ef3fd7eb8f26", size = 400115, upload-time = "2026-05-18T04:32:02.06Z" }, + { url = "https://files.pythonhosted.org/packages/5f/8f/6af2ea19065c91d8b0ea3516fdfc8c0d349f407e8e9fbf4e5a17360de8ad/watchfiles-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d95ddc1eb6914154253d239089900813f6a767e174b8e6a50e7fdacb7e4236c", size = 393659, upload-time = "2026-05-18T04:30:50.951Z" }, + { url = "https://files.pythonhosted.org/packages/13/01/b32a967c56fb3e3e5be3db52c3d3b87fa4513aa367d8ed1ad96d42952e5f/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f70d8b291ef6e88d19b1f297a6905ddb978888d9272b0d05e6f53309856bcfc", size = 453207, upload-time = "2026-05-18T04:31:04.231Z" }, + { url = "https://files.pythonhosted.org/packages/04/98/97557a812180338cb1abd32e1cffcc4588f59b5f23e0cb006b2ba95ba64a/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56d8641cf834c2836922899105bd3ce3d0dfc69291d52edf0b4d0436829b34c0", size = 459273, upload-time = "2026-05-18T04:31:50.377Z" }, + { url = "https://files.pythonhosted.org/packages/e8/a8/b4b08dcb7653b8087c6586f7ce649505900e866bbcfe40dc9587af02e686/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2581a94056e55d7d0a31a823ea92bf73749c489ca2285bfdc0fbe6b2bb49d50c", size = 489927, upload-time = "2026-05-18T04:31:42.485Z" }, + { url = "https://files.pythonhosted.org/packages/50/94/3dceea03545d2e5ddfd839f0ddd5e1cecbf1697b5a428d5ba11cef6af95d/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41bc1199f7523b3f82843c88cbb979180c949caef0342cf90968f178e5d49b01", size = 570476, upload-time = "2026-05-18T04:31:03.071Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f2/d39a5450c3532092b91f81d274360e613c2371bc874a89c7a1a3c5e8d138/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7571e4464cb6e434958f867f7f730b8ab0b75e3f8e5eac0499168486ab3c33a8", size = 465650, upload-time = "2026-05-18T04:30:12.701Z" }, + { url = "https://files.pythonhosted.org/packages/22/24/ed72f68cbc1333ca9b9f2200aa048bb6658ae41709bc1caad4310f4bdffd/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e53a384f76b631c3ae5334ce6a52f0baa3a911eb94a4eac7f160079868b716d5", size = 456398, upload-time = "2026-05-18T04:30:13.784Z" }, + { url = "https://files.pythonhosted.org/packages/0d/64/982ef4a4e5bab5b6e5b6becc8cd5e732f6130a78b855f0abec6439a9a135/watchfiles-1.2.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:d20029a60a71a052a24c4db7673bc4de39ab89adbaccbfb5d67987c5d73f424d", size = 465140, upload-time = "2026-05-18T04:31:52.111Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0c/95282abf4ed680b6096010bcfc30c5fa7a041fc5aa5a2ad17a2cc6c75bba/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2cb93af48550faf1cea04c303107c8b75833de7013e57ce27d3b8d21d8d0f58c", size = 630259, upload-time = "2026-05-18T04:31:25.676Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/607c1de1530c4bdcf2cf1d1ecc2505ddba5d96bd43ba9f2b0e79876f850f/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2995c176de7692b86a2e4c58d9ec718f753150a979cb4a754e2b4ffa38e70906", size = 659859, upload-time = "2026-05-18T04:30:24.333Z" }, + { url = "https://files.pythonhosted.org/packages/fa/08/d9e2e0f9e8e6791d33aefc694ad7eefa7f901f63caff84a81ded38692f9c/watchfiles-1.2.0-cp312-cp312-win32.whl", hash = "sha256:7a2cffd17d27d2ecbb310c2b1d8174f222a5495b1a721894afa88ec11e25b898", size = 275480, upload-time = "2026-05-18T04:30:31.307Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e6/9d42569c0102645cc8cea5d8c7d8a1e9d4ada2cb7f05f75e554b8aa2202a/watchfiles-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:f155b3a1b2a5fc89cdc70d47ee5d54e3b75e88efa34982028a35daef9ba00379", size = 288718, upload-time = "2026-05-18T04:32:10.745Z" }, + { url = "https://files.pythonhosted.org/packages/0a/26/88e0dc6ee3898169d7fa22bb6a69cabf2502d2ee25cb8c876d1262d204f8/watchfiles-1.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:8fa585ede612ee9f9e91b18bebf9ba11b9ae29a4e3a0d0cf6fca3e382133f0d5", size = 281026, upload-time = "2026-05-18T04:30:22.23Z" }, + { url = "https://files.pythonhosted.org/packages/d1/4d/70a7feced9f87e2ff26dba42667290f41694fc64646c67261fbb8cab5d5c/watchfiles-1.2.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:01ea8d66f0693b9b60a6541c8d10263091ca9a9060d242f3c1f3143f9aad2c98", size = 399730, upload-time = "2026-05-18T04:31:38.162Z" }, + { url = "https://files.pythonhosted.org/packages/31/3a/0da302f2307aee316922806ebd5726c542cbd787c938271cf14a074c7daf/watchfiles-1.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ba0480b9a74af058f43b337e937a451e109295c420916d68ad24e3dc02f5e44", size = 392842, upload-time = "2026-05-18T04:30:27.051Z" }, + { url = "https://files.pythonhosted.org/packages/db/ef/d5bdb705c224dbc256aa0c1ec47bf4e61ec52558f2afb44a71a1fe4d7015/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f34e26a19f91f710c08e0183429f0d1d15df734e6bc78c31e77b9ea9c433658", size = 452989, upload-time = "2026-05-18T04:31:11.945Z" }, + { url = "https://files.pythonhosted.org/packages/71/29/5495f2c1661949ef7a35e4d71111d129cfe7606414a26887a919d0a55406/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b4e77f6a55f858504069abd35d336a637555c09bca453dde1ee1e5ada8a6a1fb", size = 458978, upload-time = "2026-05-18T04:30:52.606Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/7f9c07c433811c2fffd93e13fdfb7135de9aab5f2ae41be08960fa0047dc/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0cb4d80e212f116474a545c21c912b445f16bb0cef9e6a73a498164223e14e2f", size = 490248, upload-time = "2026-05-18T04:31:36.003Z" }, + { url = "https://files.pythonhosted.org/packages/3c/11/d93632febc52fbc21be90231bb7c17fd5387f46c9076fd40a5f9c2ae6910/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b974946a10af379d425e2eef5b62f5c6ebeaccf91d45eaad6f5b27ecd4f91aa0", size = 571847, upload-time = "2026-05-18T04:31:10.862Z" }, + { url = "https://files.pythonhosted.org/packages/55/b4/383173e73aabb07ad1d9c7aa859d95437ac46a6d6a1e11005facda0c9d19/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86bc13c25a8d1fcd70b51d0ce7c9b65e90de5666fcbfd3e34957cc73ee19aeb5", size = 465974, upload-time = "2026-05-18T04:30:17.006Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6c/89b1a230a78f57c52dd8893adb1f92f94411721b6ec12596c56d98c74356/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca148d73dea36c9763aaa351e4d7a51780ec1584217c45276f4fe8239c768b71", size = 454782, upload-time = "2026-05-18T04:30:35.656Z" }, + { url = "https://files.pythonhosted.org/packages/24/62/1732118367cfff0a9fce3bf62ff4bfded09ef5df21d9d446b858b3f70a96/watchfiles-1.2.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:c525543d91961c6955b2636b308569e84a1d1c5f5f2932041ab9ef46422f43e3", size = 465182, upload-time = "2026-05-18T04:30:20.846Z" }, + { url = "https://files.pythonhosted.org/packages/28/96/716f7e5f51339bf22963f3345f9f27d7f3b30e2eadc597e257c881dd3c53/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a204794696ffb8f9b10fba6f7cb5216d42f3b2b71860ccac6b6e42f5f10973b0", size = 629841, upload-time = "2026-05-18T04:31:05.397Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/c40783950fd771ccf66ab3ec2722d188a9af1c7f96c6e811f36e40c6e03f/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:10d86db20695afe7997ac9e1717637d6714a8d0220458c33f3d2061f54cec427", size = 658028, upload-time = "2026-05-18T04:31:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/71/72/4508db1856d1d87fcbb3b63f4839bab1b5682cb0e8d224d122263c09654a/watchfiles-1.2.0-cp313-cp313-win32.whl", hash = "sha256:eb283ee99e21ad6443c8cdb06ac5b34b1308c329cbdf03fa02b445363714c799", size = 275183, upload-time = "2026-05-18T04:30:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/f9/36/14b76ca57652e5cc5fd1c11f32a261292c08a0d19a00351013c2549cbfb2/watchfiles-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:a0f27f01bee51861392bb6b7c4fdb290b27d1eb194e9e28788d68102a0e898d9", size = 288059, upload-time = "2026-05-18T04:32:07.937Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8d/0a85e395398d8d20fadfe5c5d32c726eee17a519e78fb356f2cf7531bffe/watchfiles-1.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:3651aa7058595e9cfb75d35dd5ada2bf9f48a5b8a0f3562821d3e210c507e077", size = 280186, upload-time = "2026-05-18T04:31:54.484Z" }, + { url = "https://files.pythonhosted.org/packages/37/68/36db056f1fdcc5f07302f56e631774d6835bcd6fa3ace402304621d5f9e5/watchfiles-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:faea288b6f0ab1902ef08f4ca6de005dccf856c4e0c4f21b8c5fce02d90a1b08", size = 399031, upload-time = "2026-05-18T04:30:44.576Z" }, + { url = "https://files.pythonhosted.org/packages/c1/64/01a9d6f66a82a5c101ce939274106cc72759d62427e153f01edd2b9f87c2/watchfiles-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9", size = 391205, upload-time = "2026-05-18T04:30:25.413Z" }, + { url = "https://files.pythonhosted.org/packages/84/2c/0a44fe058cb4bb7b8ede6b6670698bbb7c0400740e378d00022189b7b31d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4", size = 451892, upload-time = "2026-05-18T04:32:14.005Z" }, + { url = "https://files.pythonhosted.org/packages/67/a1/351e0d56cd35e6488b5c8b4fb11a809a5bc923e8fe8fed9faf8920be0c89/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b141a4891c995a039cd89e9a49e62df1dc8a559a5d1a6e4c7106d16c12777a55", size = 458867, upload-time = "2026-05-18T04:31:22.279Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7d/9d09605187f1b838998624049fcf8bf47b73c1a3b76901fcac1782f62277/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f22943b7770483f6ea0721c6b11d022947a98eb0acae14694de034f4d0d38925", size = 490217, upload-time = "2026-05-18T04:31:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/60/5d/a17a16eccb182f04188cd308ec24b1a71a9b5c4e7098269cf35d9fa56d02/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bc6195825b7dcd217968bb1f801a60fd4c16e8eeab5bedc7fe917d7d5995ab4", size = 571458, upload-time = "2026-05-18T04:32:11.875Z" }, + { url = "https://files.pythonhosted.org/packages/d3/3d/4dd457062083ab1938e5dfd45032eb425cee2ac817287ca8ff4356183e5d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4a4b147f5dca2a5d325a06a832fb43f345751adfbc63204aec30e0d9ca965a2", size = 464707, upload-time = "2026-05-18T04:30:43.492Z" }, + { url = "https://files.pythonhosted.org/packages/c6/71/ea8c57b128f5383de74d0c7d2d9c57ad7c9a65a930c451bd25d524b295b7/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4543579a9bdb0c9560039b4ffddbdb39545707659fbc430ce4c10f3f68d557f9", size = 454663, upload-time = "2026-05-18T04:30:16.061Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/2e812bf938406d7db351f0703ddd3fc6c061cf30d96153a77bc79a943a44/watchfiles-1.2.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa", size = 463537, upload-time = "2026-05-18T04:31:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/86/56/d17a7f1dd1bc3035f1072694a551301272f1739c2d8e319c927cb9e29b38/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44", size = 629194, upload-time = "2026-05-18T04:31:14.141Z" }, + { url = "https://files.pythonhosted.org/packages/be/06/f1ff66bf5cae50aa4062779a0ecd0bbaf15e466195719074078947d9a17d/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72", size = 656194, upload-time = "2026-05-18T04:31:47.14Z" }, + { url = "https://files.pythonhosted.org/packages/e7/54/a9c7ea9a82a4ac65e7004c0a03920b5cdd2f9c3b678757d9cd425aa51d53/watchfiles-1.2.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4", size = 400205, upload-time = "2026-05-18T04:32:05.153Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5d/c9ab3534374a4a67450696905d6ef16a04405448b8dc52bd752ae50423d4/watchfiles-1.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281", size = 392508, upload-time = "2026-05-18T04:30:54.849Z" }, + { url = "https://files.pythonhosted.org/packages/26/ca/1ad30103535cf0cecd7b993e8d50edc5351b1820e38f2d22e3df58962feb/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d", size = 452448, upload-time = "2026-05-18T04:30:53.727Z" }, + { url = "https://files.pythonhosted.org/packages/37/a1/ceee2cdf2afbd715fa07758d39c9859513eae411b23196f7fd039e5feedd/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e", size = 459605, upload-time = "2026-05-18T04:30:23.312Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f6/421e30fd1cb3907a84ed92ab3f1983e37ba2dca015e9a894a048418417a2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242", size = 490757, upload-time = "2026-05-18T04:30:47.358Z" }, + { url = "https://files.pythonhosted.org/packages/41/b0/55ed1b97ed08be7bba6f9a541cac15f2a858e1d74d2b07b6da70a82aab00/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add", size = 568672, upload-time = "2026-05-18T04:30:38.915Z" }, + { url = "https://files.pythonhosted.org/packages/d1/cf/d8ae8a80dd7bafab395ea7681c10237311bbf34d37704a8c744e7cf31fc7/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f", size = 464197, upload-time = "2026-05-18T04:30:09.914Z" }, + { url = "https://files.pythonhosted.org/packages/7c/8a/3076c496ca8dafe0e8cd03fcebdfc47be4b1174b4e5b24ff6e396e6b3af2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7", size = 453181, upload-time = "2026-05-18T04:30:14.829Z" }, + { url = "https://files.pythonhosted.org/packages/e5/10/9745e17c98e7b8a86454df0a3c7b5686bd650383f1e9f26e4ebcbd6cc0c0/watchfiles-1.2.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e", size = 465109, upload-time = "2026-05-18T04:30:28.123Z" }, + { url = "https://files.pythonhosted.org/packages/8f/95/8ef4a95481d3e0cb52d62a06fa6e972e81424be2d9698b91a2fecca9904c/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06", size = 630653, upload-time = "2026-05-18T04:31:49.304Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e4/3b3bf36b0f829b50c6ebcb8d031583863c59f923d6a6af3d485e470d0fac/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba", size = 657838, upload-time = "2026-05-18T04:31:06.497Z" }, + { url = "https://files.pythonhosted.org/packages/21/b1/6cbbb50c1f3002ab568777d44aa21206dfb8807a840990c4037523b51812/watchfiles-1.2.0-cp314-cp314-win32.whl", hash = "sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7", size = 275108, upload-time = "2026-05-18T04:30:06.891Z" }, + { url = "https://files.pythonhosted.org/packages/92/45/190ce6db8dcb4536682cf75d3889ff1a27182a58cb519d343cb6d9ea63d8/watchfiles-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103", size = 288441, upload-time = "2026-05-18T04:32:12.901Z" }, + { url = "https://files.pythonhosted.org/packages/74/0d/3eae1c2313ab08378431d907c3f8095ecca00f3eda33111cf4f0f2591799/watchfiles-1.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3", size = 280684, upload-time = "2026-05-18T04:31:26.902Z" }, + { url = "https://files.pythonhosted.org/packages/b1/75/fb64e6c25d6b5ca636d03df34ffb1c6e9873303e76d27967e045f8df088f/watchfiles-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2", size = 398857, upload-time = "2026-05-18T04:32:17.108Z" }, + { url = "https://files.pythonhosted.org/packages/73/4e/9f7adf01754cbf81843722ccfec169d8f26c69778281a302855cecd2ee08/watchfiles-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28", size = 392413, upload-time = "2026-05-18T04:31:07.911Z" }, + { url = "https://files.pythonhosted.org/packages/47/c8/bec626bcc2d69f44b9acb24ce7d60ed7b16b73628eea747fcbd169d8edda/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831", size = 452409, upload-time = "2026-05-18T04:31:20.142Z" }, + { url = "https://files.pythonhosted.org/packages/00/b7/b6362068e81e7c556d155a34c35d40ac3ef42d747b06d7f6e5bf58e359c2/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33", size = 458827, upload-time = "2026-05-18T04:32:06.219Z" }, + { url = "https://files.pythonhosted.org/packages/67/f8/9a813fa42afb1e0b4625e75f0479826644d3ee8dc287e093799bc01f390c/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4", size = 490104, upload-time = "2026-05-18T04:31:56.034Z" }, + { url = "https://files.pythonhosted.org/packages/2f/bf/27dfb6094ca4c9aad21298b5525b6c53cb36121ee454331d05161e58d130/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b", size = 571360, upload-time = "2026-05-18T04:31:57.133Z" }, + { url = "https://files.pythonhosted.org/packages/fb/39/44a096d67270ea93df91d33877dbe91fbda3aa4f8ec2edf799d93eda8736/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666", size = 464644, upload-time = "2026-05-18T04:30:57.33Z" }, + { url = "https://files.pythonhosted.org/packages/0e/80/c7472203bad6268e3ef1ad260739704847898938ad7ea8b63a5131f46b50/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925", size = 454771, upload-time = "2026-05-18T04:30:48.736Z" }, + { url = "https://files.pythonhosted.org/packages/51/cf/3b10b268b4b7f0fc26e9debb5eef1998b515887840f444cd3ec80c688755/watchfiles-1.2.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b", size = 463494, upload-time = "2026-05-18T04:31:33.826Z" }, + { url = "https://files.pythonhosted.org/packages/3d/3e/a4302545cd589262a0dc7d140e86f7688eba3f9c72776c27f7e23b8864c4/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30", size = 629383, upload-time = "2026-05-18T04:31:15.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/99/d5649df0a9a410d45b7c882304d0b790903ac9b6e8f2cfd12114e0c6b9f2/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5", size = 656093, upload-time = "2026-05-18T04:31:58.707Z" }, + { url = "https://files.pythonhosted.org/packages/92/b9/362702539275019a54dd2e94511b31a9b89c5f9e6a21966de7eb692549fc/watchfiles-1.2.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374", size = 400109, upload-time = "2026-05-18T04:31:16.879Z" }, + { url = "https://files.pythonhosted.org/packages/8f/75/71d5ba62db781e5587bded1d944c675374bc4aa37ff33d5018d98e8b6538/watchfiles-1.2.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65", size = 392167, upload-time = "2026-05-18T04:31:28.058Z" }, + { url = "https://files.pythonhosted.org/packages/3c/01/c66dd95d0423fe30d31820e2d1d5bda773764131bbb6ac0cb1cf303ac328/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69", size = 452372, upload-time = "2026-05-18T04:31:00.836Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/2fe99557e72f85627c6a8eed50d889e8d101623e060a22ad75b875cb932d/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579", size = 459596, upload-time = "2026-05-18T04:31:34.96Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/d4acfa0023367428ed48351b3b9b267893037b6cadae55620c61c24bcfd4/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7", size = 490869, upload-time = "2026-05-18T04:31:59.923Z" }, + { url = "https://files.pythonhosted.org/packages/a4/5f/3164cbdce06c9fb95c4f7b9e2f9760b5e2797af43a9ecc317ef42a23a278/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2", size = 571641, upload-time = "2026-05-18T04:32:00.948Z" }, + { url = "https://files.pythonhosted.org/packages/41/e6/85d3731c55e65cd7690f3f803d24c139588aaf863e4bf2148fe7a7fa1a19/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6", size = 464444, upload-time = "2026-05-18T04:30:34.298Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7d/562641012b8b09872742c3b8adf9629ec479fd78f8d68ae4a0c13da8add6/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4", size = 453593, upload-time = "2026-05-18T04:31:23.464Z" }, + { url = "https://files.pythonhosted.org/packages/56/fe/cb8ef3d6f929d14158fdaaad9925985b7310abc9384dcd4d82dd0016fb59/watchfiles-1.2.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488", size = 465096, upload-time = "2026-05-18T04:31:30.384Z" }, + { url = "https://files.pythonhosted.org/packages/25/91/80908e835e100527a9267147b08c0eee1fa6ab0ffec15edc04d1d44885f7/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb", size = 630638, upload-time = "2026-05-18T04:30:49.89Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/95ab2f256bb4af3cb2eb23b9317bda984ee6e0f11733a5c004a6c95b06e3/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377", size = 657684, upload-time = "2026-05-18T04:31:32.027Z" }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +]