From 47bf6cc5cadfd80fe7d1835da6cf9a551a3bd302 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E7=BB=AB?= <486494914@qq.com> Date: Tue, 1 Sep 2026 11:53:59 +0800 Subject: [PATCH] =?UTF-8?q?=E7=AC=AC=E4=B8=80=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 40 + .gitignore | 41 + CLAUDE.md | 47 + README.md | 86 + backend/Dockerfile | 22 + backend/app/__init__.py | 0 backend/app/api/__init__.py | 0 backend/app/api/health.py | 83 + backend/app/core/__init__.py | 0 backend/app/core/config.py | 66 + backend/app/core/db.py | 76 + backend/app/core/errors.py | 155 + backend/app/core/logging.py | 31 + backend/app/core/rate_limit.py | 62 + backend/app/core/session.py | 74 + backend/app/main.py | 68 + backend/app/models/__init__.py | 0 backend/app/processors/__init__.py | 0 backend/app/processors/parsers/__init__.py | 0 backend/app/public/__init__.py | 0 backend/app/repositories/__init__.py | 0 backend/app/retrieval/__init__.py | 0 backend/app/schemas/__init__.py | 0 backend/app/services/__init__.py | 0 backend/app/storage/__init__.py | 0 backend/app/storage/base.py | 28 + backend/app/storage/local_storage.py | 67 + backend/app/storage/object_keys.py | 50 + backend/pyproject.toml | 24 + backend/requirements.txt | 39 + backend/tests/__init__.py | 0 backend/tests/test_smoke.py | 36 + docker-compose.yml | 45 + docs/requirements.md | 342 ++ docs/technical-review.md | 536 +++ frontend/Dockerfile | 18 + frontend/index.html | 12 + frontend/package-lock.json | 3815 ++++++++++++++++++++ frontend/package.json | 33 + frontend/src/App.vue | 7 + frontend/src/App.vue.js | 26 + frontend/src/api/client.js | 35 + frontend/src/api/client.ts | 41 + frontend/src/env.d.ts | 7 + frontend/src/main.js | 12 + frontend/src/main.ts | 14 + frontend/src/router/index.js | 37 + frontend/src/router/index.ts | 39 + frontend/src/stores/user.js | 18 + frontend/src/stores/user.ts | 22 + frontend/src/views/Dashboard.vue | 10 + frontend/src/views/Dashboard.vue.js | 22 + frontend/src/views/KbDetail.vue | 10 + frontend/src/views/KbDetail.vue.js | 19 + frontend/src/views/KnowledgeBases.vue | 10 + frontend/src/views/KnowledgeBases.vue.js | 19 + frontend/src/views/Login.vue | 27 + frontend/src/views/Login.vue.js | 39 + frontend/src/views/Register.vue | 27 + frontend/src/views/Register.vue.js | 39 + frontend/src/views/Settings.vue | 10 + frontend/src/views/Settings.vue.js | 19 + frontend/tsconfig.json | 25 + frontend/tsconfig.tsbuildinfo | 1 + frontend/vite.config.ts | 27 + nginx/nginx.conf | 43 + 66 files changed, 6501 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 README.md create mode 100644 backend/Dockerfile create mode 100644 backend/app/__init__.py create mode 100644 backend/app/api/__init__.py create mode 100644 backend/app/api/health.py create mode 100644 backend/app/core/__init__.py create mode 100644 backend/app/core/config.py create mode 100644 backend/app/core/db.py create mode 100644 backend/app/core/errors.py create mode 100644 backend/app/core/logging.py create mode 100644 backend/app/core/rate_limit.py create mode 100644 backend/app/core/session.py create mode 100644 backend/app/main.py create mode 100644 backend/app/models/__init__.py create mode 100644 backend/app/processors/__init__.py create mode 100644 backend/app/processors/parsers/__init__.py create mode 100644 backend/app/public/__init__.py create mode 100644 backend/app/repositories/__init__.py create mode 100644 backend/app/retrieval/__init__.py create mode 100644 backend/app/schemas/__init__.py create mode 100644 backend/app/services/__init__.py create mode 100644 backend/app/storage/__init__.py create mode 100644 backend/app/storage/base.py create mode 100644 backend/app/storage/local_storage.py create mode 100644 backend/app/storage/object_keys.py create mode 100644 backend/pyproject.toml create mode 100644 backend/requirements.txt create mode 100644 backend/tests/__init__.py create mode 100644 backend/tests/test_smoke.py create mode 100644 docker-compose.yml create mode 100644 docs/requirements.md create mode 100644 docs/technical-review.md create mode 100644 frontend/Dockerfile create mode 100644 frontend/index.html create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/src/App.vue create mode 100644 frontend/src/App.vue.js create mode 100644 frontend/src/api/client.js create mode 100644 frontend/src/api/client.ts create mode 100644 frontend/src/env.d.ts create mode 100644 frontend/src/main.js create mode 100644 frontend/src/main.ts create mode 100644 frontend/src/router/index.js create mode 100644 frontend/src/router/index.ts create mode 100644 frontend/src/stores/user.js create mode 100644 frontend/src/stores/user.ts create mode 100644 frontend/src/views/Dashboard.vue create mode 100644 frontend/src/views/Dashboard.vue.js create mode 100644 frontend/src/views/KbDetail.vue create mode 100644 frontend/src/views/KbDetail.vue.js create mode 100644 frontend/src/views/KnowledgeBases.vue create mode 100644 frontend/src/views/KnowledgeBases.vue.js create mode 100644 frontend/src/views/Login.vue create mode 100644 frontend/src/views/Login.vue.js create mode 100644 frontend/src/views/Register.vue create mode 100644 frontend/src/views/Register.vue.js create mode 100644 frontend/src/views/Settings.vue create mode 100644 frontend/src/views/Settings.vue.js create mode 100644 frontend/tsconfig.json create mode 100644 frontend/tsconfig.tsbuildinfo create mode 100644 frontend/vite.config.ts create mode 100644 nginx/nginx.conf diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..29abc7c --- /dev/null +++ b/.env.example @@ -0,0 +1,40 @@ +# ============================================================ +# AI Knowledge Link — 环境配置模板 +# 复制为 .env 后修改;.env 已被 .gitignore 排除,绝不提交。 +# 生成密钥: python -c "import secrets; print(secrets.token_urlsafe(48))" +# ============================================================ + +# --- 基础 --- +# local / production:production 下 Cookie 强制 Secure +ENVIRONMENT=local +# 用于 Session 签名、Fernet 加密 Secret Token 等全部密码学用途 +SECRET_KEY=change-me-run-python-c-import-secrets-token_urlsafe-48 + +# --- 数据库 --- +# SQLite(开发/生产 MVP 均可): +DATABASE_URL=sqlite:///./data/app.db +# 未来 PostgreSQL: +# DATABASE_URL=postgresql+asyncpg://user:pass@localhost:5432/amb_rag + +# --- 文件存储 --- +# 本地文件系统根目录(相对于 backend/ 工作目录) +STORAGE_ROOT=./data +# 未来 MinIO: +# MINIO_ENDPOINT=localhost:9000 +# MINIO_ACCESS_KEY=change-me +# MINIO_SECRET_KEY=change-me + +# --- 套餐 --- +# 免费用户存储配额(字节) +DEFAULT_STORAGE_QUOTA=104857600 +# 单文件最大(字节) +DEFAULT_MAX_FILE_SIZE=20971520 + +# --- 公共 AI 页面 --- +# 限流:每 token / 每 IP 每分钟 +RATE_LIMIT_PER_TOKEN_PER_MIN=60 +RATE_LIMIT_PER_IP_PER_MIN=30 + +# --- 前端 --- +FRONTEND_ORIGIN=http://localhost:5173 +VITE_API_BASE_URL=/api \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..af1474e --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# Python +__pycache__/ +*.py[cod] +*.egg-info/ +.venv/ +venv/ +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +htmlcov/ +.coverage + +# Node +node_modules/ +frontend/dist/ +*.local + +# Env & secrets +.env +.env.* +!.env.example + +# Data — SQLite + uploaded files + markdown +data/ +!data/.gitkeep + +# IDE +.idea/ +.vscode/ +*.iml + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +logs/ + +# Backup +backup/ \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..5e4f7b2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,47 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project + +**AI Knowledge Link(AI知识链接)** — MVP SaaS:用户上传 Word/PDF 文档 → 自动解析为 Markdown → 建立知识库 → 生成高熵 Secret URL(`/k/{token}`)→ 用户把链接发给第三方 AI → AI 通过 SSR 纯 HTML/Markdown 页面按描述自主选择并读取文档。 + +**核心设计约束:公共 AI 页面(`/k/**`)必须零 JS、零 Cookie、零登录、SSR 输出、标准 HTML**;Secret URL 即访问凭证(128bit 熵,DB 存 SHA-256 hash,统一 404 防存在性探测)。 + +**完整需求清单(含逐节完成状态)见 `docs/requirements.md`;完整技术审查与架构决策见 `docs/technical-review.md`(最高优先级参考)**。 + +## Tech Stack (MVP v1) + +- **Backend**: Python 3.12+(本机 venv 3.13)、FastAPI、SQLAlchemy 2.x(Mapped 风格, 同步引擎 + SQLite)、Pydantic v2、Alembic、Jinja2(公共页 SSR) +- **Frontend**: Vue 3 + TypeScript + Vite + Element Plus + Pinia + Vue Router + Axios +- **Storage**: SQLite(元数据)+ 服务器本地文件系统(原始文件 + Markdown) +- **Parsing**: MarkItDown 优先, PyMuPDF(PDF fallback, AGPL—商品化前需替换为 pypdfium2), python-docx(DOCX fallback) +- **Search**: SQLite FTS5 + KeywordRetriever 抽象 +- **Auth**: Argon2id 密码哈希 + 服务端 Session(内存/SQLite) +- **Rate Limit**: 内存 TokenBucket(单进程) + +**v1 明确不依赖**: PostgreSQL, Redis, Celery, MinIO, Qdrant, Elasticsearch, Kafka + +## Environment + +- **Windows 开发**(无 Docker):Shell 为 Git Bash(POSIX 语法) +- Python venv: `.venv/`(激活:`source .venv/Scripts/activate`,或直接 `.venv/Scripts/python.exe`) +- Node 24 / npm 12 已装 +- IDE 为 PyCharm,`.venv` 已被排除在模块外 +- **Docker**: 仅用于 Ubuntu 生产部署;本地开发直接跑 uvicorn + npm run dev + +## Development Phases(需求 §56,每阶段完成必须验证后再进入下一阶段) + +Phase 1 项目初始化 → 2 数据库+Alembic → 3 用户注册登录 → 4 知识库CRUD → 5 本地StorageService → 6 文档上传 → 7 文档解析+Markdown → 8 文档管理 → 9 Secret URL → 10 AI公共页面 → 11 多格式输出 → 12 搜索 → 13 安全 → 14 前端完善 → 15 测试 → 16 Docker → 17 Ubuntu部署文档 + +## Conventions + +- 分层:api → services → repositories → models;公共 AI 页面独立在 `app/public/`,模板在 `app/templates/` +- 统一错误体:`{"code": "...", "message": "中文文案", "detail": null}`;用户侧永不暴露堆栈/内部路径/storage_path/内部 UUID +- 所有用户数据查询必须带 user_id 隔离校验(防 IDOR) +- 文件操作全部经过 StorageService 抽象,禁止业务代码直接 `open()` / `Path()` +- 上传:流式 SHA256、扩展名+MIME 双校验、随机存储对象名、原始文件名只做展示 +- Token: secrets.token_urlsafe(16), DB 存 SHA-256 hash + Fernet 加密原文, 统一 404 +- 测试:pytest(`backend/tests/`) +- 代码质量:ruff + mypy(backend)、ESLint + Prettier(frontend) +- v1 明确不做:支付、OCR、团队协作、Qdrant、自建聊天机器人、绑定特定 AI、强制 AI 端登录、Redis、Celery、MinIO、PostgreSQL \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f1b3dbb --- /dev/null +++ b/README.md @@ -0,0 +1,86 @@ +# AI Knowledge Link(AI知识链接) + +把用户的 Word / PDF 文档自动解析为 Markdown、组织成知识库,并为每个知识库生成一个**高熵 Secret URL**。用户把链接发给任意支持网页访问的 AI,AI 即可按用户描述自主选择并读取文档。 + +``` +上传文档 → 自动解析 → 转换 Markdown → 建立知识库 → 生成 AI 专属链接 + → 把链接发给任意 AI → AI 读取目录并按描述访问具体文档 +``` + +> 状态:**开发中(Phase 1 — 项目初始化)**。完整架构与技术决策见 [docs/technical-review.md](docs/technical-review.md)。 + +## 技术栈(MVP v1) + +| 层 | 技术 | +|---|---| +| 后端 | Python 3.12+ · FastAPI · SQLAlchemy 2 · Pydantic v2 · Alembic · Jinja2 | +| 前端 | Vue 3 · TypeScript · Vite · Element Plus · Pinia | +| 存储 | SQLite(元数据)+ 本地文件系统(原始文件 + Markdown) | +| 解析 | MarkItDown(优先)· PyMuPDF(PDF fallback)· python-docx(DOCX fallback) | +| 搜索 | SQLite FTS5 | + +## 目录结构 + +``` +backend/ FastAPI 应用(api 管理端 / public AI公共端 / processors 解析 / retrieval 检索 / storage 存储) +frontend/ Vue 3 管理后台 +nginx/ 反向代理配置(生产) +data/ SQLite + 用户文件(.gitignore 排除,仅保留 .gitkeep) +docs/ 架构与技术文档 +``` + +## 快速开始(Windows 开发) + +> 无需 Docker,直接运行。 + +### 后端 + +```bash +cd backend +python -m venv .venv +# Git Bash: +source .venv/Scripts/activate +# 或 CMD: +.venv\Scripts\activate + +pip install -r requirements.txt +alembic upgrade head +uvicorn app.main:app --reload --port 8000 +``` + +- API 文档:http://localhost:8000/api/docs +- 健康检查:http://localhost:8000/api/health + +### 前端 + +```bash +cd frontend +npm install +npm run dev +``` + +- 访问:http://localhost:5173 +- Vite 代理:`/api` 和 `/k` → `localhost:8000` + +## 生产部署(Ubuntu + Docker) + +```bash +git clone /opt/ai-knowledge-link +cd /opt/ai-knowledge-link +cp .env.example .env # 修改全部密钥 +mkdir -p data +docker compose up -d --build +``` + +- 数据持久化:`./data/` 宿主机 volume,删除容器不丢失 +- 备份:SQLite `.backup` + tar 用户文件目录 + +## 安全须知 + +- `.env` 携带全部密钥,**绝不提交 Git** +- 知识库 Secret URL 即访问凭证(链接即钥匙),请勿公开传播 +- 公共 AI 页面不依赖 JS/Cookie/登录;含 `noindex` meta + robots.txt 屏蔽 + +## 文档 + +- [技术审查报告](docs/technical-review.md) — 架构、ER、API、安全模型、风险清单、6 个扩展接口 \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..d8dc016 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,22 @@ +# AI Knowledge Link — Backend Dockerfile +FROM python:3.12-slim AS base + +WORKDIR /app + +# 系统依赖(PyMuPDF 需要) +RUN apt-get update && apt-get install -y --no-install-recommends \ + gcc g++ \ + && rm -rf /var/lib/apt/lists/* + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +# data/ 通过 volume 挂载,不进镜像 +VOLUME /app/data + +EXPOSE 8000 + +# 启动:先跑迁移,再起服务 +CMD ["sh", "-c", "alembic upgrade head && uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 2"] \ No newline at end of file diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/api/health.py b/backend/app/api/health.py new file mode 100644 index 0000000..1282568 --- /dev/null +++ b/backend/app/api/health.py @@ -0,0 +1,83 @@ +"""健康检查。 + +/healthz → 存活探针(liveness):不触碰依赖,快速返回。 +/api/health → 就绪探针(readiness):探测 SQLite + 文件存储。 +""" + +import time + +from fastapi import APIRouter, Response, status +from pydantic import BaseModel +from sqlalchemy import text + +from app.core.config import get_settings +from app.core.db import get_session_factory + +router = APIRouter() + + +class HealthComponent(BaseModel): + status: str + latency_ms: int | None = None + error: str | None = None + + +class HealthReport(BaseModel): + status: str + environment: str + components: dict[str, HealthComponent] + + +@router.get("/healthz") +async def liveness() -> dict[str, str]: + return {"status": "ok"} + + +@router.get("/health", response_model=HealthReport) +async def readiness(response: Response) -> HealthReport: + report = HealthReport( + status="ok", + environment=get_settings().environment, + components={ + "database": _check_db(), + "storage": _check_storage(), + }, + ) + if any(c.status != "ok" for c in report.components.values()): + report.status = "degraded" + response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE + return report + + +def _check_db() -> HealthComponent: + start = time.perf_counter() + try: + factory = get_session_factory() + with factory() as session: + session.execute(text("SELECT 1")) + except Exception as exc: # noqa: BLE001 + return HealthComponent(status="down", error=_brief(exc)) + return HealthComponent(status="ok", latency_ms=_ms(start)) + + +def _check_storage() -> HealthComponent: + start = time.perf_counter() + try: + from app.storage.local_storage import get_storage + + storage = get_storage() + root = storage._root + if not root.exists(): + root.mkdir(parents=True, exist_ok=True) + except Exception as exc: # noqa: BLE001 + return HealthComponent(status="down", error=_brief(exc)) + return HealthComponent(status="ok", latency_ms=_ms(start)) + + +def _ms(start: float) -> int: + return int((time.perf_counter() - start) * 1000) + + +def _brief(exc: Exception) -> str: + text = f"{type(exc).__name__}: {exc}" + return text.split("\n")[0][:200] \ No newline at end of file diff --git a/backend/app/core/__init__.py b/backend/app/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/core/config.py b/backend/app/core/config.py new file mode 100644 index 0000000..d8fd65a --- /dev/null +++ b/backend/app/core/config.py @@ -0,0 +1,66 @@ +"""全局配置:pydantic-settings,全部来自环境变量 / .env。 + +规则(docs/technical-review.md §1.3): +- 密钥不得硬编码;SECRET_KEY 缺失或仍为模板值时,生产环境拒绝启动。 +- DATABASE_URL 可切换 PostgreSQL(扩展接口 1)。 +""" + +from functools import lru_cache +from pathlib import Path + +from pydantic import Field +from pydantic_settings import BaseSettings, SettingsConfigDict + +_TEMPLATE_MARKERS = ("change-me",) + + +class Settings(BaseSettings): + model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore") + + # --- 基础 --- + environment: str = "local" # local / production + secret_key: str = Field(min_length=16) + + # --- 数据库 --- + database_url: str = "sqlite:///./data/app.db" + + # --- 文件存储 --- + storage_root: str = "./data" + + # --- 套餐 --- + default_storage_quota: int = 104_857_600 # 100 MB + default_max_file_size: int = 20_971_520 # 20 MB + + # --- 限流 --- + rate_limit_per_token_per_min: int = 60 + rate_limit_per_ip_per_min: int = 30 + + # --- CORS --- + frontend_origin: str = "http://localhost:5173" + + @property + def is_production(self) -> bool: + return self.environment == "production" + + @property + def storage_root_path(self) -> Path: + return Path(self.storage_root).resolve() + + def validate_secrets(self) -> None: + """生产环境禁止携带模板密钥启动。""" + if not self.is_production: + return + for field_name in ("secret_key",): + value = getattr(self, field_name).lower() + if any(marker in value for marker in _TEMPLATE_MARKERS): + raise RuntimeError( + f"配置错误:{field_name} 仍为模板值,生产环境禁止启动。" + "请运行: python -c \"import secrets; print(secrets.token_urlsafe(48))\"" + ) + + +@lru_cache +def get_settings() -> Settings: + s = Settings() # type: ignore[call-arg] + s.validate_secrets() + return s \ No newline at end of file diff --git a/backend/app/core/db.py b/backend/app/core/db.py new file mode 100644 index 0000000..07ec242 --- /dev/null +++ b/backend/app/core/db.py @@ -0,0 +1,76 @@ +"""SQLAlchemy 2.x 同步引擎 + 会话管理(MVP: SQLite)。 + +切换 PostgreSQL 时(扩展接口 1): +1. DATABASE_URL 改为 postgresql+asyncpg://... +2. 引擎换 create_async_engine + async_sessionmaker +3. get_session 改 async generator + yield +4. 业务层 Repository 调用加 await +""" + +from collections.abc import Generator + +from sqlalchemy import create_engine +from sqlalchemy.orm import Session, sessionmaker + +from app.core.config import get_settings + +_engine = None +_session_factory: sessionmaker[Session] | None = None + + +def get_engine(): + global _engine + if _engine is None: + settings = get_settings() + _engine = create_engine( + settings.database_url, + pool_pre_ping=True, + echo=False, + # SQLite 专属:启用 WAL 模式(并发读 + 写串行化) + connect_args={"check_same_thread": False} if "sqlite" in settings.database_url else {}, + ) + # SQLite: 启用 WAL 模式与外键约束 + if "sqlite" in settings.database_url: + from sqlalchemy import event, text + + @event.listens_for(_engine, "connect") + def _set_sqlite_pragma(dbapi_conn, _): # type: ignore[no-untyped-def] + cursor = dbapi_conn.cursor() + cursor.execute("PRAGMA journal_mode=WAL") + cursor.execute("PRAGMA foreign_keys=ON") + cursor.close() + + return _engine + + +def get_session_factory() -> sessionmaker[Session]: + global _session_factory + if _session_factory is None: + _session_factory = sessionmaker( + get_engine(), + class_=Session, + expire_on_commit=False, + ) + return _session_factory + + +def get_session() -> Generator[Session, None, None]: + """FastAPI 依赖:请求级会话。""" + factory = get_session_factory() + session = factory() + try: + yield session + session.commit() + except Exception: + session.rollback() + raise + finally: + session.close() + + +def dispose_engine() -> None: + global _engine, _session_factory + if _engine is not None: + _engine.dispose() + _engine = None + _session_factory = None \ No newline at end of file diff --git a/backend/app/core/errors.py b/backend/app/core/errors.py new file mode 100644 index 0000000..3978b3b --- /dev/null +++ b/backend/app/core/errors.py @@ -0,0 +1,155 @@ +"""统一业务异常与错误响应体(需求 §37 → 统一 code+message+detail)。 + +对外格式:{"code": "...", "message": "中文文案", "detail": null} +未捕获异常兜底为 500 INTERNAL_ERROR,原始异常只进日志。 +""" + +from typing import Any + +from fastapi import Request, status +from fastapi.exceptions import RequestValidationError +from fastapi.responses import JSONResponse +from starlette.exceptions import HTTPException as StarletteHTTPException + +from app.core.logging import get_logger + +logger = get_logger(__name__) + + +class AppError(Exception): + """业务异常基类。""" + + status_code: int = status.HTTP_500_INTERNAL_SERVER_ERROR + code: str = "INTERNAL_ERROR" + message: str = "服务内部错误,请稍后重试。" + + def __init__( + self, + message: str | None = None, + *, + detail: Any = None, + code: str | None = None, + ) -> None: + self.detail = detail + if message is not None: + self.message = message + if code is not None: + self.code = code + super().__init__(self.message) + + def to_response(self) -> JSONResponse: + return JSONResponse( + status_code=self.status_code, + content={"code": self.code, "message": self.message, "detail": self.detail}, + ) + + +# --- 通用 --- + + +class NotFoundError(AppError): + status_code = status.HTTP_404_NOT_FOUND + code = "NOT_FOUND" + message = "资源不存在。" + + +class PermissionDeniedError(AppError): + status_code = status.HTTP_403_FORBIDDEN + code = "PERMISSION_DENIED" + message = "没有权限访问该资源。" + + +class AuthRequiredError(AppError): + status_code = status.HTTP_401_UNAUTHORIZED + code = "AUTH_REQUIRED" + message = "请先登录。" + + +class InvalidCredentialsError(AppError): + status_code = status.HTTP_401_UNAUTHORIZED + code = "AUTH_INVALID_CREDENTIALS" + message = "用户名或密码错误。" + + +class ConflictError(AppError): + status_code = status.HTTP_409_CONFLICT + code = "CONFLICT" + message = "资源冲突。" + + +class RateLimitedError(AppError): + status_code = status.HTTP_429_TOO_MANY_REQUESTS + code = "RATE_LIMITED" + message = "请求过于频繁,请稍后再试。" + + +# --- 上传与配额 --- + + +class StorageQuotaExceededError(AppError): + status_code = status.HTTP_413_CONTENT_TOO_LARGE + code = "STORAGE_QUOTA_EXCEEDED" + message = "存储空间不足。" + + +class FileTooLargeError(AppError): + status_code = status.HTTP_413_CONTENT_TOO_LARGE + code = "FILE_TOO_LARGE" + message = "单个文件大小超出限制。" + + +class FileTypeUnsupportedError(AppError): + status_code = status.HTTP_415_UNSUPPORTED_MEDIA_TYPE + code = "FILE_TYPE_UNSUPPORTED" + message = "不支持的文件类型。" + + +# --- 异常处理器 --- + + +async def app_error_handler(_: Request, exc: AppError) -> JSONResponse: + if exc.status_code >= 500: + logger.error("AppError %s: %s", exc.code, exc.message, exc_info=exc) + return exc.to_response() + + +async def validation_error_handler(_: Request, exc: RequestValidationError) -> JSONResponse: + return JSONResponse( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + content={ + "code": "VALIDATION_ERROR", + "message": "请求参数不正确。", + "detail": exc.errors(include_url=False, include_input=False), + }, + ) + + +_STATUS_CODE_MAP = { + status.HTTP_404_NOT_FOUND: ("NOT_FOUND", "资源不存在。"), + status.HTTP_405_METHOD_NOT_ALLOWED: ("METHOD_NOT_ALLOWED", "请求方法不被允许。"), + status.HTTP_401_UNAUTHORIZED: ("AUTH_REQUIRED", "请先登录。"), +} + + +async def http_exception_handler(_: Request, exc: StarletteHTTPException) -> JSONResponse: + """把框架层 HTTPException(如未匹配路由的 404)转成统一错误体。""" + code, message = _STATUS_CODE_MAP.get(exc.status_code, ("HTTP_ERROR", str(exc.detail))) + return JSONResponse( + status_code=exc.status_code, + content={"code": code, "message": message, "detail": None}, + ) + + +async def unhandled_error_handler(request: Request, exc: Exception) -> JSONResponse: + logger.error("Unhandled: %s %s", request.method, request.url.path, exc_info=exc) + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content={"code": "INTERNAL_ERROR", "message": "服务内部错误,请稍后重试。", "detail": None}, + ) + + +def register_exception_handlers(app: Any) -> None: + app.add_exception_handler(AppError, app_error_handler) + app.add_exception_handler(RequestValidationError, validation_error_handler) + app.add_exception_handler(StarletteHTTPException, http_exception_handler) + app.add_exception_handler(Exception, unhandled_error_handler) \ No newline at end of file diff --git a/backend/app/core/logging.py b/backend/app/core/logging.py new file mode 100644 index 0000000..8f62224 --- /dev/null +++ b/backend/app/core/logging.py @@ -0,0 +1,31 @@ +"""结构化日志配置。""" + +import logging +import sys + +_CONFIGURED = False + + +def setup_logging(level: int = logging.INFO) -> None: + global _CONFIGURED + if _CONFIGURED: + return + handler = logging.StreamHandler(sys.stdout) + handler.setFormatter( + logging.Formatter( + fmt="%(asctime)s | %(levelname)-7s | %(name)s | %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + ) + root = logging.getLogger() + root.setLevel(level) + root.handlers = [handler] + + for noisy in ("uvicorn.access", "asyncio"): + logging.getLogger(noisy).setLevel(logging.WARNING) + + _CONFIGURED = True + + +def get_logger(name: str) -> logging.Logger: + return logging.getLogger(name) \ No newline at end of file diff --git a/backend/app/core/rate_limit.py b/backend/app/core/rate_limit.py new file mode 100644 index 0000000..1ad18f0 --- /dev/null +++ b/backend/app/core/rate_limit.py @@ -0,0 +1,62 @@ +"""内存限流(MVP: 单进程 TokenBucket)。 + +未来切换 Redis: 实现 RedisRateLimiter,接口不变。 +""" + +import time +from collections import defaultdict + +from app.core.config import get_settings + + +class _Bucket: + __slots__ = ("tokens", "max_tokens", "refill_rate", "last_refill") + + def __init__(self, max_tokens: int, per_seconds: int) -> None: + self.max_tokens = max_tokens + self.refill_rate = max_tokens / per_seconds + self.tokens = float(max_tokens) + self.last_refill = time.monotonic() + + def allow(self) -> bool: + now = time.monotonic() + elapsed = now - self.last_refill + self.tokens = min(self.max_tokens, self.tokens + elapsed * self.refill_rate) + self.last_refill = now + if self.tokens >= 1.0: + self.tokens -= 1.0 + return True + return False + + +# key → Bucket +_token_buckets: dict[str, _Bucket] = defaultdict(lambda: _Bucket(_token_limit, 60)) +_ip_buckets: dict[str, _Bucket] = defaultdict(lambda: _Bucket(_ip_limit, 60)) + +# 延迟初始化 +_token_limit = 60 +_ip_limit = 30 +_initialized = False + + +def _ensure_init() -> None: + global _token_limit, _ip_limit, _initialized + if _initialized: + return + settings = get_settings() + _token_limit = settings.rate_limit_per_token_per_min + _ip_limit = settings.rate_limit_per_ip_per_min + # 重建 default dict factories + _token_buckets.default_factory = lambda: _Bucket(_token_limit, 60) # type: ignore[assignment] + _ip_buckets.default_factory = lambda: _Bucket(_ip_limit, 60) # type: ignore[assignment] + _initialized = True + + +def check_rate_limit(token_key: str | None = None, ip_key: str | None = None) -> bool: + """检查是否允许请求。返回 True 表示允许。""" + _ensure_init() + if token_key and not _token_buckets[token_key].allow(): + return False + if ip_key and not _ip_buckets[ip_key].allow(): + return False + return True \ No newline at end of file diff --git a/backend/app/core/session.py b/backend/app/core/session.py new file mode 100644 index 0000000..35ebc6f --- /dev/null +++ b/backend/app/core/session.py @@ -0,0 +1,74 @@ +"""服务端 Session 管理(MVP: 内存 dict + TTL)。 + +- Session token: secrets.token_urlsafe(32) +- Cookie: HttpOnly, SameSite=Lax, 生产环境 Secure +- 默认过期: 7 天 + +未来切换 Redis: 改 _store 为 Redis 客户端,接口不变。 +""" + +import secrets +import time +from dataclasses import dataclass + +from app.core.config import get_settings + +# 内存存储:{token: SessionEntry} +_store: dict[str, "SessionEntry"] = {} + +SESSION_COOKIE_NAME = "session_id" +SESSION_TTL_SECONDS = 7 * 24 * 3600 # 7 天 + + +@dataclass +class SessionEntry: + user_id: str + created_at: float + last_accessed: float + + +def create_session(user_id: str) -> str: + """创建 session,返回 token(写入 Cookie)。""" + _cleanup_expired() + token = secrets.token_urlsafe(32) + now = time.time() + _store[token] = SessionEntry(user_id=user_id, created_at=now, last_accessed=now) + return token + + +def get_session_user(token: str) -> str | None: + """根据 token 获取 user_id;不存在或过期返回 None。""" + entry = _store.get(token) + if entry is None: + return None + now = time.time() + if now - entry.last_accessed > SESSION_TTL_SECONDS: + _store.pop(token, None) + return None + entry.last_accessed = now + return entry.user_id + + +def delete_session(token: str) -> None: + _store.pop(token, None) + + +def _cleanup_expired() -> None: + """惰性清理过期 session。""" + now = time.time() + expired = [t for t, e in _store.items() if now - e.last_accessed > SESSION_TTL_SECONDS] + for t in expired: + _store.pop(t, None) + + +def get_cookie_params() -> dict: + """构建 Set-Cookie 参数。""" + settings = get_settings() + return { + "key": SESSION_COOKIE_NAME, + "httponly": True, + "samesite": "lax", + "secure": settings.is_production, + "max_age": SESSION_TTL_SECONDS, + "path": "/", + } \ No newline at end of file diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..10eaa8a --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,68 @@ +"""AI Knowledge Link — FastAPI 应用工厂。 + +Phase 1 只包含:配置校验、日志、异常处理器、健康检查、lifespan 资源管理。 +后续 Phase 逐步挂载:auth、knowledge-bases、documents、public /k/ 路由。 +""" + +from contextlib import asynccontextmanager +from pathlib import Path + +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from app.api.health import router as health_router +from app.core.config import get_settings +from app.core.db import dispose_engine +from app.core.errors import register_exception_handlers +from app.core.logging import get_logger, setup_logging + +logger = get_logger(__name__) + + +@asynccontextmanager +async def lifespan(app: FastAPI): + settings = get_settings() + setup_logging() + + # 确保 data 目录存在 + data_dir = settings.storage_root_path + data_dir.mkdir(parents=True, exist_ok=True) + logger.info("Starting backend (env=%s, data=%s)", settings.environment, data_dir) + + yield + + dispose_engine() + logger.info("Backend shutdown complete") + + +def create_app() -> FastAPI: + settings = get_settings() + app = FastAPI( + title="AI Knowledge Link", + version="0.1.0", + lifespan=lifespan, + docs_url="/api/docs" if not settings.is_production else None, + openapi_url="/api/openapi.json" if not settings.is_production else None, + ) + + # CORS(开发期允许 Vite dev server) + app.add_middleware( + CORSMiddleware, + allow_origins=[settings.frontend_origin], + allow_credentials=True, + allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], + allow_headers=["Content-Type", "X-Requested-With"], + ) + + register_exception_handlers(app) + + # 路由挂载 + app.include_router(health_router, prefix="/api", tags=["health"]) + # Phase 3+: app.include_router(auth_router, prefix="/api/auth", tags=["auth"]) + # Phase 4+: app.include_router(kb_router, prefix="/api/knowledge-bases", tags=["knowledge-bases"]) + # Phase 10+: app.include_router(public_router, tags=["public"]) + + return app + + +app = create_app() \ No newline at end of file diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/processors/__init__.py b/backend/app/processors/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/processors/parsers/__init__.py b/backend/app/processors/parsers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/public/__init__.py b/backend/app/public/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/repositories/__init__.py b/backend/app/repositories/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/retrieval/__init__.py b/backend/app/retrieval/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/services/__init__.py b/backend/app/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/storage/__init__.py b/backend/app/storage/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/storage/base.py b/backend/app/storage/base.py new file mode 100644 index 0000000..753f72b --- /dev/null +++ b/backend/app/storage/base.py @@ -0,0 +1,28 @@ +"""存储服务抽象(扩展接口 2:本地文件 → MinIO/S3)。""" + +from typing import Protocol, runtime_checkable + + +@runtime_checkable +class StorageService(Protocol): + """文件存储统一接口。所有业务代码通过此接口读写文件,禁止直接 open()/Path()。""" + + def save(self, key: str, data: bytes) -> str: + """保存数据,返回实际存储路径。""" + ... + + def read(self, key: str) -> bytes: + """读取数据。""" + ... + + def delete(self, key: str) -> None: + """删除文件。""" + ... + + def exists(self, key: str) -> bool: + """文件是否存在。""" + ... + + def get_size(self, key: str) -> int: + """获取文件大小(字节)。""" + ... diff --git a/backend/app/storage/local_storage.py b/backend/app/storage/local_storage.py new file mode 100644 index 0000000..7a8ced1 --- /dev/null +++ b/backend/app/storage/local_storage.py @@ -0,0 +1,67 @@ +"""本地文件系统存储实现(MVP)。""" + +from pathlib import Path + +from app.core.config import get_settings +from app.core.logging import get_logger + +logger = get_logger(__name__) + + +class LocalStorageService: + """文件存储到服务器本地 data/ 目录。 + + key 格式示例: + users/{uid}/kbs/{kb_id}/original/{rand}_{safe_name} + users/{uid}/kbs/{kb_id}/markdown/{rand}.md + + 实际路径 = storage_root / key + """ + + def __init__(self) -> None: + settings = get_settings() + self._root = settings.storage_root_path + self._root.mkdir(parents=True, exist_ok=True) + + def _resolve(self, key: str) -> Path: + """解析 key 为绝对路径,同时防路径穿越。""" + path = (self._root / key).resolve() + # 安全:确保解析后路径仍在 root 内 + if not str(path).startswith(str(self._root)): + raise ValueError(f"路径穿越检测:key={key}") + return path + + def save(self, key: str, data: bytes) -> str: + path = self._resolve(key) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(data) + logger.debug("Saved file: %s (%d bytes)", key, len(data)) + return key + + def read(self, key: str) -> bytes: + path = self._resolve(key) + return path.read_bytes() + + def delete(self, key: str) -> None: + path = self._resolve(key) + if path.exists(): + path.unlink() + logger.debug("Deleted file: %s", key) + + def exists(self, key: str) -> bool: + return self._resolve(key).exists() + + def get_size(self, key: str) -> int: + path = self._resolve(key) + return path.stat().st_size + + +# 单例 +_instance: LocalStorageService | None = None + + +def get_storage() -> LocalStorageService: + global _instance + if _instance is None: + _instance = LocalStorageService() + return _instance \ No newline at end of file diff --git a/backend/app/storage/object_keys.py b/backend/app/storage/object_keys.py new file mode 100644 index 0000000..dead1f7 --- /dev/null +++ b/backend/app/storage/object_keys.py @@ -0,0 +1,50 @@ +"""对象存储 key 生成。 + +布局:users/{user_id}/kbs/{kb_id}/original/{rand8}_{safe_filename} + users/{user_id}/kbs/{kb_id}/markdown/{rand8}.md + +规则(需求 §27 / §23): +- 不信任用户文件名:取 basename,剔除路径穿越与控制字符,截断长度。 +- 主体是内部 UUID,公共 URL 使用 token,绝不回显此 key。 +""" + +import re +import secrets +import uuid + +_SAFE_NAME_RE = re.compile(r"[^A-Za-z0-9._\-]+") +_MAX_FILENAME_LEN = 120 + + +def sanitize_filename(original: str) -> str: + name = original.replace("\\", "/").split("/")[-1] + name = _SAFE_NAME_RE.sub("_", name).strip("._") + if not name: + name = "file" + return name[:_MAX_FILENAME_LEN] + + +def original_object_key( + *, + user_id: str, + knowledge_base_id: str, + document_id: str, + original_filename: str, +) -> str: + rand = secrets.token_urlsafe(6) # ~8 字符 + safe = sanitize_filename(original_filename) + return ( + f"users/{user_id}/kbs/{knowledge_base_id}/docs/{document_id}/original/{rand}_{safe}" + ) + + +def markdown_object_key( + *, + user_id: str, + knowledge_base_id: str, + document_id: str, +) -> str: + rand = secrets.token_urlsafe(6) + return ( + f"users/{user_id}/kbs/{knowledge_base_id}/docs/{document_id}/markdown/{rand}.md" + ) \ No newline at end of file diff --git a/backend/pyproject.toml b/backend/pyproject.toml new file mode 100644 index 0000000..dcc45f2 --- /dev/null +++ b/backend/pyproject.toml @@ -0,0 +1,24 @@ +[tool.ruff] +line-length = 100 +target-version = "py312" +src = ["app", "tests"] + +[tool.ruff.lint] +select = ["E", "F", "W", "I", "N", "UP", "B", "S", "C4", "ASYNC", "RUF"] +ignore = ["S101"] # S101: tests 里允许 assert + +[tool.ruff.lint.per-file-ignores] +"tests/**" = ["S105", "S106"] +"alembic/versions/**" = ["E501"] + +[tool.mypy] +python_version = "3.12" +warn_unused_ignores = true +warn_redundant_casts = true +disallow_untyped_defs = true +check_untyped_defs = true +ignore_missing_imports = true + +[tool.pytest.ini_options] +testpaths = ["tests"] +filterwarnings = ["ignore::DeprecationWarning"] \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..aa6f2aa --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,39 @@ +# AI Knowledge Link — backend dependencies (MVP v1) +# Python 3.12+(开发机为 3.13) + +# --- Web 框架 --- +fastapi>=0.115 +uvicorn[standard]>=0.30 +pydantic>=2.7 +pydantic-settings>=2.3 +jinja2>=3.1 + +# --- 数据库 --- +sqlalchemy>=2.0.30 +alembic>=1.13 + +# --- 安全 --- +argon2-cffi>=23.1 +cryptography>=42.0 + +# --- 文档解析 --- +# MIME 嗅探:纯 Python,Windows 无需 libmagic DLL +filetype>=1.2 +# MarkItDown(MIT) +markitdown>=0.1.0 +# PDF fallback: PyMuPDF(AGPL-3.0) +# TODO: 商品化前替换为 pypdfium2(Apache-2.0/BSD),见 docs/technical-review.md R3 +PyMuPDF>=1.24.0 +# DOCX fallback +python-docx>=1.1 +# 中文分词(关键词提取) +jieba>=0.42 + +# --- Markdown 安全渲染 --- +bleach>=6.1 + +# --- 开发 / 测试 --- +pytest>=8.2 +httpx>=0.27 +ruff>=0.5 +mypy>=1.10 \ No newline at end of file diff --git a/backend/tests/__init__.py b/backend/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/test_smoke.py b/backend/tests/test_smoke.py new file mode 100644 index 0000000..d9d9c2b --- /dev/null +++ b/backend/tests/test_smoke.py @@ -0,0 +1,36 @@ +"""Phase 1 冒烟测试:应用可创建、健康检查可用、错误体格式正确。""" + +from fastapi.testclient import TestClient + +from app.main import app + + +def _client() -> TestClient: + # with 语句确保 lifespan(配置校验、data 目录创建)真实执行 + return TestClient(app) + + +def test_liveness() -> None: + with _client() as client: + resp = client.get("/api/healthz") + assert resp.status_code == 200 + assert resp.json() == {"status": "ok"} + + +def test_readiness_all_components() -> None: + with _client() as client: + resp = client.get("/api/health") + assert resp.status_code == 200 + body = resp.json() + assert body["status"] == "ok" + assert set(body["components"]) == {"database", "storage"} + assert all(c["status"] == "ok" for c in body["components"].values()) + + +def test_404_uses_unified_error_envelope() -> None: + with _client() as client: + resp = client.get("/api/nonexistent") + assert resp.status_code == 404 + body = resp.json() + assert body["code"] == "NOT_FOUND" + assert isinstance(body["message"], str) \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e95d3ed --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,45 @@ +# AI Knowledge Link — Docker Compose (生产) +# Ubuntu 服务器部署用。Windows 开发不需要 Docker。 +# +# 数据持久化:./data 挂载到 backend,SQLite + 用户文件均在其中。 +# 删除容器后数据不丢失。 +# +# 用法: +# cp .env.example .env # 修改密钥 +# mkdir -p data +# docker compose up -d --build + +services: + backend: + build: + context: ./backend + restart: unless-stopped + env_file: .env + volumes: + - ./data:/app/data + expose: + - "8000" + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/healthz')"] + interval: 10s + timeout: 5s + retries: 5 + + frontend: + build: + context: ./frontend + restart: unless-stopped + expose: + - "80" + + nginx: + image: nginx:1.27-alpine + restart: unless-stopped + ports: + - "80:80" + volumes: + - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro + depends_on: + backend: + condition: service_healthy + - frontend \ No newline at end of file diff --git a/docs/requirements.md b/docs/requirements.md new file mode 100644 index 0000000..07eaa16 --- /dev/null +++ b/docs/requirements.md @@ -0,0 +1,342 @@ +# AI Knowledge Link — 产品需求文档(MVP v1) + +> 状态标注说明:✅ 已完成 | 🔄 部分完成 | ⬜ 未开始(对应 Phase N) | 📌 持续约束(贯穿全程) +> +> **版本说明**:本文档为当前有效需求(MVP 版)。早前一版需求(PostgreSQL + Redis + Celery + MinIO 全功能栈)已被本 MVP 版**取代**,核心差异:SQLite 替代 PostgreSQL、本地文件系统替代 MinIO、同步解析替代 Celery、内存限流替代 Redis、FTS5 替代 jieba+tsvector。原版中的商业许可审查结论仍有效(PDF 解析商品化前需将 PyMuPDF 替换为 pypdfium2,见 `technical-review.md` R3)。 +> +> 关联文档:[技术审查报告](technical-review.md) | [CLAUDE.md](../CLAUDE.md) + +## Phase 进度总览(对应 §56 开发顺序) + +| Phase | 内容 | 状态 | +|---|---|---| +| 1 | 项目初始化 | ✅ 完成 | +| 2 | 数据库和 Alembic | ⬜ | +| 3 | 用户注册登录 | ⬜ | +| 4 | 知识库 CRUD | ⬜ | +| 5 | 本地 StorageService | ✅ 完成(提前实现于 Phase 1) | +| 6 | 文档上传 | ⬜ | +| 7 | 文档解析和 Markdown | ⬜ | +| 8 | 文档管理 | ⬜ | +| 9 | Secret URL | ⬜ | +| 10 | AI 公共页面 | ⬜ | +| 11 | Markdown/TXT/JSON 输出 | ⬜ | +| 12 | 搜索 | ⬜ | +| 13 | 安全 | ⬜ | +| 14 | 前端完善 | ⬜ | +| 15 | 测试 | ⬜ | +| 16 | Docker | 🔄 compose/Dockerfile 已写(Phase 1),待 Docker 环境验证 | +| 17 | Ubuntu 部署文档 | ⬜ | + +--- + +## 一、产品核心目标 ⬜(产品定位,随各 Phase 逐步实现) + +这个产品不是普通网盘。 + +核心功能是: + +用户上传自己的 Word、PDF 等文档 → 系统自动解析文档 → 转换成 Markdown → 建立个人知识库 → 系统生成一个 AI 专属访问链接 → 用户复制这个链接 → 发送给第三方 AI → AI 访问知识库 → AI 查看知识库中的文档列表 → 根据用户的描述选择一个或多个相关文档 → 访问具体文档内容 → 利用这些资料回答用户问题 + +例如:用户上传 公司介绍.docx、产品说明.pdf、技术架构.pdf、项目需求.docx,系统生成 `https://example.com/k/xxxxxxxx`。用户把这个链接发送给 AI:"请访问这个知识库,阅读产品A相关的技术架构和产品说明,然后告诉我产品A有哪些技术风险。" AI 访问 /k/xxxxxxxx,看到文档列表,然后根据用户要求访问相关文档。 + +## 二、第一阶段必须保持简单 📌 持续约束(Phase 1 已遵循) + +这是一个 MVP。第一版禁止过度工程化。 + +第一版只使用: + +- 前端:Vue 3、TypeScript、Vite、Element Plus、Pinia、Axios +- 后端:Python 3.12+、FastAPI、SQLAlchemy、Pydantic、Alembic +- 数据库:SQLite +- 文件存储:服务器本地文件系统 +- 文档解析:MarkItDown、PyMuPDF、python-docx +- 部署:Docker Compose,可以使用 Nginx + +第一版禁止强制依赖:PostgreSQL、Redis、Celery、MinIO、Qdrant、Elasticsearch、Kafka。不要因为未来可能商品化而提前加入这些组件。 + +## 三、开发环境 ✅ 完成(Phase 1 验证通过) + +本地开发环境:Windows。本地没有 Docker。因此 Windows 开发必须可以直接运行。 + +- 前端:`npm install` → `npm run dev` ✅ +- 后端:Python venv → `pip install` → `uvicorn` ✅ +- 本地开发不要求 Docker ✅ +- Docker 主要用于 Ubuntu 云服务器 + +生产环境:Ubuntu、Docker、Docker Compose。 + +## 四、第一版系统架构 ⬜(架构已定,实现随 Phase 推进) + +采用: + +- Windows 开发:Vue 3 + FastAPI + SQLite + 本地文件 +- 生产:Ubuntu + Docker(frontend / backend / nginx) + +数据目录: + +``` +/data +├── app.db +└── users/{user_id}/knowledge_bases/{knowledge_base_id}/ + ├── original/ + └── markdown/ +``` + +SQLite 保存元数据。文件系统保存:Word、PDF、Markdown。 + +## 五、不要把文件塞进 SQLite 📌 持续约束(存储层设计已按此实现) + +禁止把原始 PDF、Word 二进制内容存进 SQLite。SQLite 只保存:用户信息、知识库信息、文档元数据、文件路径、Markdown 路径、文档状态、Token hash、时间、分类、描述、关键词、搜索相关数据。原始文件与 Markdown 均存服务器文件系统。 + +## 六、数据库访问必须抽象 ⬜ Phase 2(Phase 1 已建 db.py 引擎层) + +不能在业务代码中到处直接调用 sqlite3。必须使用 SQLAlchemy,并通过 Repository / Service 分层(UserRepository、KnowledgeBaseRepository、DocumentRepository)。业务逻辑不能依赖 SQLite 具体实现。数据库配置通过 `DATABASE_URL`(如 `sqlite:///./data/app.db`),未来可以切换 PostgreSQL 而尽量不修改业务层。 + +## 七、未来扩展接口 1:SQLite → PostgreSQL 📌 持续约束(设计规则已在 db.py 落地) + +第一版使用 SQLite,但设计 DatabaseRepository、SQLAlchemy models、Alembic migrations。不要使用 SQLite 特有 SQL 作为核心业务逻辑;避免 PRAGMA(WAL 设置除外)、SQLite 专属函数、SQLite 专属数据类型。业务代码尽可能使用标准 SQLAlchemy。未来切换只需更换连接配置并处理迁移。 + +## 八、未来扩展接口 2:本地文件 → MinIO/S3 ✅ 完成(Phase 1 提前实现) + +已实现 `StorageService` 协议(`storage/base.py`:save/read/delete/exists/get_size)与 `LocalStorageService`(`storage/local_storage.py`)。业务代码不能直接 `open("/data/xxx")`,所有文件读写必须经过 StorageService。未来可增加 MinioStorageService / S3StorageService。 + +## 九、未来扩展接口 3:同步解析 → 异步任务 ⬜ Phase 7(DocumentProcessor 协议) + +第一版:上传文件 → FastAPI → 同步解析 → 保存 Markdown → 返回结果。必须抽象 `DocumentProcessor`(如 `process_document()`):第一版 `LocalDocumentProcessor`,未来可增加 `CeleryDocumentProcessor`。业务代码不要把文档解析逻辑写进 API endpoint。 + +## 十、未来扩展接口 4:关键词搜索 → RAG ⬜ Phase 12(Retriever 协议) + +第一版不使用向量数据库。实现 `Retriever` 统一接口(`search(query, knowledge_base_id)`),第一版 `KeywordRetriever`(SQLite FTS 或简单关键词搜索)。未来可加 VectorRetriever / HybridRetriever / QdrantRetriever、Embedding、Reranker。第一版不安装这些依赖。 + +## 十一、未来扩展接口 5:个人用户 → 团队/企业 📌 持续约束(数据库设计时遵守) + +第一版:一个用户拥有多个知识库(User → KnowledgeBase → Documents)。但数据库设计必须避免以后无法扩展。未来:Organization → Members → KnowledgeBases → Documents。第一版不实现团队,但不要把 user_id 硬编码成未来无法扩展的权限结构。 + +## 十二、未来扩展接口 6:AI 网页访问 → API/MCP 📌 持续约束(Phase 10 落地) + +第一版:AI 通过 Secret URL 访问(GET /k/{token}),这是产品核心功能。未来可增加 API Key、REST API、MCP Server、OpenAPI、AI Agent 接口。第一版不实现 MCP,但公共知识库服务层必须独立:HTML、Markdown、TXT、JSON 都调用同一个 `KnowledgeBasePublicService`(Controller → Service → Repository → Database),不要每种格式各写一套查询逻辑。 + +## 十三、用户系统 ⬜ Phase 3 + +实现注册、登录、退出登录。 + +User:id、username、email、password_hash、status、storage_quota、storage_used、created_at、updated_at。密码必须安全哈希,优先 Argon2id;禁止明文密码、MD5、SHA1。 + +## 十四、存储限制 🔄 部分完成(config.py 已有配额配置;Plan 模型 Phase 2 落地) + +默认免费用户 100MB,单文件 20MB。不要硬编码,放到配置或 Plan 模型。第一版 Free Plan(100MB / 20MB-per-file),未来 Basic 1GB、Pro 5GB、Enterprise 自定义。第一版不实现支付。 + +## 十五、知识库 ⬜ Phase 4 + +KnowledgeBase:id、user_id、name、description、enabled、secret_token_hash、created_at、updated_at。一个用户可以创建多个知识库(公司知识库、项目A、项目B、个人AI资料)。 + +## 十六、AI 专属链接 ⬜ Phase 9 + +每个知识库生成一个 Secret Token(`https://example.com/k/7fA92xKpQ8m...`)。必须使用密码学安全随机数(`secrets.token_urlsafe()` 或同等方案);禁止自增 ID、时间戳、用户名、UUID 短截断。数据库保存 token_hash 而不是明文 Token。用户复制的完整 URL 只在生成时提供给用户。 + +## 十七、AI 链接安全模型 ⬜ Phase 9/13 + +公共 AI 访问不需要登录(第三方 AI 无法使用用户后台登录状态),Secret URL 本身就是访问凭证,相当于 Bearer Credential——任何获得 URL 的人理论上都可以读取知识库,必须在产品中明确这一点。提供启用/禁用链接、重新生成链接;重新生成后旧链接立即失效。 + +## 十八、AI 知识库首页 ⬜ Phase 10 + +`GET /k/{token}` 必须返回服务端生成的 HTML,不能依赖 Vue / JavaScript(第三方 AI 可能不执行 JS),页面必须首屏直接包含核心信息。页面结构:title=知识库名称、``、知识库名称、知识库描述、Documents 列表(每篇含标题、描述、类型、更新时间、URL)。 + +## 十九、多格式输出 ⬜ Phase 11 + +- `GET /k/{token}` → HTML +- `GET /k/{token}.md` → Markdown +- `GET /k/{token}.txt` → 纯文本 +- `GET /k/{token}.json` → JSON + +JSON 只能返回必要的信息,禁止:用户密码、token hash、内部数据库 ID、服务器路径、MinIO 地址、内部配置。 + +## 二十、单文档访问 ⬜ Phase 10/11 + +- `GET /k/{token}/doc/{document_token}` → HTML +- `GET /k/{token}/doc/{document_token}.md` → Markdown +- `GET /k/{token}/doc/{document_token}.txt` → TXT + +文档页面:标题、描述、文件类型、关键词、更新时间、Markdown 正文。默认不直接提供原始文件下载。 + +## 二十一、文档 Token ⬜ Phase 9 + +不要使用 /doc/1 这种容易猜测的 URL。每个文档生成随机 document_token,数据库保存 hash。例如 `/k/knowledge-token/doc/document-token`,两个 Token 都必须随机。 + +## 二十二、文档上传 ⬜ Phase 6 + +支持 .docx、.pdf;暂不支持 .doc、.xls、.xlsx、.ppt、.pptx,但代码架构预留扩展。 + +上传流程:POST /api/documents/upload → 检查登录 → 检查知识库权限 → 检查文件大小 → 检查扩展名 → 检查 MIME → 计算 SHA256 → 检查存储额度 → 保存原始文件 → 解析 → 转换 Markdown → 提取标题 → 生成摘要 → 提取关键词 → 保存 Markdown → 更新 Document → 返回结果。 + +## 二十三、文件目录 📌 持续约束(key 生成已实现于 storage/object_keys.py) + +推荐布局: + +``` +data/ +├── app.db +└── users/{user_id}/knowledge_bases/{knowledge_base_id}/ + ├── original/ (random-name.docx / random-name.pdf) + └── markdown/ (random-name.md) +``` + +不要使用用户原始文件名直接作为物理存储路径(如 `../../evil.pdf` 必须安全处理)。 + +## 二十四、文档解析 ⬜ Phase 7 + +优先 MarkItDown;PDF 用 PyMuPDF 作为辅助/fallback;Word 用 python-docx 作为 fallback。Markdown 需尽可能保留:标题、段落、列表、表格、粗体、斜体、代码块、引用。解析失败时 Document.status = FAILED,不向用户显示 Traceback / 服务器路径 / Python 异常堆栈,只显示友好错误("文档解析失败,请检查文件是否损坏或格式是否受支持。"),服务器日志记录详细错误。 + +## 二十五、扫描 PDF ⬜ Phase 7 + +第一版不强制 OCR。如果 PDF 没有文本层,提示"该 PDF 可能是扫描件,当前版本暂不支持 OCR。"未来预留 OCRProcessor(PaddleOCR 等),第一版不加入 OCR。 + +## 二十六、文档状态 ⬜ Phase 2(状态模型)/ Phase 7(流转) + +PENDING、PROCESSING、READY、FAILED、DELETED。虽然第一版同步处理,但状态模型必须保留,未来异步任务可以直接使用。 + +## 二十七、知识库搜索 ⬜ Phase 12 + +`GET /k/{token}/search?q=关键词` 返回:文档、标题、匹配摘要、URL。第一版用 SQLite FTS 或简单关键词搜索,实现 Retriever 接口(第一版 KeywordRetriever)。不引入 Qdrant、Embedding。 + +## 二十八、AI 选择多个文档 ⬜ Phase 10(产品最重要功能之一) + +知识库首页不能把所有文档正文全部输出。首页应提供:文档标题、文档描述、关键词、文档类型、文档 URL。例如"产品A技术架构(关键词:产品A、架构、服务器、数据库、Redis)→ /k/xxxx/doc/yyyy"。这样 AI 可以根据用户描述选择文档——用户说"请阅读产品A的架构和数据库设计",AI 应能发现并访问"产品A技术架构"和"产品A数据库设计"两个文档。 + +## 二十九、AI 兼容性 ⬜ Phase 10/11(README 声明 Phase 17) + +不能假设第三方 AI 一定支持 JavaScript、Cookie、登录、API 调用、自动跟随所有链接、搜索接口。AI 页面必须:服务端渲染、HTML 标准、正文直接返回、链接使用标准 ``、不依赖 JS、不使用复杂 SPA、内容结构清晰。同时提供 HTML / Markdown / TXT / JSON,尽可能提高 DeepSeek、豆包、Kimi、ChatGPT 及其他支持网页访问的 AI 的兼容性。README 必须明确:第三方 AI 是否支持访问外部 URL、跟随链接和读取页面,由第三方 AI 自身能力决定,本系统不能保证所有 AI 都一定会访问后续文档。 + +## 三十、前端 🔄 骨架完成(Phase 1);完整 UI ⬜ Phase 14 + +Vue 3、TypeScript、Vite、Element Plus、Pinia。UI 要求:现代、简洁、SaaS 风格、中文、不要花哨、不要大量动画。主要页面:/login、/register、/dashboard、/knowledge-bases、/knowledge-bases/:id、/settings。 + +## 三十一、登录页面 🔄 占位页已有(Phase 1);功能 ⬜ Phase 3/14 + +登录:用户名/邮箱、密码、登录。注册:用户名、邮箱、密码、确认密码。基本校验。 + +## 三十二、Dashboard 🔄 占位页已有(Phase 1);功能 ⬜ Phase 14 + +显示:知识库数量、文档数量、已使用空间/总空间(如 32MB / 100MB)、最近知识库、最近上传、解析失败文件。 + +## 三十三、知识库管理 ⬜ Phase 4(接口)/ Phase 14(UI) + +显示:知识库名称、描述、文档数量、空间、创建时间、AI 链接。操作:管理、复制 AI 链接、预览、启用/禁用、重新生成链接、删除。 + +## 三十四、文档管理 ⬜ Phase 6/8(接口)/ Phase 14(UI) + +支持拖拽上传、多文件上传。显示:文件名、类型、大小、状态、更新时间。操作:预览、重新解析、删除。 + +## 三十五、文档编辑 ⬜ Phase 8 + +用户可以修改:标题、描述、关键词、分类。Markdown 正文默认由系统生成。第一版可提供 Markdown 预览;是否允许直接编辑 Markdown 不是最高优先级,时间有限可暂时只读。 + +## 三十六、权限 ⬜ Phase 13(编码时全程遵守) + +必须防止 IDOR:用户A访问 /api/knowledge-bases/10,不能通过修改 10 → 11 读取用户B知识库。所有后台接口必须验证:当前登录用户 → 资源 owner → 允许操作。不能只验证资源 ID 存在。 + +## 三十七、公共 AI 链接权限 ⬜ Phase 9 + +公共 AI URL 不要求登录,但必须验证 Secret Token。enabled = false 返回 404。重新生成 Token 后旧 Token 立即失效。 + +## 三十八、安全 ⬜ Phase 13(编码时全程遵守) + +考虑:SQL 注入、XSS、CSRF、路径穿越、恶意文件、超大文件、Zip Bomb、暴力破解、Token 猜测、IDOR、资源耗尽。上传文件:限制大小、限制扩展名、限制 MIME、随机物理文件名。Markdown 输出必须防存储型 XSS;用户输入的标题、描述、关键词不能直接插入 HTML;Markdown 渲染需要安全处理。 + +## 三十九、限流 ✅ RateLimiter 抽象完成(Phase 1);接入公共 URL ⬜ Phase 13 + +第一版不需要 Redis,可以使用内存限流(如单 IP 每分钟一定次数),保护公共 AI URL。注意内存限流只适合单实例 MVP,代码中抽象 RateLimiter,未来可实现 RedisRateLimiter。(已实现 `core/rate_limit.py`:内存 TokenBucket + 配置化阈值。) + +## 四十、访问日志 ⬜ Phase 2(模型)/ Phase 10(记录) + +第一版可简单记录:knowledge_base_id、document_id、timestamp、user_agent、请求类型。不要默认长期保存完整 IP。后台可显示访问次数、最近访问时间。不要声称可以准确判断访问者是不是 AI,使用"外部访问"而不是"AI 访问"。 + +## 四十一、数据库模型 ⬜ Phase 2 + +至少:User、Plan、KnowledgeBase、Document、DocumentCategory、AccessLog。 + +Document:id、knowledge_base_id、user_id、document_token_hash、original_filename、storage_path、markdown_path、file_size、mime_type、sha256、title、description、keywords、category_id、status、created_at、updated_at。 + +KnowledgeBase:id、user_id、name、description、secret_token_hash、enabled、created_at、updated_at。 + +## 四十二、数据库迁移 ⬜ Phase 2 + +使用 Alembic,即使 SQLite 也必须使用迁移。不要手工修改生产数据库。README 提供:初始化迁移、升级迁移、回滚迁移。 + +## 四十三、API 设计 🔄 分 Phase 实现 + +认证:POST /api/auth/register、POST /api/auth/login、POST /api/auth/logout、GET /api/me +知识库:GET/POST /api/knowledge-bases、GET/PUT/DELETE /api/knowledge-bases/{id} +知识库链接:POST /api/knowledge-bases/{id}/regenerate-token、/enable、/disable +文档:GET /api/knowledge-bases/{id}/documents、POST /api/knowledge-bases/{id}/documents、GET/PUT/DELETE /api/documents/{id}、POST /api/documents/{id}/reprocess +存储:GET /api/storage +公共:GET /k/{token}(+.md/.txt/.json)、GET /k/{token}/search?q=、GET /k/{token}/doc/{document_token}(+.md/.txt) + +## 四十四、统一 Service 层 🔄 分 Phase 实现(Phase 1 已落 StorageService) + +不要把业务逻辑全部写进 FastAPI 路由。至少:AuthService、UserService、KnowledgeBaseService、DocumentService、DocumentProcessor、StorageService、KnowledgeBasePublicService、Retriever、RateLimiter。 + +## 四十五、公共 AI 访问统一 Service ⬜ Phase 10 + +HTML / Markdown / TXT / JSON 各 Renderer 都必须:KnowledgeBasePublicService → 返回知识库数据 → 各格式 Renderer。所有格式必须使用同一套数据来源。 + +## 四十六、Docker 🔄 compose/Dockerfile 已写(Phase 1);验证 ⬜ Phase 16 + +虽然 Windows 开发不需要 Docker,但项目必须提供生产 Docker 配置。docker-compose.yml:frontend、backend、nginx。SQLite 和文件:宿主机 ./data 挂载到 /app/data。重要:数据库和用户文件不能写进 Docker 镜像;删除容器以后数据仍然存在。 + +## 四十七、生产目录 ⬜ Phase 17 + +服务器 /opt/ai-knowledge-link/:docker-compose.yml、.env、data/(app.db + users/)、nginx/、project/(或合理目录)。data 必须独立于代码。 + +## 四十八、备份 ⬜ Phase 17 + +第一版使用 SQLite,必须提供备份脚本:app.db + users/。建议每日备份,保留最近 7 天、最近 4 周。备份不能影响正在运行的服务。README 说明如何备份、如何恢复。 + +## 四十九、Git ✅ 完成(Phase 1) + +已提供 .gitignore(禁止提交:.env、data/、*.db、上传文件、虚拟环境、node_modules)与 .env.example。 + +## 五十、Windows 开发 ✅ 完成(Phase 1,README 已含完整步骤;实测通过) + +README 必须详细说明:安装 Python、创建 venv(`python -m venv .venv`、Windows 激活 `.venv\Scripts\activate`)、安装依赖(`pip install -r requirements.txt`)、启动 FastAPI(`uvicorn app.main:app --reload`);安装 Node、`npm install`、`npm run dev`。 + +## 五十一、生产部署 ⬜ Phase 17 + +README 必须详细说明:Ubuntu 安装 Docker、克隆 Git 仓库、配置 .env、创建 data 目录、启动(`docker compose up -d --build`)、查看(`docker compose ps`)、日志(`docker compose logs -f`)、更新(`git pull && docker compose up -d --build`)。数据不能因为更新丢失。 + +## 五十二、测试 🔄 冒烟测试 3 条通过(Phase 1);完整覆盖 ⬜ Phase 15 + +必须编写测试,至少覆盖:注册、登录、错误密码、创建知识库、删除知识库、用户隔离、上传 DOCX、上传 PDF、文件大小限制、存储空间限制、文档解析、Markdown 生成、Token 生成、Token 禁用、Token 重新生成、旧 Token 失效、公共知识库 HTML、公共 Markdown、公共 TXT、公共 JSON、单文档访问、文档 Token、搜索、XSS 防护、路径穿越、IDOR。 + +## 五十三、项目目录 ✅ 完成(Phase 1) + +``` +project/ +├── backend/(app/{api,core,models,schemas,repositories,services,processors,storage,retrieval,public}、alembic/、tests/、requirements.txt、Dockerfile) +├── frontend/(src/{api,components,layouts,views,stores,router,types}、package.json、Dockerfile) +├── nginx/nginx.conf +├── data/.gitkeep +├── docker-compose.yml +├── .env.example +├── .gitignore +└── README.md +``` + +## 五十四、代码原则 📌 持续约束(Phase 1 已遵循) + +不要:一个 main.py 写完所有东西;所有数据库查询写在路由;业务代码直接 open 文件;硬编码路径;硬编码 100MB;硬编码 Secret Token;把密码明文保存;把用户 A/B 数据混在一起;使用 Vue 渲染 AI 公共页面。 + +## 五十五、产品第一阶段验收标准 ⬜ 最终验收(Phase 15 后) + +1. 用户注册。2. 用户登录。3. 创建"公司知识库"。4. 上传 公司介绍.docx、产品说明.pdf、技术架构.pdf。5. 系统自动转换 Markdown。6. 后台显示 3 个文档。7. 用户复制 AI 专属链接。8. 退出登录。9. 未登录状态打开链接仍能访问知识库。10. 页面显示知识库名称、描述、文档列表、文档描述、文档 URL。11. 访问其中一个文档能看到 Markdown 正文。12. 访问 .md 得到 Markdown。13. 访问 .txt 得到纯文本。14. 访问 .json 得到机器可读数据。15. /search?q=产品A 能找到相关文档。16. 重新生成 AI 链接后旧链接立即失效。17. 禁用 AI 链接后公共访问失败。18. 用户A无法访问用户B后台数据。19. 用户A无法通过修改 ID 读取用户B资源。20. Docker 部署后删除容器并重新创建,用户数据不能丢失。 + +## 五十六、第一阶段开发顺序 ✅ Phase 1 完成,按序推进中 + +Phase 1 项目初始化 → Phase 2 数据库和 Alembic → Phase 3 用户注册登录 → Phase 4 知识库 CRUD → Phase 5 本地 StorageService → Phase 6 文档上传 → Phase 7 文档解析和 Markdown → Phase 8 文档管理 → Phase 9 Secret URL → Phase 10 AI 公共页面 → Phase 11 Markdown/TXT/JSON 输出 → Phase 12 搜索 → Phase 13 安全 → Phase 14 前端完善 → Phase 15 测试 → Phase 16 Docker → Phase 17 Ubuntu 部署文档。 + +每完成一个 Phase:1. 检查代码 2. 运行测试 3. 修复错误 4. 确认没有破坏之前功能 5. 再进入下一阶段。 + +## 五十七、当前任务 ✅ 完成(技术审查报告见 technical-review.md) + +第一步不要立即创建大量业务代码,先对整个需求进行技术审查,输出:最终架构、数据库 ER 结构、API 结构、项目目录、六个未来扩展接口如何设计、安全模型、文档处理流程、AI 访问流程、Windows 开发流程、Docker 生产流程、发现的风险、建议修改的地方。确认后从 Phase 1 开始,不要跳过架构审查。 \ No newline at end of file diff --git a/docs/technical-review.md b/docs/technical-review.md new file mode 100644 index 0000000..224766c --- /dev/null +++ b/docs/technical-review.md @@ -0,0 +1,536 @@ +# AI Knowledge Link — 技术审查报告(MVP v1) + +> 审查范围:需求文档全文(57 节)。 +> 核心原则:**MVP 保持简单,6 个扩展接口预留但不实现,SQLite + 本地文件 + 同步解析**。 +> 结论:架构成立,核心需求可实现。发现 1 个必须修改的设计、若干建议补充。 + +--- + +## 1. 最终架构 + +### 1.1 架构总览 + +``` +开发环境(Windows) 生产环境(Ubuntu Docker) +───────────────── ───────────────────── +Vue3 dev server ─┐ nginx :80 + ├─► FastAPI :8000 ├── /api/** → backend:8000 +uvicorn --reload ─┘ ├── /k/** → backend:8000 + └── / → frontend:80 + +FastAPI Docker Compose: + ├── /api/** 管理端(需登录) ├── frontend (Vue build → nginx serve) + ├── /k/** 公共AI页(SSR, 零JS) ├── backend (uvicorn) + └── SQLite + 本地文件系统 └── nginx + +/data/ /data/ (宿主机 volume) + ├── app.db ├── app.db + └── users/{uid}/kbs/{kb}/... └── users/{uid}/kbs/{kb}/... + ├── original/ ├── original/ + └── markdown/ └── markdown/ +``` + +### 1.2 分层 + +``` +api/ → 路由层:参数校验 + 鉴权 + 调 Service + 组装响应 +services/ → 业务逻辑:配额、Token、编排 +repositories/→ 数据访问:隔离 SQLAlchemy 细节 +processors/ → 文档解析:MarkItDown → PyMuPDF/python-docx fallback +storage/ → 存储抽象:LocalStorageService(未来 MinIO/S3) +retrieval/ → 检索抽象:KeywordRetriever(未来 Vector/Hybrid) +public/ → AI 公共页面渲染:Jinja2 SSR + md/txt/json 生成器 +``` + +### 1.3 关键技术决策 + +| 决策点 | 选择 | 理由 | +|---|---|---| +| 数据库 | **SQLite** + SQLAlchemy + Alembic | MVP;DATABASE_URL 可切换 PostgreSQL(§7 扩展接口) | +| 文件存储 | **本地文件系统** + StorageService 抽象 | MVP;不引入 MinIO(§8 扩展接口) | +| 文档解析 | **同步** + DocumentProcessor 抽象 | MVP 单用户低并发;未来 Celery(§9 扩展接口) | +| 检索 | **SQLite FTS5** + Retriever 抽象 | 中文 FTS5 支持 better than LIKE;未来 Qdrant(§10 扩展接口) | +| 鉴权 | **服务端 Session**(签发 opaque token 存 SQLite,Cookie 传递) | 同源 SPA;比 JWT 简单且可即时吊销;SQLite 即 session store | +| 密码哈希 | **Argon2id**(argon2-cffi) | 需求指定 | +| 限流 | **内存 TokenBucket**(单进程够用) + RateLimiter 抽象 | 无 Redis 依赖;未来 RedisRateLimiter(§39) | +| 公共页渲染 | **Jinja2 SSR**,零 JS 零外链 | AI 兼容性第一原则 | +| Token 存储 | **SHA-256 hash**(DB)+ **Fernet 加密原文**(可解密,SECRET_KEY 派生) | 满足"后台显示完整链接"需求;DB 泄漏 ≠ 链接泄漏(需 + SECRET_KEY) | +| 后缀路由 | 显式注册 `/k/{token}.md` / `.txt` / `.json` **先于** `/k/{token}` | FastAPI path param 默认匹配 `[^/]+`,`{token}` 会吞掉 `.md`(§19) | +| Python 版本 | 3.12+ 编写;本机 3.13.9 运行 | FastAPI / SQLAlchemy 2 / Pydantic v2 均已支持 | +| PyMuPDF 许可 | **MVP 阶段可接受**(AGPL-3.0);商品化前必须替换为 pypdfium2 | 需求 §24 明确指定 PyMuPDF;AGPL 对**未分发**的 SaaS 后端(仅服务端运行)在多数法域下合规,但商品化闭源分发时需替换。MVP 阶段标记 TODO 并在 requirements 中注释 | + +--- + +## 2. 数据库 ER 设计 + +### 2.1 实体关系 + +``` +plans 1 ──── N users 1 ──── N knowledge_bases 1 ──── N documents 1 ──── 1 document_categories + │ │ + │ └ 1 ──── N access_logs + └ 1 ──── N (配额归集) +``` + +### 2.2 表定义 + +**plans** +| 列 | 类型 | 说明 | +|---|---|---| +| id | integer PK | | +| code | text unique | `free` / `basic` / `pro` | +| name | text | 套餐名 | +| storage_quota | integer | 字节;free = 104857600 (100MB) | +| max_file_size | integer | 字节;free = 20971520 (20MB) | +| is_active | boolean | | + +**users** +| 列 | 类型 | 说明 | +|---|---|---| +| id | text PK (UUID4 hex) | | +| username | text unique not null | | +| email | text unique not null | | +| password_hash | text not null | Argon2id | +| status | text | `active` / `disabled` | +| plan_id | integer FK → plans | 默认 free | +| storage_used | integer default 0 | 字节计数器 | +| created_at / updated_at | text (ISO8601) | | + +**knowledge_bases** +| 列 | 类型 | 说明 | +|---|---|---| +| id | text PK (UUID4 hex) | | +| user_id | text FK → users not null | 隔离边界 | +| name | text not null | | +| description | text | | +| enabled | boolean default true | 链接开关 | +| token_hash | text unique not null | SHA-256(secret_token) hex | +| token_encrypted | text | Fernet 加密原文(可解密供后台显示) | +| token_hint | text | token 末 8 位明文,供后台识别 | +| created_at / updated_at | text | | + +> SQLite 不支持 citext / timestamptz / uuid 原生类型,统一用 text + 应用层校验。 +> 迁移到 PostgreSQL 时,Alembic 迁移负责类型映射,业务层无感。 + +**document_categories** +| 列 | 类型 | +|---|---| +| id text PK, knowledge_base_id text FK, name text not null, sort_order integer, created_at text | + +**documents** +| 列 | 类型 | 说明 | +|---|---|---| +| id | text PK (UUID4 hex) | | +| knowledge_base_id | text FK | | +| user_id | text FK | 冗余存,便于隔离校验 + 配额统计 | +| category_id | text FK nullable | | +| original_filename | text | 仅展示用,不用于物理路径 | +| storage_path | text | 相对于 data/ 的物理路径,不外泄 | +| markdown_path | text | 相对于 data/ 的 Markdown 文件路径 | +| file_size | integer | 字节 | +| mime_type | text | | +| file_ext | text | `.docx` / `.pdf` | +| sha256 | text | 去重与完整性 | +| doc_token_hash | text unique | 单文档公共 URL 凭证 | +| doc_token_encrypted | text | Fernet 加密 | +| doc_token_hint | text | | +| title | text | 用户可改,默认从 Markdown H1 提取 | +| description | text | 用户可改,默认自动生成摘要 | +| keywords | text | 逗号分隔,jieba 提取 + 用户可改 | +| content_summary | text | 抽取式摘要 ~200 字 | +| status | text | PENDING / PROCESSING / READY / FAILED / DELETED | +| error_code | text | 安全错误码(如 `SCANNED_PDF_NO_TEXT_LAYER`) | +| created_at / updated_at | text | | + +**access_logs** +| 列 | 类型 | +|---|---| +| id integer PK AUTOINCREMENT, knowledge_base_id text FK, document_id text nullable FK, path text, accessed_at text, user_agent text, request_type text | + +> SQLite FTS5 虚拟表(documents_content)单独建,与 documents 通过 id 关联。 + +### 2.3 索引 + +- `knowledge_bases(token_hash)` unique +- `documents(doc_token_hash)` unique +- `documents(knowledge_base_id, status)` +- `documents(user_id, status)` +- `users(username)` / `users(email)` unique + +--- + +## 3. API 设计 + +### 3.1 管理端(/api,需登录) + +``` +POST /api/auth/register {username, email, password} → 201 +POST /api/auth/login {username|email, password} → Set-Cookie + 200 +POST /api/auth/logout 清除 Cookie +GET /api/me 用户信息 + 套餐 +PATCH /api/me 改密码 + +GET /api/knowledge-bases 分页列表 +POST /api/knowledge-bases {name, description} → 201(返回完整 AI URL,仅此一次) +GET /api/knowledge-bases/{id} 详情 +PUT /api/knowledge-bases/{id} 改名/改描述 +DELETE /api/knowledge-bases/{id} 软删 + 异步清理文件 + +POST /api/knowledge-bases/{id}/regenerate-token 旧链立即失效 +POST /api/knowledge-bases/{id}/enable +POST /api/knowledge-bases/{id}/disable + +GET /api/knowledge-bases/{id}/documents 文档列表 +POST /api/knowledge-bases/{id}/documents/upload multipart 上传 +GET /api/documents/{id} 详情 + markdown 预览 +PUT /api/documents/{id} 改 title/description/keywords/category +DELETE /api/documents/{id} 软删 + 清理文件 +POST /api/documents/{id}/reprocess 重新解析 + +GET /api/knowledge-bases/{id}/categories 分类 CRUD +GET /api/storage {storage_used, storage_quota} +``` + +统一错误体:`{"code": "STORAGE_QUOTA_EXCEEDED", "message": "存储空间不足", "detail": null}` + +### 3.2 公共 AI 端(/k,无登录,限流) + +``` +GET /k/{token} HTML 入口页(Jinja2 SSR) +GET /k/{token}.md Markdown 入口 +GET /k/{token}.txt 纯文本入口 +GET /k/{token}.json JSON 目录(白名单字段) + +GET /k/{token}/search?q=&page= HTML(默认)/ .json + +GET /k/{token}/doc/{doc_token} 文档 HTML 页 +GET /k/{token}/doc/{doc_token}.md 原始 Markdown +GET /k/{token}/doc/{doc_token}.txt 纯文本 +``` + +公共端硬性规则: +1. token 不存在 / enabled=false / 软删 → **一律 404** +2. `` +3. `` +4. JSON 输出白名单:`name, description, documents[{title, file_type, description, summary, keywords, updated_at, url}]` +5. 限流:内存 TokenBucket,单 IP 30/min,单 token 60/min + +--- + +## 4. 项目目录 + +``` +amb_rag/ +├── backend/ +│ ├── app/ +│ │ ├── main.py # app 工厂、路由挂载、异常处理、lifespan +│ │ ├── api/ # 管理端路由: auth.py, knowledge_bases.py, documents.py, me.py, deps.py +│ │ ├── public/ # 公共AI路由: routes.py, render.py, serializers.py +│ │ ├── core/ # config.py, security.py, errors.py, session.py, rate_limit.py, logging.py +│ │ ├── models/ # SQLAlchemy 2.0 (Mapped/mapped_column) +│ │ ├── schemas/ # Pydantic v2 request/response +│ │ ├── services/ # auth_service.py, kb_service.py, doc_service.py, kb_public_service.py, storage_service.py +│ │ ├── repositories/ # user_repo.py, kb_repo.py, doc_repo.py, access_log_repo.py +│ │ ├── processors/ # base.py(DocumentProcessor), local_processor.py, parsers/markitdown_parser.py, pdf_parser.py, docx_parser.py +│ │ ├── retrieval/ # base.py(Retriever), keyword_retriever.py +│ │ ├── storage/ # base.py(StorageService), local_storage.py +│ │ └── templates/ # kb_index.html.j2, doc_page.html.j2 +│ ├── alembic/ # env.py + versions/ +│ ├── tests/ # conftest.py + 各模块测试 +│ ├── requirements.txt +│ ├── pyproject.toml # ruff + mypy + pytest 配置 +│ └── Dockerfile +├── frontend/ +│ ├── src/ +│ │ ├── api/ # axios 实例 + 各资源 client +│ │ ├── views/ # Login, Register, Dashboard, KnowledgeBases, KbDetail, Settings +│ │ ├── components/ +│ │ ├── layouts/ +│ │ ├── stores/ # Pinia stores +│ │ ├── router/ +│ │ └── types/ +│ ├── package.json +│ ├── vite.config.ts +│ ├── tsconfig.json +│ └── Dockerfile +├── nginx/ +│ └── nginx.conf +├── data/ # .gitkeep;运行时生成 +│ └── .gitkeep +├── docs/ +│ └── technical-review.md # 本文件 +├── docker-compose.yml +├── .env.example +├── .gitignore +├── CLAUDE.md +└── README.md +``` + +--- + +## 5. 六个未来扩展接口设计 + +### 5.1 SQLite → PostgreSQL + +| 层 | 影响 | +|---|---| +| `core/config.py` | `DATABASE_URL` 从 `sqlite:///./data/app.db` 切为 `postgresql+asyncpg://...`;引擎换 async | +| `core/db.py` | 同步引擎 → 异步引擎 + async_sessionmaker;get_session 改 async generator | +| `repositories/` | 现有同步调用改 `await`;SQLAlchemy 2.x 统一风格使改动最小 | +| Alembic | 新增迁移处理类型映射(text→varchar/citext/uuid, integer→bigint) | +| 业务层 | **零改动**(Repository 已隔离) | + +**第一版必须遵守的规则**: +- 不用 PRAGMA 做业务逻辑(只用 WAL 模式设置) +- 不用 SQLite 专属函数(`group_concat` → SQLAlchemy `func` 通用) +- UUID 存 text 而非 blob(跨 DB 兼容) +- 时间存 ISO8601 text 而非 SQLite datetime 函数 +- 布尔存 integer 0/1(SQLAlchemy Boolean 在 SQLite 自动映射) + +### 5.2 本地文件 → MinIO/S3 + +```python +# storage/base.py +class StorageService(Protocol): + async def save(self, key: str, data: bytes | IO) -> str: ... + async def read(self, key: str) -> bytes: ... + async def delete(self, key: str) -> None: ... + async def exists(self, key: str) -> bool: ... + async def get_size(self, key: str) -> int: ... +``` + +- `LocalStorageService`:key 即相对路径,拼 `data/` 前缀;save → 写文件,read → 读文件。 +- `MinioStorageService`:key 即对象 key;save → minio.put_object,read → minio.get_object。 +- 业务层通过 `core/config.py` 中的 `STORAGE_BACKEND` 选择实现,注入 Service。 +- **第一版**:所有 `open()` / `Path()` 操作只在 `local_storage.py` 中出现。 + +### 5.3 同步解析 → 异步任务 + +```python +# processors/base.py +class DocumentProcessor(Protocol): + async def process(self, document_id: str) -> None: ... +``` + +- `LocalDocumentProcessor`:同步调用 parsers,在同一进程中完成。 +- `CeleryDocumentProcessor`:提交 Celery task,轮询状态。 +- **第一版**:上传 endpoint 中调 `await processor.process(doc_id)`(同步包在 `run_in_executor` 里避免阻塞事件循环)。 +- 文档状态机(PENDING→PROCESSING→READY/FAILED)在两个实现中完全相同。 + +### 5.4 关键词搜索 → RAG + +```python +# retrieval/base.py +class Retriever(Protocol): + async def search(self, query: str, knowledge_base_id: str, limit: int = 10) -> list[SearchResult]: ... +``` + +- `KeywordRetriever`:SQLite FTS5 `MATCH` 查询 + `snippet()` 高亮。 +- `VectorRetriever`:Embedding + Qdrant search。 +- `HybridRetriever`:关键词 + 向量加权融合。 +- **第一版**:FTS5 在 Alembic 迁移中建虚拟表 `documents_fts`;写入时同步更新 FTS。 + +### 5.5 个人 → 团队/企业 + +- **第一版**:`knowledge_bases.user_id` 是 owner;所有查询 `WHERE user_id = :me`。 +- **扩展**:新增 `organizations` 表 + `org_members`(role);`knowledge_bases.owner_type` + `owner_id`(多态外键);Repository 查询加 `owner_scope` 参数。 +- **关键**:第一版不把 `user_id` 硬编码成无法泛化的东西(如拼接进 URL 路径)。 + +### 5.6 网页访问 → API/MCP + +- `public/` 路由调用 `KnowledgeBasePublicService` 获取数据 → 由 Renderer(HTML/MD/TXT/JSON)格式化输出。 +- **扩展**:新增 `/api/v1/public/...` 路由,鉴权用 API Key,**复用同一个 Service**。 +- MCP Server 是独立进程,调 Repository 层。 +- **关键**:公共数据获取逻辑只在 Service 中写一次,Renderer 各格式只做序列化。 + +--- + +## 6. 安全模型 + +### 6.1 Secret Token + +``` +生成:token = secrets.token_urlsafe(16) # 22 字符, 128bit 熵 +存储:knowledge_bases.token_hash = SHA-256(token).hexdigest() + knowledge_bases.token_encrypted = Fernet(SECRET_KEY 派生).encrypt(token) + knowledge_bases.token_hint = token[-8:] +校验:请求 token → 正则校验 → SHA-256 → 单条索引查询 → enabled/软删检查 → 404 或放行 +轮换:regenerate → 新 token,旧 hash 失效,旧缓存失效 +``` + +安全属性: +- 128bit 熵 + 限流 → 暴力枚举不可行 +- DB 泄漏 ≠ 链接泄漏(需 + SECRET_KEY) +- 统一 404 防存在性探测 +- `` 防文档页跳出时 Referer 泄漏 token + +### 6.2 IDOR 防护 + +所有管理端 API 在 Service 层强制校验 `resource.user_id == current_user.id`;不匹配 → 404(不暴露存在性)。 + +### 6.3 文件上传安全 + +- 扩展名白名单(`.docx`, `.pdf`) +- MIME 嗅探(filetype 库,前 8KB) +- 随机物理文件名(`secrets.token_urlsafe(8)_safe_original_name`) +- 路径穿越清洗(取 basename,剔除 `..` / `\` / 控制字符) +- 文件大小前置校验(Nginx `client_max_body_size` + 应用层双重保险) + +### 6.4 XSS 防护 + +- AI 公共页:Markdown → HTML 走 Jinja2 `{{ content | safe }}`,但 Markdown 本身是服务端生成(非用户直接输入 HTML),风险可控;用户手改 Markdown 时需 sanitize(bleach 或 markdown 配置 `sanitize=True`)。 +- 管理端:Vue SPA,API 返回 JSON,Element Plus 组件默认 escape。 + +### 6.5 Session 安全 + +- Cookie: `HttpOnly`, `SameSite=Lax`, 生产环境 `Secure` +- Session token: `secrets.token_urlsafe(32)`, 存 SQLite `sessions` 表(或内存 dict — MVP 足够) +- 过期: 7 天不活动自动清除 + +--- + +## 7. 文档处理流程 + +``` +[HTTP 上传](同步返回) + 鉴权 → KB 归属 → MIME 嗅探 → 扩展名白名单 → 大小校验 + → 配额原子扣减(UPDATE users SET storage_used = storage_used + :size WHERE id=:uid AND storage_used + :size <= storage_quota) + 失败 → STORAGE_QUOTA_EXCEEDED / FILE_TOO_LARGE + → 流式 SHA256(边读边算,内存峰值 = 一个 chunk) + → StorageService.save() 保存原始文件(随机文件名) + → Document(PENDING) 入库 + → DocumentProcessor.process(doc_id) 同步解析: + ┌ .pdf → markitdown → 失败/空 → PyMuPDF fallback → 仍空 → SCANNED_PDF_NO_TEXT_LAYER + ├ .docx → markitdown → 失败 → python-docx fallback + └ 其他 → Phase 2+ 扩展 + → Markdown 清洗(压空行、规整标题层级、截超长行) + → 提取 H1 → title(未填时) + → 抽取前 ~200 字纯文本 → content_summary + → jieba.analyse.extract_tags top-10 → keywords + → StorageService.save() 保存 Markdown 文件 + → 更新 FTS 索引 + → Document(READY) + 任何异常 → Document(FAILED, error_code),用户只看友好文案 + → 返回 201 + Document 信息 +``` + +> **同步解析的注意事项**:大 PDF 解析可能耗时数秒,会阻塞当前请求。MVP 阶段可接受(单用户低并发),但必须: +> 1. 设置请求级超时(uvicorn `--timeout-keep-alive` 不够,需应用层 middleware 或 nginx `proxy_read_timeout`) +> 2. 前端上传组件显示 loading + 预估时间 +> 3. 未来切 Celery 时,上传立即返回 202,前端轮询状态 + +--- + +## 8. AI 访问流程 + +``` +用户 → 在 AI 对话中粘贴 https://example.com/k/7fA92xKpQ8m... + 描述需求 +AI 抓取器 → GET /k/7fA92xKpQ8m...(无 Cookie/JS) + 服务端:限流 → SHA256(token) → 查 KB → 未命中 404 + → 渲染入口页: + · KB 名称/描述 + · 文档列表(每条:标题、类型、描述、一句话摘要、关键词、更新时间、完整文档 URL) + · 分页(≤50 条/页) + · 页脚双语机器可读说明 +AI → 根据用户描述匹配文档 → GET /k/{token}/doc/{doc_token}.md(优先 .md) + 或 .html;需精确定位 → /k/{token}/search?q= +AI → 汇总回答用户 +``` + +**兜底**:若 AI 不跟随链接,用户可直接粘贴文档级 URL。README 必须说明。 + +--- + +## 9. Windows 开发流程 + +```bash +# 后端 +cd backend +python -m venv .venv +# Git Bash: +source .venv/Scripts/activate +# 或 CMD: +.venv\Scripts\activate +pip install -r requirements.txt +# 初始化数据库 +alembic upgrade head +# 启动 +uvicorn app.main:app --reload --port 8000 +# 访问 +# http://localhost:8000/api/health → 健康检查 +# http://localhost:8000/api/docs → Swagger(开发期) + +# 前端 +cd frontend +npm install +npm run dev +# http://localhost:5173 +# Vite proxy: /api → localhost:8000, /k → localhost:8000 +``` + +**关键**:`data/` 目录在首次运行时由 backend 自动创建(`os.makedirs`);`.env` 仅生产需要(开发期 config 用默认值)。 + +--- + +## 10. Docker 生产流程 + +```yaml +# docker-compose.yml(Ubuntu 服务器) +services: + frontend: # multi-stage: node build → nginx:alpine serve dist + backend: # python:3.12-slim → uvicorn + nginx: # 反代 /api + /k → backend:8000;/ → frontend:80 + +volumes: + - ./data:/app/data # SQLite + 用户文件,宿主机持久化 +``` + +```bash +# 部署 +git clone /opt/ai-knowledge-link +cd /opt/ai-knowledge-link +cp .env.example .env # 修改全部密钥 +mkdir -p data +docker compose up -d --build + +# 更新 +git pull +docker compose up -d --build # 数据在 ./data 不受影响 + +# 备份 +sqlite3 data/app.db ".backup data/backup/app_$(date +%Y%m%d).db" +tar czf data/backup/files_$(date +%Y%m%d).tar.gz data/users/ +``` + +--- + +## 11. 风险清单 + +| # | 风险 | 等级 | 缓解 | +|---|---|---|---| +| R1 | **同步解析阻塞请求**:大 PDF 解析耗时数秒到十几秒 | 中 | MVP 可接受;前端显示 loading;设请求超时 60s;未来切 Celery | +| R2 | **SQLite 并发写锁**:多用户同时上传时 WAL 模式允许并发读但写仍串行 | 中 | `PRAGMA journal_mode=WAL`;写操作短事务;未来切 PostgreSQL | +| R3 | **PyMuPDF AGPL-3.0**:商品化闭源分发时许可风险 | 中 | MVP 阶段可接受(仅服务端运行,不分发);requirements 中注释标记;商品化前换 pypdfium2 | +| R4 | **SQLite FTS5 中文分词**:FTS5 的 `unicode61` tokenizer 对中文按字分词(非词),召回率低于 jieba | 中 | 第一版仍用 FTS5(比 LIKE 好很多);关键词字段用 jieba 分词写入辅助;Phase 9 验收时若不满意可切换到 jieba 预分词 + LIKE 方案 | +| R5 | **FastAPI 后缀路由被 path param 吞掉** | 低 | 显式后缀路由注册在无后缀路由之前;token 用正则 `^[A-Za-z0-9_-]{22}$` | +| R6 | **内存限流单进程局限** | 低 | MVP 单 uvicorn worker 够用;未来多 worker/容器需 Redis 限流 | +| R7 | **Markdown XSS**:用户手改 Markdown 注入 ` + + \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..618465c --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,3815 @@ +{ + "name": "ai-knowledge-link-frontend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ai-knowledge-link-frontend", + "version": "0.1.0", + "dependencies": { + "@element-plus/icons-vue": "^2.3", + "axios": "^1.7", + "element-plus": "^2.9", + "pinia": "^2.2", + "vue": "^3.5", + "vue-router": "^4.4" + }, + "devDependencies": { + "@types/node": "^22.0", + "@typescript-eslint/eslint-plugin": "^8.0", + "@typescript-eslint/parser": "^8.0", + "@vitejs/plugin-vue": "^5.2", + "eslint": "^9.0", + "eslint-plugin-vue": "^9.28", + "prettier": "^3.4", + "typescript": "^5.7", + "vite": "^6.1", + "vue-tsc": "^2.2" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz", + "integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@element-plus/icons-vue": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz", + "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==", + "license": "MIT", + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz", + "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.0", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "license": "MIT" + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "license": "MIT" + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@popperjs/core": { + "name": "@sxzz/popperjs-es", + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz", + "integrity": "sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.1.tgz", + "integrity": "sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.1.tgz", + "integrity": "sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.1.tgz", + "integrity": "sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.1.tgz", + "integrity": "sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.1.tgz", + "integrity": "sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.1.tgz", + "integrity": "sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.1.tgz", + "integrity": "sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.1.tgz", + "integrity": "sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.1.tgz", + "integrity": "sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.1.tgz", + "integrity": "sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.1.tgz", + "integrity": "sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.1.tgz", + "integrity": "sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.1.tgz", + "integrity": "sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.1.tgz", + "integrity": "sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.1.tgz", + "integrity": "sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.1.tgz", + "integrity": "sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.1.tgz", + "integrity": "sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.1.tgz", + "integrity": "sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.1.tgz", + "integrity": "sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.1.tgz", + "integrity": "sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.1.tgz", + "integrity": "sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.1.tgz", + "integrity": "sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.1.tgz", + "integrity": "sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.1.tgz", + "integrity": "sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.1.tgz", + "integrity": "sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.25.tgz", + "integrity": "sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==", + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.69.0.tgz", + "integrity": "sha512-t5jQTKPIgVW1PE6dR6H6Qz5gm8zjMlX5/2gRaOGd9eO6V7J+tQc6iWKukEe7dY8u9HyYasQ0yfF0/FSSTEO2gA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.69.0", + "@typescript-eslint/type-utils": "8.69.0", + "@typescript-eslint/utils": "8.69.0", + "@typescript-eslint/visitor-keys": "8.69.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.69.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.69.0.tgz", + "integrity": "sha512-l4b0DhWioGg6Gt2ebGlvfkFMOjRsauxtsnDRwUSRX1qHq3HdTfQHV8wW9zEXeciai6HfeaKOedQn2Zoofx3WBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.69.0", + "@typescript-eslint/types": "8.69.0", + "@typescript-eslint/typescript-estree": "8.69.0", + "@typescript-eslint/visitor-keys": "8.69.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.69.0.tgz", + "integrity": "sha512-yi4obFrHMmnsesWehHbkg9zMA7Jt8cXT+mKM08G999pH1yT6nqgsHx7MYm0uY1wAj8CqiBXYRJ7WAT0QdQHQXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.69.0", + "@typescript-eslint/types": "^8.69.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.69.0.tgz", + "integrity": "sha512-ewfspqWvSxKSOaplqAUNbaSFO0eB6w1EtQ+esfYFRm3614Ty4uNtExkcbgd6nWsXphbqKyf9ZYdbZdv2xEoWEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.69.0", + "@typescript-eslint/visitor-keys": "8.69.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.69.0.tgz", + "integrity": "sha512-xNqK7YTDZsLniQMV/4rpFR8Z5JlqeRvVjuG1YgF/mdPVH84HSD19L8CczMA0qg2RfwEV231GHH3VnToJDo4MfQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.69.0.tgz", + "integrity": "sha512-ZfoJAVg3JZndQEpEl9petVlxau3lRuElc4HRMuAlLCf8to04/iHz692RUSNmXKDjEuJmIL+KZ2/BsOcBc16dsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.69.0", + "@typescript-eslint/typescript-estree": "8.69.0", + "@typescript-eslint/utils": "8.69.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.69.0.tgz", + "integrity": "sha512-K3VrubUPhlo9VDBS6QdI8YB5j7ClpqLRdefcz6PFrhnwicehBweqQ9Evhl4l+FYz0HdDmMqIiSX0aldGRYtDCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.69.0.tgz", + "integrity": "sha512-AdFkgqck3Vudb/kWnxlyafU/4aBhHrbQ9locP2N4psXTy5mOBg0SHJumnLvx7r6g1gV4DKvUFwV2nJZBoqOD8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.69.0", + "@typescript-eslint/tsconfig-utils": "8.69.0", + "@typescript-eslint/types": "8.69.0", + "@typescript-eslint/visitor-keys": "8.69.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.69.0.tgz", + "integrity": "sha512-tUbx60BBqQa31kXF5MCsOOLL5E/WzUuxIn7YpAvq+eaUlqvk8/NXnXMBNAdLCr0icjkzem7iUA5QqWHe/hJ1aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.69.0", + "@typescript-eslint/types": "8.69.0", + "@typescript-eslint/typescript-estree": "8.69.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.69.0.tgz", + "integrity": "sha512-+rmdgPA+EXkNgKYvHvFfhrs35utXbwaC5PGpDquSXcoXQDKUA5UjV0LmTucG/4JXkM31BTu4TilHtrN8IVBe8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.69.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.15.tgz", + "integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.15" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.15.tgz", + "integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.15.tgz", + "integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.42", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.42.tgz", + "integrity": "sha512-2Ye1ilMtKXxl8qZUrQ5j0CdgenFp/HFQmta6rfRyfEsTG69L6Wk+tWuNoHYHMx9E8tF2Slvdg1FuwDvAXdy1LQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@vue/shared": "3.5.42", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.42", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.42.tgz", + "integrity": "sha512-qbhQZEFmycr+ni/qyuccS4sucNN7VAbDfbkvNxWOX2VfgFm90MNs3/UhRNKoPMEIVn0F8gdlYjLPvqxHwHeQOA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.42", + "@vue/shared": "3.5.42" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.42", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.42.tgz", + "integrity": "sha512-fkCAFB4okcAANGMThboWnScp/gzWjU0ZSkVnjTIiplmMDq2uq0tIB3j+xVu4rhv5rvOgBySCysudmbMd6xRRqw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@vue/compiler-core": "3.5.42", + "@vue/compiler-dom": "3.5.42", + "@vue/compiler-ssr": "3.5.42", + "@vue/shared": "3.5.42", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.19", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.42", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.42.tgz", + "integrity": "sha512-xmLk3wLkbizPAiLyomjgFFosf2ys9b5Ghb+oh/k2tnvipNz8OFrQOiTcWCzyK7MpBp9KkyGtfvgfLUivbmuGYA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.42", + "@vue/shared": "3.5.42" + } + }, + "node_modules/@vue/compiler-vue2": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", + "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", + "dev": true, + "license": "MIT", + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/language-core": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.12.tgz", + "integrity": "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "@vue/compiler-dom": "^3.5.0", + "@vue/compiler-vue2": "^2.7.16", + "@vue/shared": "^3.5.0", + "alien-signals": "^1.0.3", + "minimatch": "^9.0.3", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/language-core/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/language-core/node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@vue/language-core/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.42", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.42.tgz", + "integrity": "sha512-TzNNfKpb7hDxbQltwAut8VDQA5YP+BuRlxntHUuRjyKwlMvmAPbs3unhCvieijifY6vFfVBwsS7wG/C7uq+bEQ==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.42" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.42", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.42.tgz", + "integrity": "sha512-9uACtuHs7vJGkm5Bp3xu4xRDLFTIYy5DgxpToVjqGIAhAEKwQfsaLvKINhM6nFVp6bZPRFGdDqd1g52MqKsotA==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.42", + "@vue/shared": "3.5.42" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.42", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.42.tgz", + "integrity": "sha512-rsCmhiWLaRxGltLwhlCWyYkFn7WAbKRh0q17eZ1A6Dq6eqc2ACQ61IIryxz0LrsvCzHSilLA9JHovVwM8CNE2g==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.42", + "@vue/runtime-core": "3.5.42", + "@vue/shared": "3.5.42", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.42", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.42.tgz", + "integrity": "sha512-2++5dUyYS4gvo7xQXSECUDhB7TS0aOl5SeVfC5qSq1Jgfhjvegw1zqhwTIR3imZ+QYPJQw9gfcFvXGAjGZ7ajQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.42", + "@vue/runtime-dom": "3.5.42", + "@vue/shared": "3.5.42" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.42", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.42.tgz", + "integrity": "sha512-2rPxex1jQf4jvl9MOHl6YaXCPcrNqz/FstMOEh3QWY+/OME9nQTvl9WYeCwhW7AFjaR0SnngZGlp/wkR6rkI6g==", + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "14.4.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.4.0.tgz", + "integrity": "sha512-X4WHz1HlCzCBoYXesUkifzzWBAcZgXG8Fi5iNPQg/epdzOB3gu8Fawj3hvuwYR1nGcXGnvxwYYcUC/71++svtQ==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "14.4.0", + "@vueuse/shared": "14.4.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@vueuse/metadata": { + "version": "14.4.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.4.0.tgz", + "integrity": "sha512-swx/255R6JyHZFJhx845iz5CRWDZdCfvkZOpACWc5+c5WHcG24mv8gUT1WIdFQaHt6dq79rvILd9QnCWiyVm9g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "14.4.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.4.0.tgz", + "integrity": "sha512-JRgY90Sz8DDtPMsaDflvPMp9xYk69JZAmbuDvAquUVXKr2gEjqtzGNTTthLfckH0BzBqvnu31gb4a8TGLRe79g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/alien-signals": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz", + "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz", + "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.6", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.23", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.23.tgz", + "integrity": "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==", + "license": "MIT" + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/element-plus": { + "version": "2.14.5", + "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.14.5.tgz", + "integrity": "sha512-bghYy/S+qg87enHPXELirhEdDqsVAUGcGpbGIeG8dz0kwpIkGz7gYsifulBshXX74iRtHib85XWQj0uSH2A1Yg==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^4.2.0", + "@element-plus/icons-vue": "^2.3.2", + "@floating-ui/dom": "^1.8.0", + "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.8", + "@types/lodash": "^4.17.24", + "@types/lodash-es": "^4.17.12", + "@vueuse/core": "14.4.0", + "async-validator": "^4.2.5", + "dayjs": "^1.11.20", + "lodash": "^4.18.1", + "lodash-es": "^4.18.1", + "lodash-unified": "^1.0.3", + "memoize-one": "^6.0.0", + "normalize-wheel-es": "^1.2.0", + "vue-component-type-helpers": "^3.3.9" + }, + "peerDependencies": { + "vue": "^3.3.7" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-vue": { + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.33.0.tgz", + "integrity": "sha512-174lJKuNsuDIlLpjeXc5E2Tss8P44uIimAfGD0b90k0NoirJqpG7stLuU9Vp/9ioTOrQdWVREc4mRd1BD+CvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "globals": "^13.24.0", + "natural-compare": "^1.4.0", + "nth-check": "^2.1.1", + "postcss-selector-parser": "^6.0.15", + "semver": "^7.6.3", + "vue-eslint-parser": "^9.4.3", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-vue/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/ignore": { + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.8.tgz", + "integrity": "sha512-YYNsSlXBjMk92SKnkwvB5LOVSa6OznlFUGcsvrFgNJbJCd0M1XKeFVRc8ZByeCqz32FivYNHJVooLmdqrmvp/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, + "node_modules/lodash-unified": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/lodash-unified/-/lodash-unified-1.0.3.tgz", + "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==", + "license": "MIT", + "peerDependencies": { + "@types/lodash-es": "*", + "lodash": "*", + "lodash-es": "*" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-wheel-es": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", + "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==", + "license": "BSD-3-Clause" + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pinia": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz", + "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.1.tgz", + "integrity": "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.1", + "@rollup/rollup-android-arm64": "4.63.1", + "@rollup/rollup-darwin-arm64": "4.63.1", + "@rollup/rollup-darwin-x64": "4.63.1", + "@rollup/rollup-freebsd-arm64": "4.63.1", + "@rollup/rollup-freebsd-x64": "4.63.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.1", + "@rollup/rollup-linux-arm-musleabihf": "4.63.1", + "@rollup/rollup-linux-arm64-gnu": "4.63.1", + "@rollup/rollup-linux-arm64-musl": "4.63.1", + "@rollup/rollup-linux-loong64-gnu": "4.63.1", + "@rollup/rollup-linux-loong64-musl": "4.63.1", + "@rollup/rollup-linux-ppc64-gnu": "4.63.1", + "@rollup/rollup-linux-ppc64-musl": "4.63.1", + "@rollup/rollup-linux-riscv64-gnu": "4.63.1", + "@rollup/rollup-linux-riscv64-musl": "4.63.1", + "@rollup/rollup-linux-s390x-gnu": "4.63.1", + "@rollup/rollup-linux-x64-gnu": "4.63.1", + "@rollup/rollup-linux-x64-musl": "4.63.1", + "@rollup/rollup-openbsd-x64": "4.63.1", + "@rollup/rollup-openharmony-arm64": "4.63.1", + "@rollup/rollup-win32-arm64-msvc": "4.63.1", + "@rollup/rollup-win32-ia32-msvc": "4.63.1", + "@rollup/rollup-win32-x64-gnu": "4.63.1", + "@rollup/rollup-win32-x64-msvc": "4.63.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vscode-uri": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.2.0.tgz", + "integrity": "sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.42", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.42.tgz", + "integrity": "sha512-4RyHQTbQvOPs3MfvUO1Sg0YRrKNnA0mAVtvpd12Tg1fKDN7OHBUl1IqSn8zGJjK9nI3NkNp8cgTpVrSZC5TTcA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.42", + "@vue/compiler-sfc": "3.5.42", + "@vue/runtime-dom": "3.5.42", + "@vue/server-renderer": "3.5.42", + "@vue/shared": "3.5.42" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-component-type-helpers": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-3.3.11.tgz", + "integrity": "sha512-LwcxzeliO9fkQcpJG0PoX8X5kmAhKmH9wkpDLxNabwzkQ9Zeib2YVHwFV4pcWmMLfXVfjr/dSV+DaJ3cIPgSNA==", + "license": "MIT" + }, + "node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-eslint-parser": { + "version": "9.4.3", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz", + "integrity": "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "eslint-scope": "^7.1.1", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.1", + "esquery": "^1.4.0", + "lodash": "^4.17.21", + "semver": "^7.3.6" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/vue-eslint-parser/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/vue-eslint-parser/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/vue-tsc": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.12.tgz", + "integrity": "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.15", + "@vue/language-core": "2.2.12" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..02c8f13 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,33 @@ +{ + "name": "ai-knowledge-link-frontend", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc -b && vite build", + "preview": "vite preview", + "lint": "eslint . --ext .vue,.ts,.tsx --fix", + "format": "prettier --write src/" + }, + "dependencies": { + "vue": "^3.5", + "vue-router": "^4.4", + "pinia": "^2.2", + "element-plus": "^2.9", + "axios": "^1.7", + "@element-plus/icons-vue": "^2.3" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.2", + "vite": "^6.1", + "typescript": "^5.7", + "vue-tsc": "^2.2", + "@types/node": "^22.0", + "eslint": "^9.0", + "eslint-plugin-vue": "^9.28", + "@typescript-eslint/eslint-plugin": "^8.0", + "@typescript-eslint/parser": "^8.0", + "prettier": "^3.4" + } +} \ No newline at end of file diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..14949a0 --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/frontend/src/App.vue.js b/frontend/src/App.vue.js new file mode 100644 index 0000000..531ae99 --- /dev/null +++ b/frontend/src/App.vue.js @@ -0,0 +1,26 @@ +import { RouterView } from 'vue-router'; +debugger; /* PartiallyEnd: #3632/scriptSetup.vue */ +const __VLS_ctx = {}; +let __VLS_components; +let __VLS_directives; +const __VLS_0 = {}.RouterView; +/** @type {[typeof __VLS_components.RouterView, ]} */ ; +// @ts-ignore +const __VLS_1 = __VLS_asFunctionalComponent(__VLS_0, new __VLS_0({})); +const __VLS_2 = __VLS_1({}, ...__VLS_functionalComponentArgsRest(__VLS_1)); +var __VLS_4 = {}; +var __VLS_3; +var __VLS_dollars; +const __VLS_self = (await import('vue')).defineComponent({ + setup() { + return { + RouterView: RouterView, + }; + }, +}); +export default (await import('vue')).defineComponent({ + setup() { + return {}; + }, +}); +; /* PartiallyEnd: #4569/main.vue */ diff --git a/frontend/src/api/client.js b/frontend/src/api/client.js new file mode 100644 index 0000000..a545baf --- /dev/null +++ b/frontend/src/api/client.js @@ -0,0 +1,35 @@ +import axios from 'axios'; +import { ElMessage } from 'element-plus'; +const apiClient = axios.create({ + baseURL: '/api', + timeout: 30000, + withCredentials: true, + headers: { + 'X-Requested-With': 'XMLHttpRequest', + }, +}); +// 响应拦截:统一错误处理 +apiClient.interceptors.response.use((response) => response, (error) => { + if (error.response) { + const { status, data } = error.response; + // 未登录 → 跳转登录页 + if (status === 401) { + // 避免在登录页循环跳转 + if (window.location.pathname !== '/login') { + window.location.href = '/login'; + } + return Promise.reject(error); + } + // 业务错误:显示服务端中文文案 + const message = data?.message || '请求失败'; + ElMessage.error(message); + } + else if (error.request) { + ElMessage.error('网络错误,请检查连接。'); + } + else { + ElMessage.error('请求配置错误。'); + } + return Promise.reject(error); +}); +export default apiClient; diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts new file mode 100644 index 0000000..91460fe --- /dev/null +++ b/frontend/src/api/client.ts @@ -0,0 +1,41 @@ +import axios from 'axios' +import { ElMessage } from 'element-plus' + +const apiClient = axios.create({ + baseURL: '/api', + timeout: 30000, + withCredentials: true, + headers: { + 'X-Requested-With': 'XMLHttpRequest', + }, +}) + +// 响应拦截:统一错误处理 +apiClient.interceptors.response.use( + (response) => response, + (error) => { + if (error.response) { + const { status, data } = error.response + + // 未登录 → 跳转登录页 + if (status === 401) { + // 避免在登录页循环跳转 + if (window.location.pathname !== '/login') { + window.location.href = '/login' + } + return Promise.reject(error) + } + + // 业务错误:显示服务端中文文案 + const message = data?.message || '请求失败' + ElMessage.error(message) + } else if (error.request) { + ElMessage.error('网络错误,请检查连接。') + } else { + ElMessage.error('请求配置错误。') + } + return Promise.reject(error) + }, +) + +export default apiClient \ No newline at end of file diff --git a/frontend/src/env.d.ts b/frontend/src/env.d.ts new file mode 100644 index 0000000..8c3ea27 --- /dev/null +++ b/frontend/src/env.d.ts @@ -0,0 +1,7 @@ +/// + +declare module '*.vue' { + import type { DefineComponent } from 'vue' + const component: DefineComponent + export default component +} \ No newline at end of file diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 0000000..8d9c62a --- /dev/null +++ b/frontend/src/main.js @@ -0,0 +1,12 @@ +import { createApp } from 'vue'; +import { createPinia } from 'pinia'; +import ElementPlus from 'element-plus'; +import zhCn from 'element-plus/es/locale/lang/zh-cn'; +import 'element-plus/dist/index.css'; +import App from './App.vue'; +import router from './router'; +const app = createApp(App); +app.use(createPinia()); +app.use(router); +app.use(ElementPlus, { locale: zhCn }); +app.mount('#app'); diff --git a/frontend/src/main.ts b/frontend/src/main.ts new file mode 100644 index 0000000..97e8cbc --- /dev/null +++ b/frontend/src/main.ts @@ -0,0 +1,14 @@ +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import ElementPlus from 'element-plus' +import zhCn from 'element-plus/es/locale/lang/zh-cn' +import 'element-plus/dist/index.css' + +import App from './App.vue' +import router from './router' + +const app = createApp(App) +app.use(createPinia()) +app.use(router) +app.use(ElementPlus, { locale: zhCn }) +app.mount('#app') \ No newline at end of file diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js new file mode 100644 index 0000000..7466b3d --- /dev/null +++ b/frontend/src/router/index.js @@ -0,0 +1,37 @@ +import { createRouter, createWebHistory } from 'vue-router'; +const router = createRouter({ + history: createWebHistory(), + routes: [ + { + path: '/login', + name: 'Login', + component: () => import('@/views/Login.vue'), + }, + { + path: '/register', + name: 'Register', + component: () => import('@/views/Register.vue'), + }, + { + path: '/', + name: 'Dashboard', + component: () => import('@/views/Dashboard.vue'), + }, + { + path: '/knowledge-bases', + name: 'KnowledgeBases', + component: () => import('@/views/KnowledgeBases.vue'), + }, + { + path: '/knowledge-bases/:id', + name: 'KbDetail', + component: () => import('@/views/KbDetail.vue'), + }, + { + path: '/settings', + name: 'Settings', + component: () => import('@/views/Settings.vue'), + }, + ], +}); +export default router; diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts new file mode 100644 index 0000000..fb9fef2 --- /dev/null +++ b/frontend/src/router/index.ts @@ -0,0 +1,39 @@ +import { createRouter, createWebHistory } from 'vue-router' + +const router = createRouter({ + history: createWebHistory(), + routes: [ + { + path: '/login', + name: 'Login', + component: () => import('@/views/Login.vue'), + }, + { + path: '/register', + name: 'Register', + component: () => import('@/views/Register.vue'), + }, + { + path: '/', + name: 'Dashboard', + component: () => import('@/views/Dashboard.vue'), + }, + { + path: '/knowledge-bases', + name: 'KnowledgeBases', + component: () => import('@/views/KnowledgeBases.vue'), + }, + { + path: '/knowledge-bases/:id', + name: 'KbDetail', + component: () => import('@/views/KbDetail.vue'), + }, + { + path: '/settings', + name: 'Settings', + component: () => import('@/views/Settings.vue'), + }, + ], +}) + +export default router \ No newline at end of file diff --git a/frontend/src/stores/user.js b/frontend/src/stores/user.js new file mode 100644 index 0000000..d66271b --- /dev/null +++ b/frontend/src/stores/user.js @@ -0,0 +1,18 @@ +import { defineStore } from 'pinia'; +import { ref } from 'vue'; +export const useUserStore = defineStore('user', () => { + const isLoggedIn = ref(false); + const username = ref(''); + const userId = ref(''); + function setUser(data) { + isLoggedIn.value = true; + userId.value = data.id; + username.value = data.username; + } + function clearUser() { + isLoggedIn.value = false; + userId.value = ''; + username.value = ''; + } + return { isLoggedIn, username, userId, setUser, clearUser }; +}); diff --git a/frontend/src/stores/user.ts b/frontend/src/stores/user.ts new file mode 100644 index 0000000..81acf65 --- /dev/null +++ b/frontend/src/stores/user.ts @@ -0,0 +1,22 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' + +export const useUserStore = defineStore('user', () => { + const isLoggedIn = ref(false) + const username = ref('') + const userId = ref('') + + function setUser(data: { id: string; username: string }) { + isLoggedIn.value = true + userId.value = data.id + username.value = data.username + } + + function clearUser() { + isLoggedIn.value = false + userId.value = '' + username.value = '' + } + + return { isLoggedIn, username, userId, setUser, clearUser } +}) \ No newline at end of file diff --git a/frontend/src/views/Dashboard.vue b/frontend/src/views/Dashboard.vue new file mode 100644 index 0000000..c5818c3 --- /dev/null +++ b/frontend/src/views/Dashboard.vue @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/frontend/src/views/Dashboard.vue.js b/frontend/src/views/Dashboard.vue.js new file mode 100644 index 0000000..e30bddd --- /dev/null +++ b/frontend/src/views/Dashboard.vue.js @@ -0,0 +1,22 @@ +debugger; /* PartiallyEnd: #3632/scriptSetup.vue */ +const __VLS_ctx = {}; +let __VLS_components; +let __VLS_directives; +__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({ + ...{ class: "dashboard-page" }, +}); +__VLS_asFunctionalElement(__VLS_intrinsicElements.h1, __VLS_intrinsicElements.h1)({}); +__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({}); +/** @type {__VLS_StyleScopedClasses['dashboard-page']} */ ; +var __VLS_dollars; +const __VLS_self = (await import('vue')).defineComponent({ + setup() { + return {}; + }, +}); +export default (await import('vue')).defineComponent({ + setup() { + return {}; + }, +}); +; /* PartiallyEnd: #4569/main.vue */ diff --git a/frontend/src/views/KbDetail.vue b/frontend/src/views/KbDetail.vue new file mode 100644 index 0000000..868d1ef --- /dev/null +++ b/frontend/src/views/KbDetail.vue @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/frontend/src/views/KbDetail.vue.js b/frontend/src/views/KbDetail.vue.js new file mode 100644 index 0000000..09e44bb --- /dev/null +++ b/frontend/src/views/KbDetail.vue.js @@ -0,0 +1,19 @@ +debugger; /* PartiallyEnd: #3632/scriptSetup.vue */ +const __VLS_ctx = {}; +let __VLS_components; +let __VLS_directives; +__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({}); +__VLS_asFunctionalElement(__VLS_intrinsicElements.h1, __VLS_intrinsicElements.h1)({}); +__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({}); +var __VLS_dollars; +const __VLS_self = (await import('vue')).defineComponent({ + setup() { + return {}; + }, +}); +export default (await import('vue')).defineComponent({ + setup() { + return {}; + }, +}); +; /* PartiallyEnd: #4569/main.vue */ diff --git a/frontend/src/views/KnowledgeBases.vue b/frontend/src/views/KnowledgeBases.vue new file mode 100644 index 0000000..70c30d9 --- /dev/null +++ b/frontend/src/views/KnowledgeBases.vue @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/frontend/src/views/KnowledgeBases.vue.js b/frontend/src/views/KnowledgeBases.vue.js new file mode 100644 index 0000000..09e44bb --- /dev/null +++ b/frontend/src/views/KnowledgeBases.vue.js @@ -0,0 +1,19 @@ +debugger; /* PartiallyEnd: #3632/scriptSetup.vue */ +const __VLS_ctx = {}; +let __VLS_components; +let __VLS_directives; +__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({}); +__VLS_asFunctionalElement(__VLS_intrinsicElements.h1, __VLS_intrinsicElements.h1)({}); +__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({}); +var __VLS_dollars; +const __VLS_self = (await import('vue')).defineComponent({ + setup() { + return {}; + }, +}); +export default (await import('vue')).defineComponent({ + setup() { + return {}; + }, +}); +; /* PartiallyEnd: #4569/main.vue */ diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue new file mode 100644 index 0000000..317d47a --- /dev/null +++ b/frontend/src/views/Login.vue @@ -0,0 +1,27 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/Login.vue.js b/frontend/src/views/Login.vue.js new file mode 100644 index 0000000..b9d6ec6 --- /dev/null +++ b/frontend/src/views/Login.vue.js @@ -0,0 +1,39 @@ +debugger; /* PartiallyEnd: #3632/scriptSetup.vue */ +const __VLS_ctx = {}; +let __VLS_components; +let __VLS_directives; +// CSS variable injection +// CSS variable injection end +__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({ + ...{ class: "login-page" }, +}); +const __VLS_0 = {}.ElCard; +/** @type {[typeof __VLS_components.ElCard, typeof __VLS_components.elCard, typeof __VLS_components.ElCard, typeof __VLS_components.elCard, ]} */ ; +// @ts-ignore +const __VLS_1 = __VLS_asFunctionalComponent(__VLS_0, new __VLS_0({ + ...{ class: "login-card" }, +})); +const __VLS_2 = __VLS_1({ + ...{ class: "login-card" }, +}, ...__VLS_functionalComponentArgsRest(__VLS_1)); +__VLS_3.slots.default; +{ + const { header: __VLS_thisSlot } = __VLS_3.slots; + __VLS_asFunctionalElement(__VLS_intrinsicElements.h2, __VLS_intrinsicElements.h2)({}); +} +__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({}); +var __VLS_3; +/** @type {__VLS_StyleScopedClasses['login-page']} */ ; +/** @type {__VLS_StyleScopedClasses['login-card']} */ ; +var __VLS_dollars; +const __VLS_self = (await import('vue')).defineComponent({ + setup() { + return {}; + }, +}); +export default (await import('vue')).defineComponent({ + setup() { + return {}; + }, +}); +; /* PartiallyEnd: #4569/main.vue */ diff --git a/frontend/src/views/Register.vue b/frontend/src/views/Register.vue new file mode 100644 index 0000000..efd240e --- /dev/null +++ b/frontend/src/views/Register.vue @@ -0,0 +1,27 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/Register.vue.js b/frontend/src/views/Register.vue.js new file mode 100644 index 0000000..76db03e --- /dev/null +++ b/frontend/src/views/Register.vue.js @@ -0,0 +1,39 @@ +debugger; /* PartiallyEnd: #3632/scriptSetup.vue */ +const __VLS_ctx = {}; +let __VLS_components; +let __VLS_directives; +// CSS variable injection +// CSS variable injection end +__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({ + ...{ class: "register-page" }, +}); +const __VLS_0 = {}.ElCard; +/** @type {[typeof __VLS_components.ElCard, typeof __VLS_components.elCard, typeof __VLS_components.ElCard, typeof __VLS_components.elCard, ]} */ ; +// @ts-ignore +const __VLS_1 = __VLS_asFunctionalComponent(__VLS_0, new __VLS_0({ + ...{ class: "register-card" }, +})); +const __VLS_2 = __VLS_1({ + ...{ class: "register-card" }, +}, ...__VLS_functionalComponentArgsRest(__VLS_1)); +__VLS_3.slots.default; +{ + const { header: __VLS_thisSlot } = __VLS_3.slots; + __VLS_asFunctionalElement(__VLS_intrinsicElements.h2, __VLS_intrinsicElements.h2)({}); +} +__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({}); +var __VLS_3; +/** @type {__VLS_StyleScopedClasses['register-page']} */ ; +/** @type {__VLS_StyleScopedClasses['register-card']} */ ; +var __VLS_dollars; +const __VLS_self = (await import('vue')).defineComponent({ + setup() { + return {}; + }, +}); +export default (await import('vue')).defineComponent({ + setup() { + return {}; + }, +}); +; /* PartiallyEnd: #4569/main.vue */ diff --git a/frontend/src/views/Settings.vue b/frontend/src/views/Settings.vue new file mode 100644 index 0000000..218e374 --- /dev/null +++ b/frontend/src/views/Settings.vue @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/frontend/src/views/Settings.vue.js b/frontend/src/views/Settings.vue.js new file mode 100644 index 0000000..09e44bb --- /dev/null +++ b/frontend/src/views/Settings.vue.js @@ -0,0 +1,19 @@ +debugger; /* PartiallyEnd: #3632/scriptSetup.vue */ +const __VLS_ctx = {}; +let __VLS_components; +let __VLS_directives; +__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({}); +__VLS_asFunctionalElement(__VLS_intrinsicElements.h1, __VLS_intrinsicElements.h1)({}); +__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({}); +var __VLS_dollars; +const __VLS_self = (await import('vue')).defineComponent({ + setup() { + return {}; + }, +}); +export default (await import('vue')).defineComponent({ + setup() { + return {}; + }, +}); +; /* PartiallyEnd: #4569/main.vue */ diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..a4e969b --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "jsx": "preserve", + "importHelpers": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "useDefineForClassFields": true, + "resolveJsonModule": true, + "isolatedModules": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + }, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "types": ["vite/client"] + }, + "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], + "exclude": ["node_modules", "dist"] +} \ No newline at end of file diff --git a/frontend/tsconfig.tsbuildinfo b/frontend/tsconfig.tsbuildinfo new file mode 100644 index 0000000..5fb6763 --- /dev/null +++ b/frontend/tsconfig.tsbuildinfo @@ -0,0 +1 @@ +{"root":["./src/env.d.ts","./src/main.ts","./src/api/client.ts","./src/router/index.ts","./src/stores/user.ts","./src/app.vue","./src/views/dashboard.vue","./src/views/kbdetail.vue","./src/views/knowledgebases.vue","./src/views/login.vue","./src/views/register.vue","./src/views/settings.vue"],"version":"5.9.3"} \ No newline at end of file diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..9831e4b --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,27 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import { resolve } from 'path' + +export default defineConfig({ + plugins: [vue()], + resolve: { + alias: { + '@': resolve(__dirname, 'src'), + }, + }, + server: { + port: 5173, + proxy: { + // 管理端 API + '/api': { + target: 'http://localhost:8000', + changeOrigin: true, + }, + // 公共 AI 页面 + '/k': { + target: 'http://localhost:8000', + changeOrigin: true, + }, + }, + }, +}) \ No newline at end of file diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 0000000..a961689 --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,43 @@ +# AI Knowledge Link — Nginx 生产配置 +# Docker Compose 中 frontend 容器 serve 静态产物,此配置用于独立 nginx 容器反代。 + +upstream backend { + server backend:8000; +} + +server { + listen 80; + server_name _; + + client_max_body_size 25m; + + # --- 管理端 API --- + location /api/ { + proxy_pass http://backend; + 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; + proxy_read_timeout 60s; + } + + # --- 公共 AI 页面 --- + # 禁止 Nginx 层缓存(公共页缓存只允许应用内,键含 token_hash) + location /k/ { + proxy_pass http://backend; + 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; + add_header Cache-Control "private, no-cache, no-store" always; + add_header X-Robots-Tag "noindex, nofollow, noarchive" always; + } + + # --- 前端静态 --- + location / { + proxy_pass http://frontend:80; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } +} \ No newline at end of file