commit c0f9ddabbf9537db5cf38cc5022bdfe7cf4a728f Author: ntnt Date: Wed Mar 11 16:28:04 2026 +0800 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ac9c601 --- /dev/null +++ b/.gitignore @@ -0,0 +1,61 @@ +# Python +__pycache__/ +*.py[cod] +*.egg-info/ +*.egg +dist/ +build/ +.eggs/ +*.whl + +# Virtual environments +.venv/ +venv/ +env/ + +# Node +node_modules/ +frontend/node_modules/ + +# Build output +frontend/dist/ + +# Environment & secrets +.env +.env.* +!.env.example +backend/.env + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# Uploads & generated files +backend/uploads/ +uploads/ + +# Database +*.db +*.sqlite3 + +# Pytest +.pytest_cache/ +htmlcov/ +.coverage + +# Celery +celerybeat-schedule +celerybeat.pid + +# Alembic +backend/alembic/versions/*.pyc + +# Logs +*.log + +# OS +Thumbs.db diff --git a/README.md b/README.md new file mode 100644 index 0000000..c0b3014 --- /dev/null +++ b/README.md @@ -0,0 +1,143 @@ +# 智析反诈 — 受害人被骗金额归集智能体 + +面向电信诈骗案件受害人资金梳理的多APP账单智能归集与被骗金额核验系统。 + +## 功能概览 + +- **案件管理** — 创建、查看、管理诈骗案件 +- **截图上传** — 批量上传微信/支付宝/银行/数字钱包等多APP账单截图 +- **OCR识别** — 云端多模态自动识别页面类型、提取交易字段,支持人工修正 +- **交易归并** — 自动去重(订单号/金额+时间窗口)、识别本人账户中转 +- **资金分析** — 生成资金流转关系图、交易时间轴、收款方聚合 +- **认定复核** — 高/中/低置信分层,人工复核确认,自动生成认定理由 +- **笔录辅助** — 基于分析结果自动生成笔录问询建议 +- **报告导出** — Excel汇总表 / PDF报告 / Word文书,含证据索引和审计快照 + +## 技术栈 + +| 层级 | 技术 | +|------|------| +| 前端 | React 18 + TypeScript + Ant Design + ECharts + TanStack Query + Zustand | +| 后端 | Python + FastAPI + SQLAlchemy 2.x (async) + Pydantic v2 | +| 数据库 | PostgreSQL 16 | +| 队列 | Celery + Redis 7 | +| AI能力 | 云OCR / 多模态大模型API(可配置) | +| 报告 | openpyxl + python-docx | + +## 项目结构 + +``` +fund-tracer/ +├── frontend/ # React 前端 +│ ├── src/ +│ │ ├── pages/ # 7 个核心页面 +│ │ ├── services/ # API 服务层 +│ │ ├── store/ # Zustand 状态管理 +│ │ ├── mock/ # Mock 数据(后端不可用时自动降级) +│ │ └── types/ # TypeScript 类型定义 +│ └── package.json +├── backend/ # FastAPI 后端 +│ ├── app/ +│ │ ├── api/v1/ # REST API 路由 +│ │ ├── models/ # SQLAlchemy ORM 模型 +│ │ ├── schemas/ # Pydantic 请求/响应模型 +│ │ ├── services/ # 业务逻辑层 +│ │ ├── rules/ # 规则引擎(去重/中转/认定) +│ │ ├── workers/ # Celery 异步任务 +│ │ └── repositories/ # 数据访问层 +│ ├── alembic/ # 数据库迁移 +│ ├── scripts/ # 种子数据等脚本 +│ └── tests/ # pytest 测试 +├── infra/ +│ ├── docker/ # docker-compose (PG + Redis) +│ ├── env/ # 环境变量模板 +│ └── scripts/ # 一键启动脚本 +└── docs/ # 文档 +``` + +## 快速开始 + +### 前提条件 + +- Node.js >= 18 +- Python >= 3.11 +- Docker & Docker Compose + +### 一键启动 + +```bash +bash infra/scripts/start-dev.sh +``` + +### 手动启动 + +```bash +# 1. 启动基础设施 +cd infra/docker && docker compose up -d + +# 2. 安装并启动后端 +cd backend +pip install -e ".[dev]" +cp ../infra/env/.env.example .env # 按需编辑 +alembic revision --autogenerate -m "init" +alembic upgrade head +python -m scripts.seed # 插入演示数据 +uvicorn app.main:app --reload --port 8000 + +# 3. 安装并启动前端 +cd frontend +npm install +npm run dev +``` + +### 访问 + +- 前端: http://localhost:5173 +- 后端 API: http://localhost:8000 +- API 文档: http://localhost:8000/docs + +## 纯前端演示模式 + +即使没有后端服务,前端也能正常运行。系统会自动检测后端可用性,不可用时降级为内置 mock 数据驱动。 + +```bash +cd frontend && npm run dev +``` + +## 配置 AI 能力 + +在 `backend/.env` 中配置: + +``` +OCR_API_KEY=your_key +OCR_API_URL=https://api.example.com/v1/chat/completions +LLM_API_KEY=your_key +LLM_API_URL=https://api.example.com/v1/chat/completions +LLM_MODEL=model_name +``` + +支持 OpenAI 兼容格式的多模态 API。未配置时自动使用 mock 数据。 + +## 测试 + +```bash +cd backend +pytest tests/ -v +``` + +## API 端点一览 + +| 端点 | 方法 | 功能 | +|------|------|------| +| `/api/v1/cases` | GET/POST | 案件列表/创建 | +| `/api/v1/cases/{id}` | GET/PATCH | 案件详情/更新 | +| `/api/v1/cases/{id}/images` | GET/POST | 截图列表/上传 | +| `/api/v1/images/{id}` | GET | 截图详情+OCR结果 | +| `/api/v1/cases/{id}/analyze` | POST | 触发分析 | +| `/api/v1/cases/{id}/transactions` | GET | 交易列表 | +| `/api/v1/cases/{id}/flows` | GET | 资金流图 | +| `/api/v1/cases/{id}/assessments` | GET | 认定列表 | +| `/api/v1/assessments/{id}/review` | POST | 提交复核 | +| `/api/v1/cases/{id}/inquiry-suggestions` | GET | 问询建议 | +| `/api/v1/cases/{id}/reports` | GET/POST | 报告列表/生成 | +| `/api/v1/reports/{id}/download` | GET | 下载报告 | diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..99d38bf --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,6 @@ +DATABASE_URL=postgresql+asyncpg://fundtracer:fundtracer_dev@localhost:5432/fundtracer +DATABASE_URL_SYNC=postgresql://fundtracer:fundtracer_dev@localhost:5432/fundtracer +REDIS_URL=redis://localhost:6379/0 +UPLOAD_DIR=./uploads +SECRET_KEY=dev-secret-key +DEBUG=true diff --git a/backend/alembic.ini b/backend/alembic.ini new file mode 100644 index 0000000..5f5b45e --- /dev/null +++ b/backend/alembic.ini @@ -0,0 +1,36 @@ +[alembic] +script_location = alembic +sqlalchemy.url = postgresql://fundtracer:fundtracer_dev@localhost:5432/fundtracer + +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/backend/alembic/env.py b/backend/alembic/env.py new file mode 100644 index 0000000..3181e12 --- /dev/null +++ b/backend/alembic/env.py @@ -0,0 +1,41 @@ +from logging.config import fileConfig + +from sqlalchemy import engine_from_config, pool +from alembic import context + +from app.core.config import settings +from app.core.database import Base +import app.models # noqa: F401 – ensure all models are imported + +config = context.config +config.set_main_option("sqlalchemy.url", settings.DATABASE_URL_SYNC) + +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +target_metadata = Base.metadata + + +def run_migrations_offline(): + url = config.get_main_option("sqlalchemy.url") + context.configure(url=url, target_metadata=target_metadata, literal_binds=True) + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + connectable = engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + with connectable.connect() as connection: + context.configure(connection=connection, target_metadata=target_metadata) + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/backend/alembic/script.py.mako b/backend/alembic/script.py.mako new file mode 100644 index 0000000..590f5b3 --- /dev/null +++ b/backend/alembic/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} 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/v1/__init__.py b/backend/app/api/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/api/v1/analysis.py b/backend/app/api/v1/analysis.py new file mode 100644 index 0000000..6f6dee2 --- /dev/null +++ b/backend/app/api/v1/analysis.py @@ -0,0 +1,54 @@ +from uuid import UUID + +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.database import get_db +from app.repositories.case_repo import CaseRepository +from app.schemas.analysis import AnalysisStatusOut, AnalysisTriggerOut + +router = APIRouter() + + +@router.post("/cases/{case_id}/analyze", response_model=AnalysisTriggerOut) +async def trigger_analysis(case_id: UUID, db: AsyncSession = Depends(get_db)): + repo = CaseRepository(db) + case = await repo.get(case_id) + if not case: + raise HTTPException(404, "案件不存在") + + from app.workers.analysis_tasks import run_full_analysis + try: + task = run_full_analysis.delay(str(case_id)) + task_id = task.id + except Exception: + task_id = "sync-fallback" + from app.services.analysis_pipeline import run_analysis_sync + await run_analysis_sync(case_id, db) + + return AnalysisTriggerOut(task_id=task_id, message="分析任务已提交") + + +@router.get("/cases/{case_id}/analyze/status", response_model=AnalysisStatusOut) +async def analysis_status(case_id: UUID, db: AsyncSession = Depends(get_db)): + repo = CaseRepository(db) + case = await repo.get(case_id) + if not case: + raise HTTPException(404, "案件不存在") + + step_map = { + "pending": ("等待上传", 0), + "uploading": ("上传中", 15), + "analyzing": ("分析中", 50), + "reviewing": ("待复核", 85), + "completed": ("已完成", 100), + } + step_label, progress = step_map.get(case.status.value, ("未知", 0)) + + return AnalysisStatusOut( + case_id=str(case_id), + status=case.status.value, + progress=progress, + current_step=step_label, + message=f"当前状态: {step_label}", + ) diff --git a/backend/app/api/v1/assessments.py b/backend/app/api/v1/assessments.py new file mode 100644 index 0000000..7f1fa2f --- /dev/null +++ b/backend/app/api/v1/assessments.py @@ -0,0 +1,55 @@ +from uuid import UUID +from datetime import datetime, timezone + +from fastapi import APIRouter, Depends, Query, HTTPException +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.database import get_db +from app.models.assessment import ConfidenceLevel +from app.repositories.assessment_repo import AssessmentRepository +from app.schemas.assessment import ( + AssessmentOut, + AssessmentListOut, + ReviewSubmit, + InquirySuggestionOut, +) +from app.services.assessment_service import generate_inquiry_suggestions + +router = APIRouter() + + +@router.get("/cases/{case_id}/assessments", response_model=AssessmentListOut) +async def list_assessments( + case_id: UUID, + confidence_level: ConfidenceLevel | None = None, + db: AsyncSession = Depends(get_db), +): + repo = AssessmentRepository(db) + items, total = await repo.list_by_case(case_id, confidence_level=confidence_level) + return AssessmentListOut(items=items, total=total) + + +@router.post("/assessments/{assessment_id}/review", response_model=AssessmentOut) +async def review_assessment( + assessment_id: UUID, + body: ReviewSubmit, + db: AsyncSession = Depends(get_db), +): + repo = AssessmentRepository(db) + assessment = await repo.get(assessment_id) + if not assessment: + raise HTTPException(404, "认定记录不存在") + + assessment = await repo.update(assessment, { + "review_status": body.review_status, + "review_note": body.review_note, + "reviewed_by": body.reviewed_by, + "reviewed_at": datetime.now(timezone.utc), + }) + return assessment + + +@router.get("/cases/{case_id}/inquiry-suggestions", response_model=InquirySuggestionOut) +async def get_inquiry_suggestions(case_id: UUID, db: AsyncSession = Depends(get_db)): + suggestions = await generate_inquiry_suggestions(case_id, db) + return InquirySuggestionOut(suggestions=suggestions) diff --git a/backend/app/api/v1/cases.py b/backend/app/api/v1/cases.py new file mode 100644 index 0000000..5ee1839 --- /dev/null +++ b/backend/app/api/v1/cases.py @@ -0,0 +1,58 @@ +from uuid import UUID + +from fastapi import APIRouter, Depends, Query +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.database import get_db +from app.models.case import Case, CaseStatus +from app.repositories.case_repo import CaseRepository +from app.schemas.case import CaseCreate, CaseUpdate, CaseOut, CaseListOut + +router = APIRouter() + + +@router.post("", response_model=CaseOut, status_code=201) +async def create_case(body: CaseCreate, db: AsyncSession = Depends(get_db)): + repo = CaseRepository(db) + case = Case( + case_no=body.case_no, + title=body.title, + victim_name=body.victim_name, + handler=body.handler, + ) + case = await repo.create(case) + return case + + +@router.get("", response_model=CaseListOut) +async def list_cases( + offset: int = Query(0, ge=0), + limit: int = Query(50, ge=1, le=200), + status: CaseStatus | None = None, + search: str | None = None, + db: AsyncSession = Depends(get_db), +): + repo = CaseRepository(db) + items, total = await repo.list_cases(offset=offset, limit=limit, status=status, search=search) + return CaseListOut(items=items, total=total) + + +@router.get("/{case_id}", response_model=CaseOut) +async def get_case(case_id: UUID, db: AsyncSession = Depends(get_db)): + repo = CaseRepository(db) + case = await repo.get(case_id) + if not case: + from fastapi import HTTPException + raise HTTPException(status_code=404, detail="案件不存在") + return case + + +@router.patch("/{case_id}", response_model=CaseOut) +async def update_case(case_id: UUID, body: CaseUpdate, db: AsyncSession = Depends(get_db)): + repo = CaseRepository(db) + case = await repo.get(case_id) + if not case: + from fastapi import HTTPException + raise HTTPException(status_code=404, detail="案件不存在") + case = await repo.update(case, body.model_dump(exclude_unset=True)) + return case diff --git a/backend/app/api/v1/images.py b/backend/app/api/v1/images.py new file mode 100644 index 0000000..ca17f25 --- /dev/null +++ b/backend/app/api/v1/images.py @@ -0,0 +1,130 @@ +from uuid import UUID + +from fastapi import APIRouter, Depends, UploadFile, File, HTTPException, Query +from fastapi.responses import FileResponse +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.config import settings +from app.core.database import get_db +from app.models.evidence_image import EvidenceImage, SourceApp, PageType +from app.repositories.image_repo import ImageRepository +from app.repositories.case_repo import CaseRepository +from app.schemas.image import ImageOut, ImageDetailOut, OcrFieldCorrection +from app.utils.hash import sha256_file +from app.utils.file_storage import save_upload + +router = APIRouter() + + +@router.post("/cases/{case_id}/images", response_model=list[ImageOut], status_code=201) +async def upload_images( + case_id: UUID, + files: list[UploadFile] = File(...), + db: AsyncSession = Depends(get_db), +): + case_repo = CaseRepository(db) + case = await case_repo.get(case_id) + if not case: + raise HTTPException(404, "案件不存在") + + img_repo = ImageRepository(db) + results: list[EvidenceImage] = [] + + for f in files: + data = await f.read() + file_hash = sha256_file(data) + + existing = await img_repo.find_by_hash(file_hash) + if existing: + results.append(existing) + continue + + file_path, thumb_path = save_upload(data, str(case_id), f.filename or "upload.png") + image = EvidenceImage( + case_id=case_id, + file_path=file_path, + thumb_path=thumb_path, + file_hash=file_hash, + file_size=len(data), + ) + image = await img_repo.create(image) + results.append(image) + + case.image_count = await img_repo.count_by_case(case_id) + await db.flush() + + # trigger OCR tasks (non-blocking) + from app.workers.ocr_tasks import process_image_ocr + for img in results: + if img.ocr_status.value == "pending": + try: + process_image_ocr.delay(str(img.id)) + except Exception: + pass + + return results + + +@router.get("/cases/{case_id}/images", response_model=list[ImageOut]) +async def list_images( + case_id: UUID, + source_app: SourceApp | None = None, + page_type: PageType | None = None, + db: AsyncSession = Depends(get_db), +): + repo = ImageRepository(db) + return await repo.list_by_case(case_id, source_app=source_app, page_type=page_type) + + +@router.get("/images/{image_id}", response_model=ImageDetailOut) +async def get_image_detail(image_id: UUID, db: AsyncSession = Depends(get_db)): + repo = ImageRepository(db) + image = await repo.get(image_id) + if not image: + raise HTTPException(404, "截图不存在") + return ImageDetailOut( + id=image.id, + case_id=image.case_id, + url=f"/api/v1/images/{image.id}/file", + thumb_url=f"/api/v1/images/{image.id}/file", + source_app=image.source_app, + page_type=image.page_type, + ocr_status=image.ocr_status, + file_hash=image.file_hash, + uploaded_at=image.uploaded_at, + ocr_blocks=[ + { + "id": b.id, + "content": b.content, + "bbox": b.bbox, + "seq_order": b.seq_order, + "confidence": b.confidence, + } + for b in image.ocr_blocks + ], + ) + + +@router.patch("/images/{image_id}/ocr") +async def correct_ocr( + image_id: UUID, + corrections: list[OcrFieldCorrection], + db: AsyncSession = Depends(get_db), +): + repo = ImageRepository(db) + image = await repo.get(image_id) + if not image: + raise HTTPException(404, "截图不存在") + return {"message": "修正已保存", "corrections": len(corrections)} + + +@router.get("/images/{image_id}/file") +async def get_image_file(image_id: UUID, db: AsyncSession = Depends(get_db)): + repo = ImageRepository(db) + image = await repo.get(image_id) + if not image: + raise HTTPException(404, "截图不存在") + full_path = settings.upload_path / image.file_path + if not full_path.exists(): + raise HTTPException(404, "文件不存在") + return FileResponse(full_path) diff --git a/backend/app/api/v1/reports.py b/backend/app/api/v1/reports.py new file mode 100644 index 0000000..ee91646 --- /dev/null +++ b/backend/app/api/v1/reports.py @@ -0,0 +1,48 @@ +from uuid import UUID + +from fastapi import APIRouter, Depends, HTTPException +from fastapi.responses import FileResponse +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.config import settings +from app.core.database import get_db +from app.models.report import ExportReport +from app.repositories.case_repo import CaseRepository +from app.schemas.report import ReportCreate, ReportOut, ReportListOut + +router = APIRouter() + + +@router.post("/cases/{case_id}/reports", response_model=ReportOut, status_code=201) +async def generate_report(case_id: UUID, body: ReportCreate, db: AsyncSession = Depends(get_db)): + repo = CaseRepository(db) + case = await repo.get(case_id) + if not case: + raise HTTPException(404, "案件不存在") + + from app.services.report_service import generate_report as gen + report = await gen(case_id, body, db) + return report + + +@router.get("/cases/{case_id}/reports", response_model=ReportListOut) +async def list_reports(case_id: UUID, db: AsyncSession = Depends(get_db)): + result = await db.execute( + select(ExportReport) + .where(ExportReport.case_id == case_id) + .order_by(ExportReport.created_at.desc()) + ) + items = list(result.scalars().all()) + return ReportListOut(items=items, total=len(items)) + + +@router.get("/reports/{report_id}/download") +async def download_report(report_id: UUID, db: AsyncSession = Depends(get_db)): + report = await db.get(ExportReport, report_id) + if not report: + raise HTTPException(404, "报告不存在") + full_path = settings.upload_path / report.file_path + if not full_path.exists(): + raise HTTPException(404, "报告文件不存在") + return FileResponse(full_path, filename=full_path.name) diff --git a/backend/app/api/v1/router.py b/backend/app/api/v1/router.py new file mode 100644 index 0000000..e19d844 --- /dev/null +++ b/backend/app/api/v1/router.py @@ -0,0 +1,12 @@ +from fastapi import APIRouter + +from app.api.v1 import cases, images, analysis, transactions, assessments, reports + +api_router = APIRouter() + +api_router.include_router(cases.router, prefix="/cases", tags=["案件管理"]) +api_router.include_router(images.router, tags=["截图管理"]) +api_router.include_router(analysis.router, tags=["分析任务"]) +api_router.include_router(transactions.router, tags=["交易管理"]) +api_router.include_router(assessments.router, tags=["认定复核"]) +api_router.include_router(reports.router, tags=["报告导出"]) diff --git a/backend/app/api/v1/transactions.py b/backend/app/api/v1/transactions.py new file mode 100644 index 0000000..8c6e633 --- /dev/null +++ b/backend/app/api/v1/transactions.py @@ -0,0 +1,36 @@ +from uuid import UUID + +from fastapi import APIRouter, Depends, Query, HTTPException +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.database import get_db +from app.repositories.transaction_repo import TransactionRepository +from app.schemas.transaction import TransactionOut, TransactionListOut, FlowGraphOut +from app.services.flow_service import build_flow_graph + +router = APIRouter() + + +@router.get("/cases/{case_id}/transactions", response_model=TransactionListOut) +async def list_transactions( + case_id: UUID, + filter_type: str | None = Query(None, description="all / unique / duplicate"), + db: AsyncSession = Depends(get_db), +): + repo = TransactionRepository(db) + items, total = await repo.list_by_case(case_id, filter_type=filter_type) + return TransactionListOut(items=items, total=total) + + +@router.get("/transactions/{tx_id}", response_model=TransactionOut) +async def get_transaction(tx_id: UUID, db: AsyncSession = Depends(get_db)): + repo = TransactionRepository(db) + tx = await repo.get(tx_id) + if not tx: + raise HTTPException(404, "交易不存在") + return tx + + +@router.get("/cases/{case_id}/flows", response_model=FlowGraphOut) +async def get_fund_flows(case_id: UUID, db: AsyncSession = Depends(get_db)): + return await build_flow_graph(case_id, db) 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..6093ce3 --- /dev/null +++ b/backend/app/core/config.py @@ -0,0 +1,34 @@ +from pathlib import Path +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + model_config = SettingsConfigDict( + env_file=".env", + env_file_encoding="utf-8", + extra="ignore", + ) + + DATABASE_URL: str = "postgresql+asyncpg://fundtracer:fundtracer_dev@localhost:5432/fundtracer" + DATABASE_URL_SYNC: str = "postgresql://fundtracer:fundtracer_dev@localhost:5432/fundtracer" + REDIS_URL: str = "redis://localhost:6379/0" + + UPLOAD_DIR: str = "./uploads" + + OCR_API_KEY: str = "" + OCR_API_URL: str = "" + LLM_API_KEY: str = "" + LLM_API_URL: str = "" + LLM_MODEL: str = "" + + SECRET_KEY: str = "change-me-in-production" + DEBUG: bool = True + + @property + def upload_path(self) -> Path: + p = Path(self.UPLOAD_DIR) + p.mkdir(parents=True, exist_ok=True) + return p + + +settings = Settings() diff --git a/backend/app/core/database.py b/backend/app/core/database.py new file mode 100644 index 0000000..ad53fe2 --- /dev/null +++ b/backend/app/core/database.py @@ -0,0 +1,23 @@ +from collections.abc import AsyncGenerator + +from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine +from sqlalchemy.orm import DeclarativeBase + +from app.core.config import settings + +engine = create_async_engine(settings.DATABASE_URL, echo=settings.DEBUG, future=True) +async_session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) + + +class Base(DeclarativeBase): + pass + + +async def get_db() -> AsyncGenerator[AsyncSession, None]: + async with async_session_factory() as session: + try: + yield session + await session.commit() + except Exception: + await session.rollback() + raise diff --git a/backend/app/core/security.py b/backend/app/core/security.py new file mode 100644 index 0000000..b6dac36 --- /dev/null +++ b/backend/app/core/security.py @@ -0,0 +1,9 @@ +"""Placeholder for authentication & authorization. + +In the competition demo we skip real auth. This module reserves the +extension point so RBAC / JWT can be plugged in later. +""" + + +async def get_current_user() -> str: + return "demo_user" diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..56148a5 --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,36 @@ +from contextlib import asynccontextmanager + +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from app.api.v1.router import api_router +from app.core.config import settings + + +@asynccontextmanager +async def lifespan(app: FastAPI): + settings.upload_path # ensure upload dir exists + yield + + +app = FastAPI( + title="智析反诈", + description="受害人被骗金额归集智能体 API", + version="0.1.0", + lifespan=lifespan, +) + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +app.include_router(api_router, prefix="/api/v1") + + +@app.get("/health") +async def health(): + return {"status": "ok"} diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py new file mode 100644 index 0000000..fffc96a --- /dev/null +++ b/backend/app/models/__init__.py @@ -0,0 +1,19 @@ +from app.models.case import Case +from app.models.evidence_image import EvidenceImage +from app.models.ocr_block import OcrBlock +from app.models.transaction import TransactionRecord +from app.models.transaction_cluster import TransactionCluster +from app.models.fund_flow import FundFlowEdge +from app.models.assessment import FraudAssessment +from app.models.report import ExportReport + +__all__ = [ + "Case", + "EvidenceImage", + "OcrBlock", + "TransactionRecord", + "TransactionCluster", + "FundFlowEdge", + "FraudAssessment", + "ExportReport", +] diff --git a/backend/app/models/assessment.py b/backend/app/models/assessment.py new file mode 100644 index 0000000..5542b2c --- /dev/null +++ b/backend/app/models/assessment.py @@ -0,0 +1,42 @@ +import uuid +from datetime import datetime +import enum + +from sqlalchemy import String, Numeric, Text, DateTime, ForeignKey, Enum as SAEnum, func +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.core.database import Base + + +class ConfidenceLevel(str, enum.Enum): + high = "high" + medium = "medium" + low = "low" + + +class ReviewStatus(str, enum.Enum): + pending = "pending" + confirmed = "confirmed" + rejected = "rejected" + needs_info = "needs_info" + + +class FraudAssessment(Base): + __tablename__ = "fraud_assessments" + + id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + case_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("cases.id"), index=True) + transaction_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("transaction_records.id")) + confidence_level: Mapped[ConfidenceLevel] = mapped_column(SAEnum(ConfidenceLevel)) + assessed_amount: Mapped[float] = mapped_column(Numeric(14, 2), default=0) + reason: Mapped[str] = mapped_column(Text, default="") + exclude_reason: Mapped[str] = mapped_column(Text, default="") + review_status: Mapped[ReviewStatus] = mapped_column(SAEnum(ReviewStatus), default=ReviewStatus.pending) + review_note: Mapped[str] = mapped_column(Text, default="") + reviewed_by: Mapped[str] = mapped_column(String(128), default="") + reviewed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) + + case = relationship("Case", back_populates="assessments") + transaction = relationship("TransactionRecord") diff --git a/backend/app/models/case.py b/backend/app/models/case.py new file mode 100644 index 0000000..a397a42 --- /dev/null +++ b/backend/app/models/case.py @@ -0,0 +1,40 @@ +import uuid +from datetime import datetime + +from sqlalchemy import String, Numeric, Integer, DateTime, Enum as SAEnum, func +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.core.database import Base + +import enum + + +class CaseStatus(str, enum.Enum): + pending = "pending" + uploading = "uploading" + analyzing = "analyzing" + reviewing = "reviewing" + completed = "completed" + + +class Case(Base): + __tablename__ = "cases" + + id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + case_no: Mapped[str] = mapped_column(String(64), unique=True, index=True) + title: Mapped[str] = mapped_column(String(256)) + victim_name: Mapped[str] = mapped_column(String(128)) + handler: Mapped[str] = mapped_column(String(128), default="") + status: Mapped[CaseStatus] = mapped_column(SAEnum(CaseStatus), default=CaseStatus.pending) + image_count: Mapped[int] = mapped_column(Integer, default=0) + total_amount: Mapped[float] = mapped_column(Numeric(14, 2), default=0) + created_by: Mapped[str] = mapped_column(String(128), default="") + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) + updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + + images = relationship("EvidenceImage", back_populates="case", lazy="selectin") + transactions = relationship("TransactionRecord", back_populates="case", lazy="selectin") + assessments = relationship("FraudAssessment", back_populates="case", lazy="selectin") + reports = relationship("ExportReport", back_populates="case", lazy="selectin") diff --git a/backend/app/models/evidence_image.py b/backend/app/models/evidence_image.py new file mode 100644 index 0000000..449d9e7 --- /dev/null +++ b/backend/app/models/evidence_image.py @@ -0,0 +1,51 @@ +import uuid +from datetime import datetime +import enum + +from sqlalchemy import String, Integer, DateTime, ForeignKey, Enum as SAEnum, func +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.core.database import Base + + +class SourceApp(str, enum.Enum): + wechat = "wechat" + alipay = "alipay" + bank = "bank" + digital_wallet = "digital_wallet" + other = "other" + + +class PageType(str, enum.Enum): + bill_list = "bill_list" + bill_detail = "bill_detail" + transfer_receipt = "transfer_receipt" + sms_notice = "sms_notice" + balance = "balance" + unknown = "unknown" + + +class OcrStatus(str, enum.Enum): + pending = "pending" + processing = "processing" + done = "done" + failed = "failed" + + +class EvidenceImage(Base): + __tablename__ = "evidence_images" + + id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + case_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("cases.id"), index=True) + file_path: Mapped[str] = mapped_column(String(512)) + thumb_path: Mapped[str] = mapped_column(String(512), default="") + source_app: Mapped[SourceApp] = mapped_column(SAEnum(SourceApp), default=SourceApp.other) + page_type: Mapped[PageType] = mapped_column(SAEnum(PageType), default=PageType.unknown) + ocr_status: Mapped[OcrStatus] = mapped_column(SAEnum(OcrStatus), default=OcrStatus.pending) + file_hash: Mapped[str] = mapped_column(String(128), unique=True, index=True) + file_size: Mapped[int] = mapped_column(Integer, default=0) + uploaded_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) + + case = relationship("Case", back_populates="images") + ocr_blocks = relationship("OcrBlock", back_populates="image", lazy="selectin") diff --git a/backend/app/models/fund_flow.py b/backend/app/models/fund_flow.py new file mode 100644 index 0000000..0e10e8b --- /dev/null +++ b/backend/app/models/fund_flow.py @@ -0,0 +1,23 @@ +import uuid +from datetime import datetime + +from sqlalchemy import String, Numeric, Integer, DateTime, ForeignKey, func +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import Mapped, mapped_column + +from app.core.database import Base + + +class FundFlowEdge(Base): + __tablename__ = "fund_flow_edges" + + id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + case_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("cases.id"), index=True) + source_node: Mapped[str] = mapped_column(String(256)) + target_node: Mapped[str] = mapped_column(String(256)) + source_type: Mapped[str] = mapped_column(String(32), default="unknown") + target_type: Mapped[str] = mapped_column(String(32), default="unknown") + amount: Mapped[float] = mapped_column(Numeric(14, 2), default=0) + tx_count: Mapped[int] = mapped_column(Integer, default=1) + earliest_time: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) diff --git a/backend/app/models/ocr_block.py b/backend/app/models/ocr_block.py new file mode 100644 index 0000000..8c850d8 --- /dev/null +++ b/backend/app/models/ocr_block.py @@ -0,0 +1,20 @@ +import uuid + +from sqlalchemy import String, Integer, Float, Text, ForeignKey +from sqlalchemy.dialects.postgresql import UUID, JSONB +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.core.database import Base + + +class OcrBlock(Base): + __tablename__ = "ocr_blocks" + + id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + image_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("evidence_images.id"), index=True) + content: Mapped[str] = mapped_column(Text, default="") + bbox: Mapped[dict] = mapped_column(JSONB, default=dict) + seq_order: Mapped[int] = mapped_column(Integer, default=0) + confidence: Mapped[float] = mapped_column(Float, default=0.0) + + image = relationship("EvidenceImage", back_populates="ocr_blocks") diff --git a/backend/app/models/report.py b/backend/app/models/report.py new file mode 100644 index 0000000..e24b460 --- /dev/null +++ b/backend/app/models/report.py @@ -0,0 +1,29 @@ +import uuid +from datetime import datetime +import enum + +from sqlalchemy import String, Integer, DateTime, ForeignKey, Enum as SAEnum, func +from sqlalchemy.dialects.postgresql import UUID, JSONB +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.core.database import Base + + +class ReportType(str, enum.Enum): + pdf = "pdf" + excel = "excel" + word = "word" + + +class ExportReport(Base): + __tablename__ = "export_reports" + + id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + case_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("cases.id"), index=True) + report_type: Mapped[ReportType] = mapped_column(SAEnum(ReportType)) + file_path: Mapped[str] = mapped_column(String(512), default="") + version: Mapped[int] = mapped_column(Integer, default=1) + content_snapshot: Mapped[dict] = mapped_column(JSONB, default=dict) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) + + case = relationship("Case", back_populates="reports") diff --git a/backend/app/models/transaction.py b/backend/app/models/transaction.py new file mode 100644 index 0000000..87d2b32 --- /dev/null +++ b/backend/app/models/transaction.py @@ -0,0 +1,43 @@ +import uuid +from datetime import datetime +import enum + +from sqlalchemy import ( + String, Numeric, Float, Boolean, DateTime, Text, + ForeignKey, Enum as SAEnum, func, +) +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.core.database import Base +from app.models.evidence_image import SourceApp + + +class Direction(str, enum.Enum): + in_ = "in" + out = "out" + + +class TransactionRecord(Base): + __tablename__ = "transaction_records" + + id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + case_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("cases.id"), index=True) + evidence_image_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("evidence_images.id"), nullable=True) + cluster_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("transaction_clusters.id"), nullable=True) + source_app: Mapped[SourceApp] = mapped_column(SAEnum(SourceApp), default=SourceApp.other) + trade_time: Mapped[datetime] = mapped_column(DateTime(timezone=True), index=True) + amount: Mapped[float] = mapped_column(Numeric(14, 2), default=0) + direction: Mapped[Direction] = mapped_column(SAEnum(Direction)) + counterparty_name: Mapped[str] = mapped_column(String(256), default="") + counterparty_account: Mapped[str] = mapped_column(String(256), default="") + self_account_tail_no: Mapped[str] = mapped_column(String(32), default="") + order_no: Mapped[str] = mapped_column(String(128), default="", index=True) + remark: Mapped[str] = mapped_column(Text, default="") + confidence: Mapped[float] = mapped_column(Float, default=0.0) + is_duplicate: Mapped[bool] = mapped_column(Boolean, default=False) + is_transit: Mapped[bool] = mapped_column(Boolean, default=False) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) + + case = relationship("Case", back_populates="transactions") + cluster = relationship("TransactionCluster", back_populates="transactions", foreign_keys=[cluster_id]) diff --git a/backend/app/models/transaction_cluster.py b/backend/app/models/transaction_cluster.py new file mode 100644 index 0000000..e7acd6b --- /dev/null +++ b/backend/app/models/transaction_cluster.py @@ -0,0 +1,22 @@ +import uuid + +from sqlalchemy import String, ForeignKey +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.core.database import Base + + +class TransactionCluster(Base): + __tablename__ = "transaction_clusters" + + id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + case_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("cases.id"), index=True) + primary_tx_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), nullable=True) + match_reason: Mapped[str] = mapped_column(String(512), default="") + + transactions = relationship( + "TransactionRecord", + back_populates="cluster", + foreign_keys="TransactionRecord.cluster_id", + ) 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/repositories/assessment_repo.py b/backend/app/repositories/assessment_repo.py new file mode 100644 index 0000000..941417f --- /dev/null +++ b/backend/app/repositories/assessment_repo.py @@ -0,0 +1,34 @@ +from uuid import UUID + +from sqlalchemy import select, func +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import selectinload + +from app.models.assessment import FraudAssessment, ConfidenceLevel +from app.repositories.base import BaseRepository + + +class AssessmentRepository(BaseRepository[FraudAssessment]): + def __init__(self, session: AsyncSession): + super().__init__(FraudAssessment, session) + + async def list_by_case( + self, + case_id: UUID, + confidence_level: ConfidenceLevel | None = None, + ) -> tuple[list[FraudAssessment], int]: + query = ( + select(FraudAssessment) + .options(selectinload(FraudAssessment.transaction)) + .where(FraudAssessment.case_id == case_id) + ) + count_q = select(func.count()).select_from(FraudAssessment).where(FraudAssessment.case_id == case_id) + + if confidence_level: + query = query.where(FraudAssessment.confidence_level == confidence_level) + count_q = count_q.where(FraudAssessment.confidence_level == confidence_level) + + total = (await self.session.execute(count_q)).scalar() or 0 + query = query.order_by(FraudAssessment.created_at.asc()) + result = await self.session.execute(query) + return list(result.scalars().all()), total diff --git a/backend/app/repositories/base.py b/backend/app/repositories/base.py new file mode 100644 index 0000000..af5f5b6 --- /dev/null +++ b/backend/app/repositories/base.py @@ -0,0 +1,50 @@ +from typing import TypeVar, Generic, Type +from uuid import UUID + +from sqlalchemy import select, func +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.database import Base + +ModelT = TypeVar("ModelT", bound=Base) + + +class BaseRepository(Generic[ModelT]): + def __init__(self, model: Type[ModelT], session: AsyncSession): + self.model = model + self.session = session + + async def get(self, id: UUID) -> ModelT | None: + return await self.session.get(self.model, id) + + async def list(self, offset: int = 0, limit: int = 50, **filters) -> tuple[list[ModelT], int]: + query = select(self.model) + count_query = select(func.count()).select_from(self.model) + + for attr, value in filters.items(): + if value is not None and hasattr(self.model, attr): + query = query.where(getattr(self.model, attr) == value) + count_query = count_query.where(getattr(self.model, attr) == value) + + total = (await self.session.execute(count_query)).scalar() or 0 + query = query.offset(offset).limit(limit) + result = await self.session.execute(query) + return list(result.scalars().all()), total + + async def create(self, obj: ModelT) -> ModelT: + self.session.add(obj) + await self.session.flush() + await self.session.refresh(obj) + return obj + + async def update(self, obj: ModelT, data: dict) -> ModelT: + for key, value in data.items(): + if value is not None: + setattr(obj, key, value) + await self.session.flush() + await self.session.refresh(obj) + return obj + + async def delete(self, obj: ModelT) -> None: + await self.session.delete(obj) + await self.session.flush() diff --git a/backend/app/repositories/case_repo.py b/backend/app/repositories/case_repo.py new file mode 100644 index 0000000..5ada9fa --- /dev/null +++ b/backend/app/repositories/case_repo.py @@ -0,0 +1,35 @@ +from sqlalchemy import select, func, or_ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models.case import Case, CaseStatus +from app.repositories.base import BaseRepository + + +class CaseRepository(BaseRepository[Case]): + def __init__(self, session: AsyncSession): + super().__init__(Case, session) + + async def list_cases( + self, + offset: int = 0, + limit: int = 50, + status: CaseStatus | None = None, + search: str | None = None, + ) -> tuple[list[Case], int]: + query = select(Case).where(Case.deleted_at.is_(None)) + count_query = select(func.count()).select_from(Case).where(Case.deleted_at.is_(None)) + + if status: + query = query.where(Case.status == status) + count_query = count_query.where(Case.status == status) + + if search: + pattern = f"%{search}%" + search_filter = or_(Case.case_no.ilike(pattern), Case.title.ilike(pattern)) + query = query.where(search_filter) + count_query = count_query.where(search_filter) + + total = (await self.session.execute(count_query)).scalar() or 0 + query = query.order_by(Case.created_at.desc()).offset(offset).limit(limit) + result = await self.session.execute(query) + return list(result.scalars().all()), total diff --git a/backend/app/repositories/image_repo.py b/backend/app/repositories/image_repo.py new file mode 100644 index 0000000..b277bda --- /dev/null +++ b/backend/app/repositories/image_repo.py @@ -0,0 +1,39 @@ +from uuid import UUID + +from sqlalchemy import select, func +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models.evidence_image import EvidenceImage, SourceApp, PageType +from app.repositories.base import BaseRepository + + +class ImageRepository(BaseRepository[EvidenceImage]): + def __init__(self, session: AsyncSession): + super().__init__(EvidenceImage, session) + + async def find_by_hash(self, file_hash: str) -> EvidenceImage | None: + result = await self.session.execute( + select(EvidenceImage).where(EvidenceImage.file_hash == file_hash) + ) + return result.scalar_one_or_none() + + async def list_by_case( + self, + case_id: UUID, + source_app: SourceApp | None = None, + page_type: PageType | None = None, + ) -> list[EvidenceImage]: + query = select(EvidenceImage).where(EvidenceImage.case_id == case_id) + if source_app: + query = query.where(EvidenceImage.source_app == source_app) + if page_type: + query = query.where(EvidenceImage.page_type == page_type) + query = query.order_by(EvidenceImage.uploaded_at.desc()) + result = await self.session.execute(query) + return list(result.scalars().all()) + + async def count_by_case(self, case_id: UUID) -> int: + result = await self.session.execute( + select(func.count()).select_from(EvidenceImage).where(EvidenceImage.case_id == case_id) + ) + return result.scalar() or 0 diff --git a/backend/app/repositories/transaction_repo.py b/backend/app/repositories/transaction_repo.py new file mode 100644 index 0000000..884cd22 --- /dev/null +++ b/backend/app/repositories/transaction_repo.py @@ -0,0 +1,40 @@ +from uuid import UUID + +from sqlalchemy import select, func +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models.transaction import TransactionRecord +from app.repositories.base import BaseRepository + + +class TransactionRepository(BaseRepository[TransactionRecord]): + def __init__(self, session: AsyncSession): + super().__init__(TransactionRecord, session) + + async def list_by_case( + self, + case_id: UUID, + filter_type: str | None = None, + ) -> tuple[list[TransactionRecord], int]: + query = select(TransactionRecord).where(TransactionRecord.case_id == case_id) + count_q = select(func.count()).select_from(TransactionRecord).where(TransactionRecord.case_id == case_id) + + if filter_type == "unique": + query = query.where(TransactionRecord.is_duplicate.is_(False)) + count_q = count_q.where(TransactionRecord.is_duplicate.is_(False)) + elif filter_type == "duplicate": + query = query.where(TransactionRecord.is_duplicate.is_(True)) + count_q = count_q.where(TransactionRecord.is_duplicate.is_(True)) + + total = (await self.session.execute(count_q)).scalar() or 0 + query = query.order_by(TransactionRecord.trade_time.asc()) + result = await self.session.execute(query) + return list(result.scalars().all()), total + + async def get_all_by_case(self, case_id: UUID) -> list[TransactionRecord]: + result = await self.session.execute( + select(TransactionRecord) + .where(TransactionRecord.case_id == case_id) + .order_by(TransactionRecord.trade_time.asc()) + ) + return list(result.scalars().all()) diff --git a/backend/app/rules/__init__.py b/backend/app/rules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/rules/assessment_rules.py b/backend/app/rules/assessment_rules.py new file mode 100644 index 0000000..930b132 --- /dev/null +++ b/backend/app/rules/assessment_rules.py @@ -0,0 +1,57 @@ +"""Rule-based fraud amount assessment. + +Classifies each transaction into high / medium / low confidence fraud, +and generates initial reason text. +""" +from app.models.transaction import TransactionRecord +from app.models.assessment import ConfidenceLevel + +FRAUD_KEYWORDS = ["投资", "保证金", "手续费", "解冻", "税费", "充值", "提币", "提现"] + + +def classify_transaction(tx: TransactionRecord) -> tuple[ConfidenceLevel, str, str]: + """Return (confidence_level, reason, exclude_reason).""" + if tx.is_transit: + return ( + ConfidenceLevel.low, + f"该笔为本人账户间中转({tx.source_app.value} -> {tx.counterparty_name}),不直接计入被骗损失。", + "本人账户间互转,仅作为资金路径展示。", + ) + + if tx.direction.value == "in": + return ( + ConfidenceLevel.low, + f"该笔为收入方向交易(+¥{float(tx.amount):,.2f}),通常不属于被骗损失。", + "收入交易不计入损失。", + ) + + remark = tx.remark or "" + counterparty = tx.counterparty_name or "" + confidence = tx.confidence + + has_fraud_keyword = any(kw in remark or kw in counterparty for kw in FRAUD_KEYWORDS) + + if confidence >= 0.9 and has_fraud_keyword: + return ( + ConfidenceLevel.high, + f"受害人经{tx.source_app.value}向「{counterparty}」转账¥{float(tx.amount):,.2f}," + f"备注为「{remark}」,与诈骗常见话术吻合,OCR置信度{confidence:.0%}。", + "", + ) + + if confidence >= 0.85: + reason = ( + f"受害人经{tx.source_app.value}向「{counterparty}」转账¥{float(tx.amount):,.2f}。" + ) + if has_fraud_keyword: + reason += f"备注「{remark}」含涉诈关键词。" + return ConfidenceLevel.high, reason, "" + reason += "建议结合笔录确认是否受诱导操作。" + return ConfidenceLevel.medium, reason, "如经核实该笔为受害人主动日常消费,应排除。" + + return ( + ConfidenceLevel.medium, + f"受害人经{tx.source_app.value}向「{counterparty}」转账¥{float(tx.amount):,.2f}," + f"OCR置信度较低({confidence:.0%}),需人工复核。", + "OCR置信度不足,可能存在识别误差。", + ) diff --git a/backend/app/rules/dedup_rules.py b/backend/app/rules/dedup_rules.py new file mode 100644 index 0000000..1af81f4 --- /dev/null +++ b/backend/app/rules/dedup_rules.py @@ -0,0 +1,32 @@ +"""Transaction deduplication rules. + +Determines whether two transaction records likely represent the same +underlying financial event captured from different screenshots / pages. +""" +from datetime import timedelta + +from app.models.transaction import TransactionRecord + +TIME_WINDOW = timedelta(minutes=5) + + +def is_duplicate_pair(a: TransactionRecord, b: TransactionRecord) -> bool: + # Rule 1: exact order_no match + if a.order_no and b.order_no and a.order_no == b.order_no: + return True + + # Rule 2: same amount + close time + same account tail + if ( + float(a.amount) == float(b.amount) + and a.trade_time + and b.trade_time + and abs(a.trade_time - b.trade_time) <= TIME_WINDOW + ): + if a.self_account_tail_no and b.self_account_tail_no: + if a.self_account_tail_no == b.self_account_tail_no: + return True + # same counterparty and close time is also strong signal + if a.counterparty_name and a.counterparty_name == b.counterparty_name: + return True + + return False diff --git a/backend/app/rules/transit_rules.py b/backend/app/rules/transit_rules.py new file mode 100644 index 0000000..8b3ac06 --- /dev/null +++ b/backend/app/rules/transit_rules.py @@ -0,0 +1,35 @@ +"""Transit (self-transfer) detection rules. + +Identifies transactions that are internal transfers between the victim's +own accounts (e.g. bank -> Alipay -> WeChat) and should NOT be counted +as fraud loss. +""" +from app.models.transaction import TransactionRecord + +SELF_KEYWORDS = ["本人", "自己", "余额", "充值", "提现", "银行卡转入", "银行卡充值"] + + +def is_self_transfer(tx: TransactionRecord, known_self_accounts: list[str]) -> bool: + """Check if a transaction is an inter-account transfer by the victim.""" + counterparty = (tx.counterparty_name or "").lower() + remark = (tx.remark or "").lower() + + # Rule 1: counterparty matches known self accounts + for acct in known_self_accounts: + if acct and acct.lower() in counterparty: + return True + + # Rule 2: counterparty contains self-transfer keywords + for kw in SELF_KEYWORDS: + if kw in counterparty or kw in remark: + return True + + # Rule 3: counterparty references another payment app owned by victim + app_keywords = ["支付宝", "微信", "银行卡", "数字钱包"] + victim_patterns = [f"{app}-" for app in app_keywords] + app_keywords + for pat in victim_patterns: + if pat in counterparty: + if tx.direction.value == "out": + return True + + return False 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/schemas/analysis.py b/backend/app/schemas/analysis.py new file mode 100644 index 0000000..4aacbe6 --- /dev/null +++ b/backend/app/schemas/analysis.py @@ -0,0 +1,14 @@ +from pydantic import BaseModel + + +class AnalysisStatusOut(BaseModel): + case_id: str + status: str + progress: int = 0 + current_step: str = "" + message: str = "" + + +class AnalysisTriggerOut(BaseModel): + task_id: str + message: str diff --git a/backend/app/schemas/assessment.py b/backend/app/schemas/assessment.py new file mode 100644 index 0000000..3ac8dc7 --- /dev/null +++ b/backend/app/schemas/assessment.py @@ -0,0 +1,39 @@ +from datetime import datetime +from uuid import UUID + +from pydantic import BaseModel + +from app.models.assessment import ConfidenceLevel, ReviewStatus +from app.schemas.transaction import TransactionOut + + +class AssessmentOut(BaseModel): + id: UUID + case_id: UUID + transaction_id: UUID + transaction: TransactionOut | None = None + confidence_level: ConfidenceLevel + assessed_amount: float + reason: str + exclude_reason: str + review_status: ReviewStatus + review_note: str + reviewed_by: str + reviewed_at: datetime | None = None + + model_config = {"from_attributes": True} + + +class AssessmentListOut(BaseModel): + items: list[AssessmentOut] + total: int + + +class ReviewSubmit(BaseModel): + review_status: ReviewStatus + review_note: str = "" + reviewed_by: str = "demo_user" + + +class InquirySuggestionOut(BaseModel): + suggestions: list[str] diff --git a/backend/app/schemas/case.py b/backend/app/schemas/case.py new file mode 100644 index 0000000..50d4584 --- /dev/null +++ b/backend/app/schemas/case.py @@ -0,0 +1,40 @@ +from datetime import datetime +from uuid import UUID + +from pydantic import BaseModel + +from app.models.case import CaseStatus + + +class CaseCreate(BaseModel): + case_no: str + title: str + victim_name: str + handler: str = "" + + +class CaseUpdate(BaseModel): + title: str | None = None + victim_name: str | None = None + handler: str | None = None + status: CaseStatus | None = None + + +class CaseOut(BaseModel): + id: UUID + case_no: str + title: str + victim_name: str + handler: str + status: CaseStatus + image_count: int + total_amount: float + created_at: datetime + updated_at: datetime + + model_config = {"from_attributes": True} + + +class CaseListOut(BaseModel): + items: list[CaseOut] + total: int diff --git a/backend/app/schemas/image.py b/backend/app/schemas/image.py new file mode 100644 index 0000000..c3d80c7 --- /dev/null +++ b/backend/app/schemas/image.py @@ -0,0 +1,40 @@ +from datetime import datetime +from uuid import UUID + +from pydantic import BaseModel + +from app.models.evidence_image import SourceApp, PageType, OcrStatus + + +class ImageOut(BaseModel): + id: UUID + case_id: UUID + url: str = "" + thumb_url: str = "" + source_app: SourceApp + page_type: PageType + ocr_status: OcrStatus + file_hash: str + uploaded_at: datetime + + model_config = {"from_attributes": True} + + +class OcrBlockOut(BaseModel): + id: UUID + content: str + bbox: dict + seq_order: int + confidence: float + + model_config = {"from_attributes": True} + + +class ImageDetailOut(ImageOut): + ocr_blocks: list[OcrBlockOut] = [] + + +class OcrFieldCorrection(BaseModel): + field_name: str + old_value: str + new_value: str diff --git a/backend/app/schemas/report.py b/backend/app/schemas/report.py new file mode 100644 index 0000000..1fc2f6e --- /dev/null +++ b/backend/app/schemas/report.py @@ -0,0 +1,33 @@ +from datetime import datetime +from uuid import UUID + +from pydantic import BaseModel + +from app.models.report import ReportType + + +class ReportCreate(BaseModel): + report_type: ReportType + include_summary: bool = True + include_transactions: bool = True + include_flow_chart: bool = True + include_timeline: bool = True + include_reasons: bool = True + include_inquiry: bool = False + include_screenshots: bool = False + + +class ReportOut(BaseModel): + id: UUID + case_id: UUID + report_type: ReportType + file_path: str + version: int + created_at: datetime + + model_config = {"from_attributes": True} + + +class ReportListOut(BaseModel): + items: list[ReportOut] + total: int diff --git a/backend/app/schemas/transaction.py b/backend/app/schemas/transaction.py new file mode 100644 index 0000000..070f224 --- /dev/null +++ b/backend/app/schemas/transaction.py @@ -0,0 +1,52 @@ +from datetime import datetime +from uuid import UUID + +from pydantic import BaseModel + +from app.models.evidence_image import SourceApp +from app.models.transaction import Direction + + +class TransactionOut(BaseModel): + id: UUID + case_id: UUID + source_app: SourceApp + trade_time: datetime + amount: float + direction: Direction + counterparty_name: str + counterparty_account: str + self_account_tail_no: str + order_no: str + remark: str + evidence_image_id: UUID | None = None + confidence: float + cluster_id: UUID | None = None + is_duplicate: bool + is_transit: bool + + model_config = {"from_attributes": True} + + +class TransactionListOut(BaseModel): + items: list[TransactionOut] + total: int + + +class FlowNodeOut(BaseModel): + id: str + label: str + type: str + + +class FlowEdgeOut(BaseModel): + source: str + target: str + amount: float + count: int + trade_time: str + + +class FlowGraphOut(BaseModel): + nodes: list[FlowNodeOut] + edges: list[FlowEdgeOut] 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/services/analysis_pipeline.py b/backend/app/services/analysis_pipeline.py new file mode 100644 index 0000000..395bf94 --- /dev/null +++ b/backend/app/services/analysis_pipeline.py @@ -0,0 +1,42 @@ +"""Orchestrates the full analysis pipeline: matching -> flow -> assessment.""" +from uuid import UUID + +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models.case import Case, CaseStatus +from app.services.matching_service import run_matching +from app.services.assessment_service import assess_case +from app.services.case_service import recalculate_case_total + + +async def run_analysis_sync(case_id: UUID, db: AsyncSession) -> None: + """Run the full analysis pipeline synchronously (fallback when Celery is down).""" + case = await db.get(Case, case_id) + if not case: + return + + case.status = CaseStatus.analyzing + await db.flush() + + # Step 1: Matching & dedup + self_accounts = _extract_self_accounts(case) + await run_matching(case_id, self_accounts, db) + + # Step 2: Assessment + await assess_case(case_id, db) + + # Step 3: Recalculate total + await recalculate_case_total(case_id, db) + + case.status = CaseStatus.reviewing + await db.flush() + + +def _extract_self_accounts(case: Case) -> list[str]: + """Extract known self-account identifiers from case context. + + In a full implementation this would come from user input or a + dedicated 'victim accounts' table. For now we return an empty list + and rely on heuristic rules. + """ + return [] diff --git a/backend/app/services/assessment_service.py b/backend/app/services/assessment_service.py new file mode 100644 index 0000000..93065a0 --- /dev/null +++ b/backend/app/services/assessment_service.py @@ -0,0 +1,150 @@ +"""Fraud amount assessment and inquiry suggestion generation.""" +import logging +from uuid import UUID + +import httpx +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.config import settings +from app.models.transaction import TransactionRecord +from app.models.assessment import FraudAssessment, ConfidenceLevel, ReviewStatus +from app.rules.assessment_rules import classify_transaction + +logger = logging.getLogger(__name__) + + +async def assess_case(case_id: UUID, db: AsyncSession) -> list[FraudAssessment]: + """Run rule-based assessment on all non-duplicate transactions and generate reasons.""" + result = await db.execute( + select(TransactionRecord) + .where(TransactionRecord.case_id == case_id) + .where(TransactionRecord.is_duplicate.is_(False)) + .order_by(TransactionRecord.trade_time.asc()) + ) + transactions = list(result.scalars().all()) + + assessments: list[FraudAssessment] = [] + for tx in transactions: + level, reason, exclude_reason = classify_transaction(tx) + + fa = FraudAssessment( + case_id=case_id, + transaction_id=tx.id, + confidence_level=level, + assessed_amount=float(tx.amount) if level != ConfidenceLevel.low else 0, + reason=reason, + exclude_reason=exclude_reason, + review_status=ReviewStatus.pending, + ) + db.add(fa) + assessments.append(fa) + + await db.flush() + + # try to enhance reasons via LLM + if settings.LLM_API_KEY and settings.LLM_API_URL: + for fa in assessments: + try: + enhanced = await _enhance_reason_via_llm(fa, transactions) + if enhanced: + fa.reason = enhanced + except Exception as e: + logger.debug("LLM reason enhancement skipped: %s", e) + await db.flush() + + return assessments + + +async def generate_inquiry_suggestions(case_id: UUID, db: AsyncSession) -> list[str]: + """Generate interview / inquiry suggestions based on assessment results.""" + result = await db.execute( + select(FraudAssessment) + .where(FraudAssessment.case_id == case_id) + .order_by(FraudAssessment.created_at.asc()) + ) + assessments = list(result.scalars().all()) + + if not assessments: + return ["暂无分析结果,请先执行案件分析。"] + + # try LLM generation + if settings.LLM_API_KEY and settings.LLM_API_URL: + try: + return await _generate_suggestions_via_llm(assessments) + except Exception as e: + logger.debug("LLM suggestions skipped: %s", e) + + return _generate_suggestions_rule_based(assessments) + + +def _generate_suggestions_rule_based(assessments: list[FraudAssessment]) -> list[str]: + suggestions: list[str] = [] + pending = [a for a in assessments if a.review_status == ReviewStatus.pending] + medium = [a for a in assessments if a.confidence_level == ConfidenceLevel.medium] + + if pending: + suggestions.append( + f"有 {len(pending)} 笔交易尚未确认,建议逐笔向受害人核实是否受到诱导操作。" + ) + if medium: + suggestions.append( + "部分交易置信度为中等,建议追问受害人交易的具体背景和对方的诱导话术。" + ) + suggestions.append("是否还有其他未截图的转账记录或 APP 需要补充?") + suggestions.append("涉案金额中是否有已部分追回或返还的款项?") + suggestions.append( + "除了截图所示的 APP 外,是否还存在银行柜台、ATM、其他支付平台等转账渠道?" + ) + return suggestions + + +async def _enhance_reason_via_llm(fa: FraudAssessment, all_tx: list) -> str | None: + prompt = ( + f"这笔交易金额{fa.assessed_amount}元,置信等级{fa.confidence_level.value}。" + f"原始认定理由:{fa.reason}。" + "请用简洁中文优化认定理由表述,使之适合出现在办案文书中。只返回优化后的理由文字。" + ) + async with httpx.AsyncClient(timeout=15) as client: + resp = await client.post( + settings.LLM_API_URL, + headers={"Authorization": f"Bearer {settings.LLM_API_KEY}"}, + json={ + "model": settings.LLM_MODEL, + "messages": [{"role": "user", "content": prompt}], + "max_tokens": 300, + }, + ) + resp.raise_for_status() + return resp.json()["choices"][0]["message"]["content"].strip() + + +async def _generate_suggestions_via_llm(assessments: list[FraudAssessment]) -> list[str]: + summary_lines = [] + for a in assessments: + summary_lines.append( + f"- 金额{a.assessed_amount}元, 置信{a.confidence_level.value}, " + f"状态{a.review_status.value}, 理由: {a.reason[:60]}" + ) + summary = "\n".join(summary_lines) + + prompt = ( + "你是一名反诈案件办案助手。以下是某诈骗案件的交易认定摘要:\n" + f"{summary}\n\n" + "请生成5条笔录辅助问询建议,帮助民警追问受害人以完善证据链。" + "只返回JSON数组格式的5个字符串。" + ) + import json + async with httpx.AsyncClient(timeout=20) as client: + resp = await client.post( + settings.LLM_API_URL, + headers={"Authorization": f"Bearer {settings.LLM_API_KEY}"}, + json={ + "model": settings.LLM_MODEL, + "messages": [{"role": "user", "content": prompt}], + "max_tokens": 600, + }, + ) + resp.raise_for_status() + text = resp.json()["choices"][0]["message"]["content"].strip() + return json.loads(text.strip().strip("`").removeprefix("json").strip()) diff --git a/backend/app/services/case_service.py b/backend/app/services/case_service.py new file mode 100644 index 0000000..17cbd1c --- /dev/null +++ b/backend/app/services/case_service.py @@ -0,0 +1,23 @@ +from uuid import UUID +from decimal import Decimal + +from sqlalchemy import select, func +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models.case import Case +from app.models.assessment import FraudAssessment, ReviewStatus + + +async def recalculate_case_total(case_id: UUID, db: AsyncSession) -> float: + """Recalculate and persist the total confirmed fraud amount for a case.""" + result = await db.execute( + select(func.coalesce(func.sum(FraudAssessment.assessed_amount), 0)) + .where(FraudAssessment.case_id == case_id) + .where(FraudAssessment.review_status == ReviewStatus.confirmed) + ) + total = float(result.scalar() or 0) + case = await db.get(Case, case_id) + if case: + case.total_amount = total + await db.flush() + return total diff --git a/backend/app/services/flow_service.py b/backend/app/services/flow_service.py new file mode 100644 index 0000000..a86d9ff --- /dev/null +++ b/backend/app/services/flow_service.py @@ -0,0 +1,72 @@ +"""Build the fund-flow graph from deduplicated transactions.""" +from uuid import UUID +from collections import defaultdict + +from sqlalchemy.ext.asyncio import AsyncSession + +from app.repositories.transaction_repo import TransactionRepository +from app.schemas.transaction import FlowGraphOut, FlowNodeOut, FlowEdgeOut + + +async def build_flow_graph(case_id: UUID, db: AsyncSession) -> FlowGraphOut: + repo = TransactionRepository(db) + transactions = await repo.get_all_by_case(case_id) + + valid = [tx for tx in transactions if not tx.is_duplicate] + + nodes_map: dict[str, str] = {} # label -> type + edge_agg: dict[tuple[str, str], dict] = defaultdict( + lambda: {"amount": 0.0, "count": 0, "trade_time": ""} + ) + + for tx in valid: + self_label = _self_label(tx) + counter_label = tx.counterparty_name or "未知对手方" + + if self_label not in nodes_map: + nodes_map[self_label] = "self" + + if counter_label not in nodes_map: + nodes_map[counter_label] = "suspect" if not tx.is_transit else "transit" + + if tx.direction.value == "out": + key = (self_label, counter_label) + else: + key = (counter_label, self_label) + + edge_agg[key]["amount"] += float(tx.amount) + edge_agg[key]["count"] += 1 + time_str = tx.trade_time.strftime("%Y-%m-%d %H:%M") if tx.trade_time else "" + if not edge_agg[key]["trade_time"]: + edge_agg[key]["trade_time"] = time_str + + nodes = [ + FlowNodeOut(id=f"n-{i}", label=label, type=ntype) + for i, (label, ntype) in enumerate(nodes_map.items()) + ] + + label_to_id = {n.label: n.id for n in nodes} + + edges = [ + FlowEdgeOut( + source=label_to_id[src], + target=label_to_id[tgt], + amount=info["amount"], + count=info["count"], + trade_time=info["trade_time"], + ) + for (src, tgt), info in edge_agg.items() + ] + + return FlowGraphOut(nodes=nodes, edges=edges) + + +def _self_label(tx) -> str: + app_names = { + "wechat": "微信支付", + "alipay": "支付宝", + "bank": f"银行卡({tx.self_account_tail_no})" if tx.self_account_tail_no else "银行卡", + "digital_wallet": "数字钱包", + "other": "其他账户", + } + return app_names.get(tx.source_app.value, "未知账户") diff --git a/backend/app/services/image_service.py b/backend/app/services/image_service.py new file mode 100644 index 0000000..8ba1c73 --- /dev/null +++ b/backend/app/services/image_service.py @@ -0,0 +1,24 @@ +"""Image post-processing helpers (thumbnail generation, etc.).""" +from pathlib import Path + +from PIL import Image + +from app.core.config import settings + + +def generate_thumbnail(file_path: str, max_size: int = 400) -> str: + full = settings.upload_path / file_path + if not full.exists(): + return file_path + + thumb_dir = full.parent / "thumbs" + thumb_dir.mkdir(exist_ok=True) + thumb_path = thumb_dir / full.name + + try: + with Image.open(full) as img: + img.thumbnail((max_size, max_size)) + img.save(thumb_path) + return str(thumb_path.relative_to(settings.upload_path)) + except Exception: + return file_path diff --git a/backend/app/services/matching_service.py b/backend/app/services/matching_service.py new file mode 100644 index 0000000..140fcc7 --- /dev/null +++ b/backend/app/services/matching_service.py @@ -0,0 +1,83 @@ +"""Transaction deduplication and matching engine. + +Multi-layer strategy: + 1. Exact order_no match + 2. Amount + time-window + account-tail match + 3. Fuzzy text similarity (placeholder for LLM-assisted matching) +""" +from uuid import UUID +from datetime import timedelta + +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models.transaction import TransactionRecord +from app.models.transaction_cluster import TransactionCluster +from app.repositories.transaction_repo import TransactionRepository +from app.rules.dedup_rules import is_duplicate_pair +from app.rules.transit_rules import is_self_transfer + + +async def run_matching(case_id: UUID, self_accounts: list[str], db: AsyncSession) -> None: + """Execute the full dedup + transit-marking pipeline for a case.""" + repo = TransactionRepository(db) + transactions = await repo.get_all_by_case(case_id) + + if not transactions: + return + + # reset flags + for tx in transactions: + tx.is_duplicate = False + tx.is_transit = False + tx.cluster_id = None + + # ── Layer 1 & 2: dedup ── + matched: set[UUID] = set() + clusters: list[TransactionCluster] = [] + + for i, tx_a in enumerate(transactions): + if tx_a.id in matched: + continue + group = [tx_a] + for tx_b in transactions[i + 1:]: + if tx_b.id in matched: + continue + if is_duplicate_pair(tx_a, tx_b): + group.append(tx_b) + matched.add(tx_b.id) + + if len(group) > 1: + primary = max(group, key=lambda t: t.confidence) + cluster = TransactionCluster( + case_id=case_id, + primary_tx_id=primary.id, + match_reason=_match_reason(primary, group), + ) + db.add(cluster) + await db.flush() + + for tx in group: + tx.cluster_id = cluster.id + if tx.id != primary.id: + tx.is_duplicate = True + clusters.append(cluster) + + # ── Layer 3: transit detection ── + for tx in transactions: + if tx.is_duplicate: + continue + if is_self_transfer(tx, self_accounts): + tx.is_transit = True + + await db.flush() + + +def _match_reason(primary: TransactionRecord, group: list[TransactionRecord]) -> str: + reasons: list[str] = [] + orders = {tx.order_no for tx in group if tx.order_no} + if len(orders) == 1: + reasons.append("订单号一致") + amounts = {float(tx.amount) for tx in group} + if len(amounts) == 1: + reasons.append("金额一致") + return "; ".join(reasons) if reasons else "时间和金额近似" diff --git a/backend/app/services/ocr_service.py b/backend/app/services/ocr_service.py new file mode 100644 index 0000000..bba83cb --- /dev/null +++ b/backend/app/services/ocr_service.py @@ -0,0 +1,145 @@ +"""OCR and multimodal extraction service. + +Wraps calls to cloud OCR / multimodal APIs with a provider-agnostic interface. +When API keys are not configured, falls back to a mock implementation that +returns placeholder data (sufficient for demo / competition). +""" +import json +import logging +from pathlib import Path + +import httpx + +from app.core.config import settings +from app.models.evidence_image import SourceApp, PageType + +logger = logging.getLogger(__name__) + + +# ── provider-agnostic interface ────────────────────────────────────────── + +async def classify_page(image_path: str) -> tuple[SourceApp, PageType]: + """Identify the source app and page type of a screenshot.""" + if settings.LLM_API_KEY and settings.LLM_API_URL: + return await _classify_via_api(image_path) + return _classify_mock(image_path) + + +async def extract_transaction_fields(image_path: str, source_app: SourceApp, page_type: PageType) -> dict: + """Extract structured transaction fields from a screenshot.""" + if settings.LLM_API_KEY and settings.LLM_API_URL: + return await _extract_via_api(image_path, source_app, page_type) + return _extract_mock(image_path, source_app, page_type) + + +# ── real API implementation ────────────────────────────────────────────── + +async def _classify_via_api(image_path: str) -> tuple[SourceApp, PageType]: + import base64 + full_path = settings.upload_path / image_path + if not full_path.exists(): + return SourceApp.other, PageType.unknown + + image_b64 = base64.b64encode(full_path.read_bytes()).decode() + prompt = ( + "请分析这张手机截图,判断它来自哪个APP(wechat/alipay/bank/digital_wallet/other)" + "以及页面类型(bill_list/bill_detail/transfer_receipt/sms_notice/balance/unknown)。" + "只返回JSON: {\"source_app\": \"...\", \"page_type\": \"...\"}" + ) + + try: + async with httpx.AsyncClient(timeout=30) as client: + resp = await client.post( + settings.LLM_API_URL, + headers={"Authorization": f"Bearer {settings.LLM_API_KEY}"}, + json={ + "model": settings.LLM_MODEL, + "messages": [ + { + "role": "user", + "content": [ + {"type": "text", "text": prompt}, + {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{image_b64}"}}, + ], + } + ], + "max_tokens": 200, + }, + ) + resp.raise_for_status() + text = resp.json()["choices"][0]["message"]["content"] + data = json.loads(text.strip().strip("`").removeprefix("json").strip()) + return SourceApp(data.get("source_app", "other")), PageType(data.get("page_type", "unknown")) + except Exception as e: + logger.warning("classify_page API failed: %s", e) + return SourceApp.other, PageType.unknown + + +async def _extract_via_api(image_path: str, source_app: SourceApp, page_type: PageType) -> dict: + import base64 + full_path = settings.upload_path / image_path + if not full_path.exists(): + return {} + + image_b64 = base64.b64encode(full_path.read_bytes()).decode() + prompt = ( + f"这是一张来自{source_app.value}的{page_type.value}截图。" + "请提取其中的交易信息,返回JSON格式,字段包括:" + "trade_time(交易时间,格式YYYY-MM-DD HH:MM:SS), amount(金额,数字), " + "direction(in或out), counterparty_name(对方名称), counterparty_account(对方账号), " + "self_account_tail_no(本方账户尾号), order_no(订单号), remark(备注), confidence(0-1)。" + "如果截图包含多笔交易,返回JSON数组。否则返回单个JSON对象。" + ) + + try: + async with httpx.AsyncClient(timeout=60) as client: + resp = await client.post( + settings.LLM_API_URL, + headers={"Authorization": f"Bearer {settings.LLM_API_KEY}"}, + json={ + "model": settings.LLM_MODEL, + "messages": [ + { + "role": "user", + "content": [ + {"type": "text", "text": prompt}, + {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{image_b64}"}}, + ], + } + ], + "max_tokens": 2000, + }, + ) + resp.raise_for_status() + text = resp.json()["choices"][0]["message"]["content"] + return json.loads(text.strip().strip("`").removeprefix("json").strip()) + except Exception as e: + logger.warning("extract_transaction_fields API failed: %s", e) + return {} + + +# ── mock fallback ──────────────────────────────────────────────────────── + +def _classify_mock(image_path: str) -> tuple[SourceApp, PageType]: + lower = image_path.lower() + if "wechat" in lower or "wx" in lower: + return SourceApp.wechat, PageType.bill_detail + if "alipay" in lower or "ali" in lower: + return SourceApp.alipay, PageType.bill_list + if "bank" in lower: + return SourceApp.bank, PageType.bill_detail + return SourceApp.other, PageType.unknown + + +def _extract_mock(image_path: str, source_app: SourceApp, page_type: PageType) -> dict: + return { + "trade_time": "2026-03-08 10:00:00", + "amount": 1000.00, + "direction": "out", + "counterparty_name": "模拟对手方", + "counterparty_account": "", + "self_account_tail_no": "", + "order_no": f"MOCK-{hash(image_path) % 100000:05d}", + "remark": "模拟交易", + "confidence": 0.80, + } diff --git a/backend/app/services/parser_service.py b/backend/app/services/parser_service.py new file mode 100644 index 0000000..579ce5c --- /dev/null +++ b/backend/app/services/parser_service.py @@ -0,0 +1,47 @@ +"""Parse raw OCR / multimodal extraction results into TransactionRecord instances.""" +from datetime import datetime +from uuid import UUID + +from app.models.transaction import TransactionRecord, Direction +from app.models.evidence_image import SourceApp + + +def parse_extracted_fields( + raw: dict | list, + case_id: UUID, + image_id: UUID, + source_app: SourceApp, +) -> list[TransactionRecord]: + """Convert raw extraction dict(s) into TransactionRecord ORM objects.""" + items = raw if isinstance(raw, list) else [raw] + records: list[TransactionRecord] = [] + + for item in items: + if not item or not item.get("amount"): + continue + + try: + trade_time = datetime.fromisoformat(item["trade_time"]) + except (ValueError, KeyError): + trade_time = datetime.now() + + direction_str = item.get("direction", "out") + direction = Direction.in_ if direction_str == "in" else Direction.out + + record = TransactionRecord( + case_id=case_id, + evidence_image_id=image_id, + source_app=source_app, + trade_time=trade_time, + amount=float(item.get("amount", 0)), + direction=direction, + counterparty_name=str(item.get("counterparty_name", "")), + counterparty_account=str(item.get("counterparty_account", "")), + self_account_tail_no=str(item.get("self_account_tail_no", "")), + order_no=str(item.get("order_no", "")), + remark=str(item.get("remark", "")), + confidence=float(item.get("confidence", 0.5)), + ) + records.append(record) + + return records diff --git a/backend/app/services/report_service.py b/backend/app/services/report_service.py new file mode 100644 index 0000000..9b9dc5d --- /dev/null +++ b/backend/app/services/report_service.py @@ -0,0 +1,156 @@ +"""Report generation: Excel / Word / PDF.""" +import uuid +from pathlib import Path +from uuid import UUID + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.config import settings +from app.models.assessment import FraudAssessment, ReviewStatus +from app.models.transaction import TransactionRecord +from app.models.report import ExportReport, ReportType +from app.schemas.report import ReportCreate + + +async def generate_report(case_id: UUID, body: ReportCreate, db: AsyncSession) -> ExportReport: + result = await db.execute( + select(ExportReport) + .where(ExportReport.case_id == case_id, ExportReport.report_type == body.report_type) + ) + existing = list(result.scalars().all()) + version = len(existing) + 1 + + report_dir = settings.upload_path / str(case_id) / "reports" + report_dir.mkdir(parents=True, exist_ok=True) + + if body.report_type == ReportType.excel: + file_path = await _gen_excel(case_id, report_dir, db) + elif body.report_type == ReportType.word: + file_path = await _gen_word(case_id, report_dir, db) + else: + file_path = await _gen_pdf_placeholder(case_id, report_dir) + + relative = str(file_path.relative_to(settings.upload_path)) + + # snapshot confirmed assessments + snap_result = await db.execute( + select(FraudAssessment).where( + FraudAssessment.case_id == case_id, + FraudAssessment.review_status == ReviewStatus.confirmed, + ) + ) + snapshot = [ + {"amount": float(a.assessed_amount), "reason": a.reason} + for a in snap_result.scalars().all() + ] + + report = ExportReport( + case_id=case_id, + report_type=body.report_type, + file_path=relative, + version=version, + content_snapshot={"assessments": snapshot}, + ) + db.add(report) + await db.flush() + await db.refresh(report) + return report + + +async def _gen_excel(case_id: UUID, report_dir: Path, db: AsyncSession) -> Path: + from openpyxl import Workbook + + wb = Workbook() + + # Sheet 1: Summary + ws = wb.active + ws.title = "被骗金额汇总" + ws.append(["交易时间", "金额(元)", "方向", "对方", "来源APP", "备注", "置信度", "认定理由"]) + + assessments_result = await db.execute( + select(FraudAssessment).where( + FraudAssessment.case_id == case_id, + FraudAssessment.review_status == ReviewStatus.confirmed, + ) + ) + for a in assessments_result.scalars().all(): + tx = await db.get(TransactionRecord, a.transaction_id) + if tx: + ws.append([ + tx.trade_time.strftime("%Y-%m-%d %H:%M:%S"), + float(a.assessed_amount), + "支出" if tx.direction.value == "out" else "收入", + tx.counterparty_name, + tx.source_app.value, + tx.remark, + tx.confidence, + a.reason[:100], + ]) + + # Sheet 2: All transactions + ws2 = wb.create_sheet("交易明细") + ws2.append(["交易时间", "金额", "方向", "对方", "来源", "订单号", "是否重复", "是否中转"]) + tx_result = await db.execute( + select(TransactionRecord).where(TransactionRecord.case_id == case_id) + ) + for tx in tx_result.scalars().all(): + ws2.append([ + tx.trade_time.strftime("%Y-%m-%d %H:%M:%S"), + float(tx.amount), + tx.direction.value, + tx.counterparty_name, + tx.source_app.value, + tx.order_no, + "是" if tx.is_duplicate else "否", + "是" if tx.is_transit else "否", + ]) + + file_path = report_dir / f"report_{uuid.uuid4().hex[:8]}.xlsx" + wb.save(file_path) + return file_path + + +async def _gen_word(case_id: UUID, report_dir: Path, db: AsyncSession) -> Path: + from docx import Document + + doc = Document() + doc.add_heading("受害人被骗金额汇总报告", level=1) + + assessments_result = await db.execute( + select(FraudAssessment).where( + FraudAssessment.case_id == case_id, + FraudAssessment.review_status == ReviewStatus.confirmed, + ) + ) + confirmed = list(assessments_result.scalars().all()) + total = sum(float(a.assessed_amount) for a in confirmed) + + doc.add_paragraph(f"已确认被骗金额: ¥{total:,.2f}") + doc.add_paragraph(f"已确认交易笔数: {len(confirmed)}") + + table = doc.add_table(rows=1, cols=4) + table.style = "Table Grid" + hdr = table.rows[0].cells + hdr[0].text = "交易时间" + hdr[1].text = "金额(元)" + hdr[2].text = "对方" + hdr[3].text = "认定理由" + + for a in confirmed: + tx = await db.get(TransactionRecord, a.transaction_id) + row = table.add_row().cells + row[0].text = tx.trade_time.strftime("%Y-%m-%d %H:%M") if tx else "" + row[1].text = f"{float(a.assessed_amount):,.2f}" + row[2].text = tx.counterparty_name if tx else "" + row[3].text = a.reason[:80] + + file_path = report_dir / f"report_{uuid.uuid4().hex[:8]}.docx" + doc.save(file_path) + return file_path + + +async def _gen_pdf_placeholder(case_id: UUID, report_dir: Path) -> Path: + file_path = report_dir / f"report_{uuid.uuid4().hex[:8]}.pdf" + file_path.write_text("PDF report placeholder – integrate weasyprint/reportlab for production.") + return file_path diff --git a/backend/app/utils/__init__.py b/backend/app/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/utils/file_storage.py b/backend/app/utils/file_storage.py new file mode 100644 index 0000000..b353ccf --- /dev/null +++ b/backend/app/utils/file_storage.py @@ -0,0 +1,18 @@ +import uuid +from pathlib import Path + +from app.core.config import settings + + +def save_upload(data: bytes, case_id: str, filename: str) -> tuple[str, str]: + """Save uploaded file and return (file_path, thumb_path) relative to UPLOAD_DIR.""" + case_dir = settings.upload_path / case_id + case_dir.mkdir(parents=True, exist_ok=True) + + ext = Path(filename).suffix or ".png" + unique_name = f"{uuid.uuid4().hex}{ext}" + file_path = case_dir / unique_name + file_path.write_bytes(data) + + relative = f"{case_id}/{unique_name}" + return relative, relative diff --git a/backend/app/utils/hash.py b/backend/app/utils/hash.py new file mode 100644 index 0000000..a1e150b --- /dev/null +++ b/backend/app/utils/hash.py @@ -0,0 +1,5 @@ +import hashlib + + +def sha256_file(data: bytes) -> str: + return hashlib.sha256(data).hexdigest() diff --git a/backend/app/workers/__init__.py b/backend/app/workers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/workers/analysis_tasks.py b/backend/app/workers/analysis_tasks.py new file mode 100644 index 0000000..1d32f20 --- /dev/null +++ b/backend/app/workers/analysis_tasks.py @@ -0,0 +1,38 @@ +"""Celery task: full-case analysis pipeline.""" +import asyncio +import logging +from uuid import UUID + +from app.workers.celery_app import celery_app + +logger = logging.getLogger(__name__) + + +def _run_async(coro): + loop = asyncio.new_event_loop() + try: + return loop.run_until_complete(coro) + finally: + loop.close() + + +@celery_app.task(name="app.workers.analysis_tasks.run_full_analysis", bind=True, max_retries=2) +def run_full_analysis(self, case_id_str: str): + _run_async(_run(case_id_str)) + + +async def _run(case_id_str: str): + from app.core.database import async_session_factory + from app.services.analysis_pipeline import run_analysis_sync + + case_id = UUID(case_id_str) + + async with async_session_factory() as db: + try: + await run_analysis_sync(case_id, db) + await db.commit() + logger.info("Full analysis completed for case %s", case_id) + except Exception as e: + await db.rollback() + logger.error("Analysis failed for case %s: %s", case_id, e) + raise diff --git a/backend/app/workers/celery_app.py b/backend/app/workers/celery_app.py new file mode 100644 index 0000000..5834d44 --- /dev/null +++ b/backend/app/workers/celery_app.py @@ -0,0 +1,25 @@ +from celery import Celery + +from app.core.config import settings + +celery_app = Celery( + "fund_tracer", + broker=settings.REDIS_URL, + backend=settings.REDIS_URL, +) + +celery_app.conf.update( + task_serializer="json", + accept_content=["json"], + result_serializer="json", + timezone="Asia/Shanghai", + enable_utc=True, + task_track_started=True, + task_routes={ + "app.workers.ocr_tasks.*": {"queue": "ocr"}, + "app.workers.analysis_tasks.*": {"queue": "analysis"}, + "app.workers.report_tasks.*": {"queue": "reports"}, + }, +) + +celery_app.autodiscover_tasks(["app.workers"]) diff --git a/backend/app/workers/ocr_tasks.py b/backend/app/workers/ocr_tasks.py new file mode 100644 index 0000000..e0d8e40 --- /dev/null +++ b/backend/app/workers/ocr_tasks.py @@ -0,0 +1,74 @@ +"""Celery tasks for OCR processing of uploaded screenshots.""" +import asyncio +import logging +from uuid import UUID + +from app.workers.celery_app import celery_app + +logger = logging.getLogger(__name__) + + +def _run_async(coro): + """Run an async coroutine from synchronous Celery task context.""" + loop = asyncio.new_event_loop() + try: + return loop.run_until_complete(coro) + finally: + loop.close() + + +@celery_app.task(name="app.workers.ocr_tasks.process_image_ocr", bind=True, max_retries=3) +def process_image_ocr(self, image_id: str): + """Process a single image: classify page, extract fields, save to DB.""" + _run_async(_process(image_id)) + + +async def _process(image_id_str: str): + from app.core.database import async_session_factory + from app.models.evidence_image import EvidenceImage, OcrStatus + from app.models.ocr_block import OcrBlock + from app.services.ocr_service import classify_page, extract_transaction_fields + from app.services.parser_service import parse_extracted_fields + + image_id = UUID(image_id_str) + + async with async_session_factory() as db: + image = await db.get(EvidenceImage, image_id) + if not image: + logger.error("Image %s not found", image_id) + return + + image.ocr_status = OcrStatus.processing + await db.flush() + + try: + source_app, page_type = await classify_page(image.file_path) + image.source_app = source_app + image.page_type = page_type + + raw_fields = await extract_transaction_fields(image.file_path, source_app, page_type) + + # save raw OCR block + block = OcrBlock( + image_id=image.id, + content=str(raw_fields), + bbox={}, + seq_order=0, + confidence=raw_fields.get("confidence", 0.5) if isinstance(raw_fields, dict) else 0.5, + ) + db.add(block) + + # parse into transaction records + records = parse_extracted_fields(raw_fields, image.case_id, image.id, source_app) + for r in records: + db.add(r) + + image.ocr_status = OcrStatus.done + await db.commit() + logger.info("Image %s processed: %d transactions", image_id, len(records)) + + except Exception as e: + image.ocr_status = OcrStatus.failed + await db.commit() + logger.error("Image %s OCR failed: %s", image_id, e) + raise diff --git a/backend/app/workers/report_tasks.py b/backend/app/workers/report_tasks.py new file mode 100644 index 0000000..4671b6d --- /dev/null +++ b/backend/app/workers/report_tasks.py @@ -0,0 +1,41 @@ +"""Celery task: async report generation.""" +import asyncio +import logging +from uuid import UUID + +from app.workers.celery_app import celery_app + +logger = logging.getLogger(__name__) + + +def _run_async(coro): + loop = asyncio.new_event_loop() + try: + return loop.run_until_complete(coro) + finally: + loop.close() + + +@celery_app.task(name="app.workers.report_tasks.generate_report_async", bind=True) +def generate_report_async(self, case_id_str: str, report_type: str): + _run_async(_run(case_id_str, report_type)) + + +async def _run(case_id_str: str, report_type: str): + from app.core.database import async_session_factory + from app.models.report import ReportType + from app.schemas.report import ReportCreate + from app.services.report_service import generate_report + + case_id = UUID(case_id_str) + body = ReportCreate(report_type=ReportType(report_type)) + + async with async_session_factory() as db: + try: + report = await generate_report(case_id, body, db) + await db.commit() + logger.info("Report generated for case %s: %s", case_id, report.file_path) + except Exception as e: + await db.rollback() + logger.error("Report generation failed: %s", e) + raise diff --git a/backend/pyproject.toml b/backend/pyproject.toml new file mode 100644 index 0000000..dc8cd4a --- /dev/null +++ b/backend/pyproject.toml @@ -0,0 +1,39 @@ +[project] +name = "fund-tracer-backend" +version = "0.1.0" +description = "智析反诈 - 受害人被骗金额归集智能体后端" +requires-python = ">=3.11" +dependencies = [ + "fastapi>=0.115.0", + "uvicorn[standard]>=0.34.0", + "sqlalchemy[asyncio]>=2.0.0", + "asyncpg>=0.30.0", + "alembic>=1.14.0", + "celery[redis]>=5.4.0", + "redis>=5.0.0", + "pydantic-settings>=2.0.0", + "python-multipart>=0.0.18", + "Pillow>=11.0.0", + "httpx>=0.28.0", + "openpyxl>=3.1.0", + "python-docx>=1.1.0", + "psycopg2-binary>=2.9.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.0.0", + "pytest-asyncio>=0.24.0", + "httpx>=0.28.0", +] + +[tool.pytest.ini_options] +asyncio_mode = "auto" +testpaths = ["tests"] + +[tool.setuptools.packages.find] +include = ["app*"] + +[build-system] +requires = ["setuptools>=75.0"] +build-backend = "setuptools.build_meta" diff --git a/backend/scripts/seed.py b/backend/scripts/seed.py new file mode 100644 index 0000000..e29f667 --- /dev/null +++ b/backend/scripts/seed.py @@ -0,0 +1,107 @@ +"""Seed the database with demo case data matching the frontend mock.""" +import asyncio +import uuid +from datetime import datetime, timezone + +from app.core.database import async_session_factory +from app.models.case import Case, CaseStatus +from app.models.evidence_image import EvidenceImage, SourceApp, PageType, OcrStatus +from app.models.transaction import TransactionRecord, Direction +from app.models.assessment import FraudAssessment, ConfidenceLevel, ReviewStatus + + +async def seed(): + async with async_session_factory() as db: + # Case 1 + c1_id = uuid.UUID("00000000-0000-0000-0000-000000000001") + c1 = Case( + id=c1_id, + case_no="ZA-2026-001538", + title="张某被电信诈骗案", + victim_name="张某某", + handler="李警官", + status=CaseStatus.reviewing, + image_count=8, + total_amount=186500.00, + ) + db.add(c1) + + # Images + imgs = [ + ("wechat", "bill_list", "done"), + ("wechat", "bill_detail", "done"), + ("alipay", "bill_list", "done"), + ("alipay", "transfer_receipt", "done"), + ("bank", "bill_detail", "done"), + ("bank", "sms_notice", "done"), + ("digital_wallet", "bill_list", "done"), + ("wechat", "bill_detail", "done"), + ] + img_ids = [] + for i, (app, pt, status) in enumerate(imgs): + iid = uuid.UUID(f"00000000-0000-0000-0001-{i:012d}") + img_ids.append(iid) + db.add(EvidenceImage( + id=iid, case_id=c1_id, file_path=f"demo/img_{i}.png", thumb_path=f"demo/img_{i}.png", + source_app=SourceApp(app), page_type=PageType(pt), ocr_status=OcrStatus(status), + file_hash=f"demohash{i:04d}", + )) + + # Transactions + txs_data = [ + ("bank", "2026-03-06T10:15:00", 50000, "out", "支付宝-张某某", "", "6621", "BK20260306001", "转账至支付宝", 0.95, True, True), + ("alipay", "2026-03-06T10:16:00", 50000, "in", "银行卡(6621)", "", "", "AL20260306001", "银行卡转入", 0.92, True, True), + ("alipay", "2026-03-06T10:25:00", 50000, "out", "李*华", "138****5678", "", "AL20260306002", "投资款", 0.97, False, False), + ("wechat", "2026-03-07T14:30:00", 30000, "out", "财富管家-客服", "", "", "WX20260307001", "手续费", 0.88, False, False), + ("wechat", "2026-03-07T16:00:00", 20000, "out", "李*华", "138****5678", "", "WX20260307002", "追加保证金", 0.91, False, False), + ("digital_wallet", "2026-03-08T09:00:00", 86500, "out", "USDT-TRC20地址", "T9yD...Xk3m", "", "DW20260308001", "提币", 0.85, False, False), + ("bank", "2026-03-07T20:00:00", 86500, "out", "某数字钱包充值", "", "6621", "BK20260307002", "充值", 0.90, False, True), + ] + tx_ids = [] + for i, (app, tt, amt, dir_, cp, ca, sat, ono, rmk, conf, dup, trans) in enumerate(txs_data): + tid = uuid.UUID(f"00000000-0000-0000-0002-{i:012d}") + tx_ids.append(tid) + db.add(TransactionRecord( + id=tid, case_id=c1_id, evidence_image_id=img_ids[min(i, len(img_ids)-1)], + source_app=SourceApp(app), trade_time=datetime.fromisoformat(tt).replace(tzinfo=timezone.utc), + amount=amt, direction=Direction.in_ if dir_ == "in" else Direction.out, + counterparty_name=cp, counterparty_account=ca, self_account_tail_no=sat, + order_no=ono, remark=rmk, confidence=conf, is_duplicate=dup, is_transit=trans, + )) + + # Assessments + assessments_data = [ + (tx_ids[2], "high", 50000, "受害人经支付宝向涉诈账户转账5万元", "", "confirmed"), + (tx_ids[3], "high", 30000, "受害人经微信向客服转账3万元手续费", "", "confirmed"), + (tx_ids[4], "high", 20000, "受害人经微信向涉诈账户追加转账2万元", "", "pending"), + (tx_ids[5], "medium", 86500, "受害人通过数字钱包提币86500元", "如经查实为个人操作应排除", "pending"), + (tx_ids[0], "low", 0, "该笔为本人银行卡向支付宝中转", "本人账户间互转", "confirmed"), + ] + for i, (tid, level, amt, reason, excl, status) in enumerate(assessments_data): + db.add(FraudAssessment( + id=uuid.UUID(f"00000000-0000-0000-0003-{i:012d}"), + case_id=c1_id, transaction_id=tid, + confidence_level=ConfidenceLevel(level), assessed_amount=amt, + reason=reason, exclude_reason=excl, review_status=ReviewStatus(status), + reviewed_by="李警官" if status == "confirmed" else "", + )) + + # More cases (summary only) + for idx, (cno, title, victim, handler, st) in enumerate([ + ("ZA-2026-001612", "王某被投资诈骗案", "王某", "李警官", "analyzing"), + ("ZA-2026-001705", "刘某某被冒充客服诈骗案", "刘某某", "陈警官", "completed"), + ("ZA-2026-001821", "赵某被刷单诈骗案", "赵某", "王警官", "pending"), + ("ZA-2026-001890", "陈某被杀猪盘诈骗案", "陈某", "李警官", "uploading"), + ], start=2): + db.add(Case( + id=uuid.UUID(f"00000000-0000-0000-0000-{idx:012d}"), + case_no=cno, title=title, victim_name=victim, handler=handler, + status=CaseStatus(st), + )) + + await db.commit() + print("Seed data inserted successfully.") + + +if __name__ == "__main__": + asyncio.run(seed()) diff --git a/backend/tests/__init__.py b/backend/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py new file mode 100644 index 0000000..a20f660 --- /dev/null +++ b/backend/tests/conftest.py @@ -0,0 +1,19 @@ +import pytest +import asyncio +from httpx import AsyncClient, ASGITransport + +from app.main import app + + +@pytest.fixture(scope="session") +def event_loop(): + loop = asyncio.new_event_loop() + yield loop + loop.close() + + +@pytest.fixture +async def client(): + transport = ASGITransport(app=app) + async with AsyncClient(transport=transport, base_url="http://test") as c: + yield c diff --git a/backend/tests/test_api.py b/backend/tests/test_api.py new file mode 100644 index 0000000..f68c2c9 --- /dev/null +++ b/backend/tests/test_api.py @@ -0,0 +1,19 @@ +"""API integration tests.""" +import pytest +from httpx import AsyncClient, ASGITransport + +from app.main import app + + +@pytest.fixture +async def client(): + transport = ASGITransport(app=app) + async with AsyncClient(transport=transport, base_url="http://test") as c: + yield c + + +@pytest.mark.asyncio +async def test_health(client: AsyncClient): + resp = await client.get("/health") + assert resp.status_code == 200 + assert resp.json()["status"] == "ok" diff --git a/backend/tests/test_rules.py b/backend/tests/test_rules.py new file mode 100644 index 0000000..b929f08 --- /dev/null +++ b/backend/tests/test_rules.py @@ -0,0 +1,87 @@ +"""Unit tests for the rules engine using plain objects (no SQLAlchemy session).""" +from datetime import datetime, timezone +from types import SimpleNamespace +from uuid import uuid4 + +import pytest + +from app.models.transaction import Direction +from app.models.evidence_image import SourceApp +from app.rules.dedup_rules import is_duplicate_pair +from app.rules.transit_rules import is_self_transfer +from app.rules.assessment_rules import classify_transaction + + +def _make_tx(**kwargs): + defaults = dict( + id=uuid4(), case_id=uuid4(), source_app=SourceApp.alipay, + trade_time=datetime(2026, 3, 8, 10, 0, tzinfo=timezone.utc), + amount=10000, direction=Direction.out, + counterparty_name="测试对手方", counterparty_account="", + self_account_tail_no="1234", order_no="ORD001", + remark="测试", confidence=0.9, is_duplicate=False, is_transit=False, + evidence_image_id=None, cluster_id=None, + ) + defaults.update(kwargs) + return SimpleNamespace(**defaults) + + +class TestDedupRules: + def test_same_order_no(self): + a = _make_tx(order_no="ORD001") + b = _make_tx(order_no="ORD001", self_account_tail_no="5678") + assert is_duplicate_pair(a, b) + + def test_different_order_no_different_counterparty(self): + a = _make_tx(order_no="ORD001", counterparty_name="A", self_account_tail_no="1111") + b = _make_tx(order_no="ORD002", counterparty_name="B", self_account_tail_no="2222") + assert not is_duplicate_pair(a, b) + + def test_same_amount_close_time_same_tail(self): + a = _make_tx(order_no="", amount=5000) + b = _make_tx( + order_no="", + amount=5000, + trade_time=datetime(2026, 3, 8, 10, 3, tzinfo=timezone.utc), + ) + assert is_duplicate_pair(a, b) + + def test_same_amount_far_time(self): + a = _make_tx(order_no="", amount=5000) + b = _make_tx( + order_no="", + amount=5000, + trade_time=datetime(2026, 3, 8, 11, 0, tzinfo=timezone.utc), + ) + assert not is_duplicate_pair(a, b) + + +class TestTransitRules: + def test_keyword_match(self): + tx = _make_tx(counterparty_name="支付宝-张某", direction=Direction.out) + assert is_self_transfer(tx, []) + + def test_known_account_match(self): + tx = _make_tx(counterparty_name="我的银行卡") + assert is_self_transfer(tx, ["我的银行卡"]) + + def test_not_transit(self): + tx = _make_tx(counterparty_name="李*华", remark="投资款") + assert not is_self_transfer(tx, []) + + +class TestAssessmentRules: + def test_transit_classified_as_low(self): + tx = _make_tx(is_transit=True) + level, reason, _ = classify_transaction(tx) + assert level.value == "low" + + def test_high_confidence_fraud_keyword(self): + tx = _make_tx(confidence=0.95, remark="投资款") + level, reason, _ = classify_transaction(tx) + assert level.value == "high" + + def test_income_classified_as_low(self): + tx = _make_tx(direction=Direction.in_) + level, _, _ = classify_transaction(tx) + assert level.value == "low" diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..d2e7761 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..8a043ed --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + 智析反诈 - 受害人被骗金额归集智能体 + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..256b513 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,5397 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "@ant-design/icons": "^6.1.0", + "@ant-design/pro-components": "^2.8.10", + "@tanstack/react-query": "^5.90.21", + "antd": "^5.29.3", + "dayjs": "^1.11.19", + "echarts": "^6.0.0", + "echarts-for-react": "^3.0.6", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-router-dom": "^7.13.1", + "zustand": "^5.0.11" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.48.0", + "vite": "^7.3.1" + } + }, + "node_modules/@ant-design/colors": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-8.0.1.tgz", + "integrity": "sha512-foPVl0+SWIslGUtD/xBr1p9U4AKzPhNYEseXYRRo5QSzGACYZrQbe11AYJbYfAWnWSpGBx6JjBmSeugUsD9vqQ==", + "license": "MIT", + "dependencies": { + "@ant-design/fast-color": "^3.0.0" + } + }, + "node_modules/@ant-design/cssinjs": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-1.24.0.tgz", + "integrity": "sha512-K4cYrJBsgvL+IoozUXYjbT6LHHNt+19a9zkvpBPxLjFHas1UpPM2A5MlhROb0BT8N8WoavM5VsP9MeSeNK/3mg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "@emotion/hash": "^0.8.0", + "@emotion/unitless": "^0.7.5", + "classnames": "^2.3.1", + "csstype": "^3.1.3", + "rc-util": "^5.35.0", + "stylis": "^4.3.4" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/cssinjs-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs-utils/-/cssinjs-utils-1.1.3.tgz", + "integrity": "sha512-nOoQMLW1l+xR1Co8NFVYiP8pZp3VjIIzqV6D6ShYF2ljtdwWJn5WSsH+7kvCktXL/yhEtWURKOfH5Xz/gzlwsg==", + "license": "MIT", + "dependencies": { + "@ant-design/cssinjs": "^1.21.0", + "@babel/runtime": "^7.23.2", + "rc-util": "^5.38.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@ant-design/fast-color": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-3.0.1.tgz", + "integrity": "sha512-esKJegpW4nckh0o6kV3Tkb7NPIZYbPnnFxmQDUmL08ukXZAvV85TZBr70eGuke/CIArLaP6aw8lt9KILjnWuOw==", + "license": "MIT", + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@ant-design/icons": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-6.1.0.tgz", + "integrity": "sha512-KrWMu1fIg3w/1F2zfn+JlfNDU8dDqILfA5Tg85iqs1lf8ooyGlbkA+TkwfOKKgqpUmAiRY1PTFpuOU2DAIgSUg==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^8.0.0", + "@ant-design/icons-svg": "^4.4.0", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/icons-svg": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz", + "integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==", + "license": "MIT" + }, + "node_modules/@ant-design/pro-card": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@ant-design/pro-card/-/pro-card-2.10.0.tgz", + "integrity": "sha512-sLONn1odmE0Wkbse8pol4WiaEzBV8JU5s3FAMflPpycfUcbSaa1ktXzQ7LCo2SAvOS7gkfmpFjBPtrfbigKh4g==", + "license": "MIT", + "dependencies": { + "@ant-design/cssinjs": "^1.21.1", + "@ant-design/icons": "^5.0.0", + "@ant-design/pro-provider": "2.16.2", + "@ant-design/pro-utils": "2.18.0", + "@babel/runtime": "^7.18.0", + "classnames": "^2.3.2", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.4.0" + }, + "peerDependencies": { + "antd": "^4.24.15 || ^5.11.2", + "react": ">=17.0.0" + } + }, + "node_modules/@ant-design/pro-card/node_modules/@ant-design/colors": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.2.1.tgz", + "integrity": "sha512-lCHDcEzieu4GA3n8ELeZ5VQ8pKQAWcGGLRTQ50aQM2iqPpq2evTxER84jfdPvsPAtEcZ7m44NI45edFMo8oOYQ==", + "license": "MIT", + "dependencies": { + "@ant-design/fast-color": "^2.0.6" + } + }, + "node_modules/@ant-design/pro-card/node_modules/@ant-design/fast-color": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-2.0.6.tgz", + "integrity": "sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7" + }, + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@ant-design/pro-card/node_modules/@ant-design/icons": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz", + "integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^7.0.0", + "@ant-design/icons-svg": "^4.4.0", + "@babel/runtime": "^7.24.8", + "classnames": "^2.2.6", + "rc-util": "^5.31.1" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/pro-components": { + "version": "2.8.10", + "resolved": "https://registry.npmjs.org/@ant-design/pro-components/-/pro-components-2.8.10.tgz", + "integrity": "sha512-QHnnIXdmC5GTAtm6i8eeJy5yT9npPlFyxpDm+duiDrTRKRFaAQBduArxlH3DA/hoRCCypzPONxfK9BQNIhIyZA==", + "license": "MIT", + "dependencies": { + "@ant-design/pro-card": "2.10.0", + "@ant-design/pro-descriptions": "2.6.10", + "@ant-design/pro-field": "3.1.0", + "@ant-design/pro-form": "2.32.0", + "@ant-design/pro-layout": "7.22.7", + "@ant-design/pro-list": "2.6.10", + "@ant-design/pro-provider": "2.16.2", + "@ant-design/pro-skeleton": "2.2.1", + "@ant-design/pro-table": "3.21.0", + "@ant-design/pro-utils": "2.18.0", + "@babel/runtime": "^7.16.3" + }, + "peerDependencies": { + "antd": "^4.24.15 || ^5.11.2", + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@ant-design/pro-descriptions": { + "version": "2.6.10", + "resolved": "https://registry.npmjs.org/@ant-design/pro-descriptions/-/pro-descriptions-2.6.10.tgz", + "integrity": "sha512-+4MbiOfumnWlW0Awm4m8JML5o3lR649FD24AaivCmr8BQvIAAXdTITnDMXEg8BqvdP4KOvNsStZrvYfqoev33A==", + "license": "MIT", + "dependencies": { + "@ant-design/pro-field": "3.1.0", + "@ant-design/pro-form": "2.32.0", + "@ant-design/pro-provider": "2.16.2", + "@ant-design/pro-skeleton": "2.2.1", + "@ant-design/pro-utils": "2.18.0", + "@babel/runtime": "^7.18.0", + "rc-resize-observer": "^0.2.3", + "rc-util": "^5.0.6" + }, + "peerDependencies": { + "antd": "^4.24.15 || ^5.11.2", + "react": ">=17.0.0" + } + }, + "node_modules/@ant-design/pro-descriptions/node_modules/rc-resize-observer": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/rc-resize-observer/-/rc-resize-observer-0.2.6.tgz", + "integrity": "sha512-YX6nYnd6fk7zbuvT6oSDMKiZjyngjHoy+fz+vL3Tez38d/G5iGdaDJa2yE7345G6sc4Mm1IGRUIwclvltddhmA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.1", + "rc-util": "^5.0.0", + "resize-observer-polyfill": "^1.5.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@ant-design/pro-field": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@ant-design/pro-field/-/pro-field-3.1.0.tgz", + "integrity": "sha512-+Dgp31WjD+iwg9KIRAMgNkfQivkJKMcYBrIBmho1e8ep/O0HgWSp48g70tBIWi/Lfem/Ky2schF7O8XCFouczw==", + "license": "MIT", + "dependencies": { + "@ant-design/icons": "^5.0.0", + "@ant-design/pro-provider": "2.16.2", + "@ant-design/pro-utils": "2.18.0", + "@babel/runtime": "^7.18.0", + "@chenshuai2144/sketch-color": "^1.0.8", + "classnames": "^2.3.2", + "dayjs": "^1.11.10", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "rc-util": "^5.4.0", + "swr": "^2.0.0" + }, + "peerDependencies": { + "antd": "^4.24.15 || ^5.11.2", + "react": ">=17.0.0" + } + }, + "node_modules/@ant-design/pro-field/node_modules/@ant-design/colors": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.2.1.tgz", + "integrity": "sha512-lCHDcEzieu4GA3n8ELeZ5VQ8pKQAWcGGLRTQ50aQM2iqPpq2evTxER84jfdPvsPAtEcZ7m44NI45edFMo8oOYQ==", + "license": "MIT", + "dependencies": { + "@ant-design/fast-color": "^2.0.6" + } + }, + "node_modules/@ant-design/pro-field/node_modules/@ant-design/fast-color": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-2.0.6.tgz", + "integrity": "sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7" + }, + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@ant-design/pro-field/node_modules/@ant-design/icons": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz", + "integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^7.0.0", + "@ant-design/icons-svg": "^4.4.0", + "@babel/runtime": "^7.24.8", + "classnames": "^2.2.6", + "rc-util": "^5.31.1" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/pro-form": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/@ant-design/pro-form/-/pro-form-2.32.0.tgz", + "integrity": "sha512-GZnVAMeYv+YHJb17lJ7rX5PYuQPvEA6EotQnPbHi9tGLN3PfexcAd21rqzuO+OrulU2x7TEMDIxtY9MzvvOGbg==", + "license": "MIT", + "dependencies": { + "@ant-design/icons": "^5.0.0", + "@ant-design/pro-field": "3.1.0", + "@ant-design/pro-provider": "2.16.2", + "@ant-design/pro-utils": "2.18.0", + "@babel/runtime": "^7.18.0", + "@chenshuai2144/sketch-color": "^1.0.7", + "@umijs/use-params": "^1.0.9", + "classnames": "^2.3.2", + "dayjs": "^1.11.10", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "rc-resize-observer": "^1.1.0", + "rc-util": "^5.0.6" + }, + "peerDependencies": { + "antd": "^4.24.15 || ^5.11.2", + "rc-field-form": ">=1.22.0", + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@ant-design/pro-form/node_modules/@ant-design/colors": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.2.1.tgz", + "integrity": "sha512-lCHDcEzieu4GA3n8ELeZ5VQ8pKQAWcGGLRTQ50aQM2iqPpq2evTxER84jfdPvsPAtEcZ7m44NI45edFMo8oOYQ==", + "license": "MIT", + "dependencies": { + "@ant-design/fast-color": "^2.0.6" + } + }, + "node_modules/@ant-design/pro-form/node_modules/@ant-design/fast-color": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-2.0.6.tgz", + "integrity": "sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7" + }, + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@ant-design/pro-form/node_modules/@ant-design/icons": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz", + "integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^7.0.0", + "@ant-design/icons-svg": "^4.4.0", + "@babel/runtime": "^7.24.8", + "classnames": "^2.2.6", + "rc-util": "^5.31.1" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/pro-layout": { + "version": "7.22.7", + "resolved": "https://registry.npmjs.org/@ant-design/pro-layout/-/pro-layout-7.22.7.tgz", + "integrity": "sha512-fvmtNA1r9SaasVIQIQt611VSlNxtVxDbQ3e+1GhYQza3tVJi/3gCZuDyfMfTnbLmf3PaW/YvLkn7MqDbzAzoLA==", + "license": "MIT", + "dependencies": { + "@ant-design/cssinjs": "^1.21.1", + "@ant-design/icons": "^5.0.0", + "@ant-design/pro-provider": "2.16.2", + "@ant-design/pro-utils": "2.18.0", + "@babel/runtime": "^7.18.0", + "@umijs/route-utils": "^4.0.0", + "@umijs/use-params": "^1.0.9", + "classnames": "^2.3.2", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "path-to-regexp": "8.2.0", + "rc-resize-observer": "^1.1.0", + "rc-util": "^5.0.6", + "swr": "^2.0.0", + "warning": "^4.0.3" + }, + "peerDependencies": { + "antd": "^4.24.15 || ^5.11.2", + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@ant-design/pro-layout/node_modules/@ant-design/colors": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.2.1.tgz", + "integrity": "sha512-lCHDcEzieu4GA3n8ELeZ5VQ8pKQAWcGGLRTQ50aQM2iqPpq2evTxER84jfdPvsPAtEcZ7m44NI45edFMo8oOYQ==", + "license": "MIT", + "dependencies": { + "@ant-design/fast-color": "^2.0.6" + } + }, + "node_modules/@ant-design/pro-layout/node_modules/@ant-design/fast-color": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-2.0.6.tgz", + "integrity": "sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7" + }, + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@ant-design/pro-layout/node_modules/@ant-design/icons": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz", + "integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^7.0.0", + "@ant-design/icons-svg": "^4.4.0", + "@babel/runtime": "^7.24.8", + "classnames": "^2.2.6", + "rc-util": "^5.31.1" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/pro-list": { + "version": "2.6.10", + "resolved": "https://registry.npmjs.org/@ant-design/pro-list/-/pro-list-2.6.10.tgz", + "integrity": "sha512-xSWwnqCr+hPEYR4qY7nFUaxO5RQBxNlFaPNmobP2i+Im31slk9JuAusgWeIYO0mNhLJuLbxd8CCma2AZij3fBQ==", + "license": "MIT", + "dependencies": { + "@ant-design/cssinjs": "^1.21.1", + "@ant-design/icons": "^5.0.0", + "@ant-design/pro-card": "2.10.0", + "@ant-design/pro-field": "3.1.0", + "@ant-design/pro-table": "3.21.0", + "@ant-design/pro-utils": "2.18.0", + "@babel/runtime": "^7.18.0", + "classnames": "^2.3.2", + "dayjs": "^1.11.10", + "rc-resize-observer": "^1.0.0", + "rc-util": "^4.19.0" + }, + "peerDependencies": { + "antd": "^4.24.15 || ^5.11.2", + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@ant-design/pro-list/node_modules/@ant-design/colors": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.2.1.tgz", + "integrity": "sha512-lCHDcEzieu4GA3n8ELeZ5VQ8pKQAWcGGLRTQ50aQM2iqPpq2evTxER84jfdPvsPAtEcZ7m44NI45edFMo8oOYQ==", + "license": "MIT", + "dependencies": { + "@ant-design/fast-color": "^2.0.6" + } + }, + "node_modules/@ant-design/pro-list/node_modules/@ant-design/fast-color": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-2.0.6.tgz", + "integrity": "sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7" + }, + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@ant-design/pro-list/node_modules/@ant-design/icons": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz", + "integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^7.0.0", + "@ant-design/icons-svg": "^4.4.0", + "@babel/runtime": "^7.24.8", + "classnames": "^2.2.6", + "rc-util": "^5.31.1" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/pro-list/node_modules/@ant-design/icons/node_modules/rc-util": { + "version": "5.44.4", + "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.44.4.tgz", + "integrity": "sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "react-is": "^18.2.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@ant-design/pro-list/node_modules/rc-util": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-4.21.1.tgz", + "integrity": "sha512-Z+vlkSQVc1l8O2UjR3WQ+XdWlhj5q9BMQNLk2iOBch75CqPfrJyGtcWMcnhRlNuDu0Ndtt4kLVO8JI8BrABobg==", + "license": "MIT", + "dependencies": { + "add-dom-event-listener": "^1.1.0", + "prop-types": "^15.5.10", + "react-is": "^16.12.0", + "react-lifecycles-compat": "^3.0.4", + "shallowequal": "^1.1.0" + } + }, + "node_modules/@ant-design/pro-list/node_modules/rc-util/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/@ant-design/pro-provider": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/@ant-design/pro-provider/-/pro-provider-2.16.2.tgz", + "integrity": "sha512-0KmCH1EaOND787Jz6VRMYtLNZmqfT0JPjdUfxhyOxFfnBRfrjyfZgIa6CQoAJLEUMWv57PccWS8wRHVUUk2Yiw==", + "license": "MIT", + "dependencies": { + "@ant-design/cssinjs": "^1.21.1", + "@babel/runtime": "^7.18.0", + "@ctrl/tinycolor": "^3.4.0", + "dayjs": "^1.11.10", + "rc-util": "^5.0.1", + "swr": "^2.0.0" + }, + "peerDependencies": { + "antd": "^4.24.15 || ^5.11.2", + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@ant-design/pro-skeleton": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ant-design/pro-skeleton/-/pro-skeleton-2.2.1.tgz", + "integrity": "sha512-3M2jNOZQZWEDR8pheY00OkHREfb0rquvFZLCa6DypGmiksiuuYuR9Y4iA82ZF+mva2FmpHekdwbje/GpbxqBeg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0" + }, + "peerDependencies": { + "antd": "^4.24.15 || ^5.11.2", + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@ant-design/pro-table": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@ant-design/pro-table/-/pro-table-3.21.0.tgz", + "integrity": "sha512-sI81d3FYRv5sXamUc+M5CsHZ9CchuUQgOAPzo5H4oPAVL5h+mkYGRsBzPsxQX7khTNpWjrAtPoRm5ipx3vvWog==", + "license": "MIT", + "dependencies": { + "@ant-design/cssinjs": "^1.21.1", + "@ant-design/icons": "^5.0.0", + "@ant-design/pro-card": "2.10.0", + "@ant-design/pro-field": "3.1.0", + "@ant-design/pro-form": "2.32.0", + "@ant-design/pro-provider": "2.16.2", + "@ant-design/pro-utils": "2.18.0", + "@babel/runtime": "^7.18.0", + "@dnd-kit/core": "^6.0.8", + "@dnd-kit/modifiers": "^6.0.1", + "@dnd-kit/sortable": "^7.0.2", + "@dnd-kit/utilities": "^3.2.1", + "classnames": "^2.3.2", + "dayjs": "^1.11.10", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.0.1" + }, + "peerDependencies": { + "antd": "^4.24.15 || ^5.11.2", + "rc-field-form": ">=1.22.0", + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@ant-design/pro-table/node_modules/@ant-design/colors": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.2.1.tgz", + "integrity": "sha512-lCHDcEzieu4GA3n8ELeZ5VQ8pKQAWcGGLRTQ50aQM2iqPpq2evTxER84jfdPvsPAtEcZ7m44NI45edFMo8oOYQ==", + "license": "MIT", + "dependencies": { + "@ant-design/fast-color": "^2.0.6" + } + }, + "node_modules/@ant-design/pro-table/node_modules/@ant-design/fast-color": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-2.0.6.tgz", + "integrity": "sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7" + }, + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@ant-design/pro-table/node_modules/@ant-design/icons": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz", + "integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^7.0.0", + "@ant-design/icons-svg": "^4.4.0", + "@babel/runtime": "^7.24.8", + "classnames": "^2.2.6", + "rc-util": "^5.31.1" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/pro-utils": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/@ant-design/pro-utils/-/pro-utils-2.18.0.tgz", + "integrity": "sha512-8+ikyrN8L8a8Ph4oeHTOJEiranTj18+9+WHCHjKNdEfukI7Rjn8xpYdLJWb2AUJkb9d4eoAqjd5+k+7w81Df0w==", + "license": "MIT", + "dependencies": { + "@ant-design/icons": "^5.0.0", + "@ant-design/pro-provider": "2.16.2", + "@babel/runtime": "^7.18.0", + "classnames": "^2.3.2", + "dayjs": "^1.11.10", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "rc-util": "^5.0.6", + "safe-stable-stringify": "^2.4.3", + "swr": "^2.0.0" + }, + "peerDependencies": { + "antd": "^4.24.15 || ^5.11.2", + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@ant-design/pro-utils/node_modules/@ant-design/colors": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.2.1.tgz", + "integrity": "sha512-lCHDcEzieu4GA3n8ELeZ5VQ8pKQAWcGGLRTQ50aQM2iqPpq2evTxER84jfdPvsPAtEcZ7m44NI45edFMo8oOYQ==", + "license": "MIT", + "dependencies": { + "@ant-design/fast-color": "^2.0.6" + } + }, + "node_modules/@ant-design/pro-utils/node_modules/@ant-design/fast-color": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-2.0.6.tgz", + "integrity": "sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7" + }, + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@ant-design/pro-utils/node_modules/@ant-design/icons": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz", + "integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^7.0.0", + "@ant-design/icons-svg": "^4.4.0", + "@babel/runtime": "^7.24.8", + "classnames": "^2.2.6", + "rc-util": "^5.31.1" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/react-slick": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-1.1.2.tgz", + "integrity": "sha512-EzlvzE6xQUBrZuuhSAFTdsr4P2bBBHGZwKFemEfq8gIGyIQCxalYfZW/T2ORbtQx5rU69o+WycP3exY/7T1hGA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.4", + "classnames": "^2.2.5", + "json2mq": "^0.2.0", + "resize-observer-polyfill": "^1.5.1", + "throttle-debounce": "^5.0.0" + }, + "peerDependencies": { + "react": ">=16.9.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@chenshuai2144/sketch-color": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@chenshuai2144/sketch-color/-/sketch-color-1.0.9.tgz", + "integrity": "sha512-obzSy26cb7Pm7OprWyVpgMpIlrZpZ0B7vbrU0RMbvRg0YAI890S5Xy02Aj1Nhl4+KTbi1lVYHt6HQP8Hm9s+1w==", + "license": "MIT", + "dependencies": { + "reactcss": "^1.2.3", + "tinycolor2": "^1.4.2" + }, + "peerDependencies": { + "react": ">=16.12.0" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", + "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/modifiers": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-6.0.1.tgz", + "integrity": "sha512-rbxcsg3HhzlcMHVHWDuh9LCjpOVAgqbV78wLGI8tziXY3+qcMQ61qVXIvNKQFuhj75dSfD+o+PYZQ/NUk2A23A==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.1", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.0.6", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-7.0.2.tgz", + "integrity": "sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.0", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.0.7", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "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/eslint-utils/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-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-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.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "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.1.1", + "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/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/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "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/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.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/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rc-component/async-validator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-5.1.0.tgz", + "integrity": "sha512-n4HcR5siNUXRX23nDizbZBQPO0ZM/5oTtmKZ6/eqL0L2bo747cklFdZGRN2f+c9qWGICwDzrhW0H7tE9PptdcA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.4" + }, + "engines": { + "node": ">=14.x" + } + }, + "node_modules/@rc-component/color-picker": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-2.0.1.tgz", + "integrity": "sha512-WcZYwAThV/b2GISQ8F+7650r5ZZJ043E57aVBFkQ+kSY4C6wdofXgB0hBx+GPGpIU0Z81eETNoDUJMr7oy/P8Q==", + "license": "MIT", + "dependencies": { + "@ant-design/fast-color": "^2.0.6", + "@babel/runtime": "^7.23.6", + "classnames": "^2.2.6", + "rc-util": "^5.38.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/color-picker/node_modules/@ant-design/fast-color": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-2.0.6.tgz", + "integrity": "sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7" + }, + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@rc-component/context": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@rc-component/context/-/context-1.4.0.tgz", + "integrity": "sha512-kFcNxg9oLRMoL3qki0OMxK+7g5mypjgaaJp/pkOis/6rVxma9nJBF/8kCIuTYHUQNr0ii7MxqE33wirPZLJQ2w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/mini-decimal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rc-component/mini-decimal/-/mini-decimal-1.1.0.tgz", + "integrity": "sha512-jS4E7T9Li2GuYwI6PyiVXmxTiM6b07rlD9Ge8uGZSCz3WlzcG5ZK7g5bbuKNeZ9pgUuPK/5guV781ujdVpm4HQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0" + }, + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@rc-component/mutate-observer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rc-component/mutate-observer/-/mutate-observer-1.1.0.tgz", + "integrity": "sha512-QjrOsDXQusNwGZPf4/qRQasg7UFEj06XiCJ8iuiq/Io7CrHrgVi6Uuetw60WAMG1799v+aM8kyc+1L/GBbHSlw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0", + "classnames": "^2.3.2", + "rc-util": "^5.24.4" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/portal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@rc-component/portal/-/portal-1.1.2.tgz", + "integrity": "sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0", + "classnames": "^2.3.2", + "rc-util": "^5.24.4" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/qrcode": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@rc-component/qrcode/-/qrcode-1.1.1.tgz", + "integrity": "sha512-LfLGNymzKdUPjXUbRP+xOhIWY4jQ+YMj5MmWAcgcAq1Ij8XP7tRmAXqyuv96XvLUBE/5cA8hLFl9eO1JQMujrA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/tour": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@rc-component/tour/-/tour-1.15.1.tgz", + "integrity": "sha512-Tr2t7J1DKZUpfJuDZWHxyxWpfmj8EZrqSgyMZ+BCdvKZ6r1UDsfU46M/iWAAFBy961Ssfom2kv5f3UcjIL2CmQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0", + "@rc-component/portal": "^1.0.0-9", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.3.2", + "rc-util": "^5.24.4" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/trigger": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-2.3.1.tgz", + "integrity": "sha512-ORENF39PeXTzM+gQEshuk460Z8N4+6DkjpxlpE7Q3gYy1iBpLrx0FOJz3h62ryrJZ/3zCAUIkT1Pb/8hHWpb3A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2", + "@rc-component/portal": "^1.1.0", + "classnames": "^2.3.2", + "rc-motion": "^2.0.0", + "rc-resize-observer": "^1.3.1", + "rc-util": "^5.44.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/util": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@rc-component/util/-/util-1.9.0.tgz", + "integrity": "sha512-5uW6AfhIigCWeEQDthTozlxiT4Prn6xYQWeO0xokjcaa186OtwPRHBZJ2o0T0FhbjGhZ3vXdbkv0sx3gAYW7Vg==", + "license": "MIT", + "dependencies": { + "is-mobile": "^5.0.0", + "react-is": "^18.2.0" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tanstack/query-core": { + "version": "5.90.20", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz", + "integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.21", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz", + "integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/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/node": { + "version": "24.12.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", + "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.0.tgz", + "integrity": "sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.57.0", + "@typescript-eslint/type-utils": "8.57.0", + "@typescript-eslint/utils": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.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.57.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.0.tgz", + "integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.57.0", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.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.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.0.tgz", + "integrity": "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.57.0", + "@typescript-eslint/types": "^8.57.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.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.0.tgz", + "integrity": "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.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.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.0.tgz", + "integrity": "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==", + "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.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.0.tgz", + "integrity": "sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0", + "@typescript-eslint/utils": "8.57.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.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.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.0.tgz", + "integrity": "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==", + "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.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.0.tgz", + "integrity": "sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.57.0", + "@typescript-eslint/tsconfig-utils": "8.57.0", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.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.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/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/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.0.tgz", + "integrity": "sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.57.0", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.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.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.0.tgz", + "integrity": "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.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/@umijs/route-utils": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@umijs/route-utils/-/route-utils-4.0.3.tgz", + "integrity": "sha512-zPEcYhl1cSfkSRDzzGgoD1mDvGjxoOTJFvkn55srfgdQ3NZe2ZMCScCU6DEnOxuKP1XDVf8pqyqCDVd2+RCQIw==", + "license": "MIT" + }, + "node_modules/@umijs/use-params": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@umijs/use-params/-/use-params-1.0.9.tgz", + "integrity": "sha512-QlN0RJSBVQBwLRNxbxjQ5qzqYIGn+K7USppMoIOVlf7fxXHsnQZ2bEsa6Pm74bt6DVQxpUE8HqvdStn6Y9FV1w==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.4.tgz", + "integrity": "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "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/add-dom-event-listener": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/add-dom-event-listener/-/add-dom-event-listener-1.1.0.tgz", + "integrity": "sha512-WCxx1ixHT0GQU9hb0KI/mhgRQhnU+U3GvwY6ZvVjYq8rsihIGoaIOUbY0yMPBxLH5MDtr0kz3fisWGNcbWW7Jw==", + "license": "MIT", + "dependencies": { + "object-assign": "4.x" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "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/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/antd": { + "version": "5.29.3", + "resolved": "https://registry.npmjs.org/antd/-/antd-5.29.3.tgz", + "integrity": "sha512-3DdbGCa9tWAJGcCJ6rzR8EJFsv2CtyEbkVabZE14pfgUHfCicWCj0/QzQVLDYg8CPfQk9BH7fHCoTXHTy7MP/A==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^7.2.1", + "@ant-design/cssinjs": "^1.23.0", + "@ant-design/cssinjs-utils": "^1.1.3", + "@ant-design/fast-color": "^2.0.6", + "@ant-design/icons": "^5.6.1", + "@ant-design/react-slick": "~1.1.2", + "@babel/runtime": "^7.26.0", + "@rc-component/color-picker": "~2.0.1", + "@rc-component/mutate-observer": "^1.1.0", + "@rc-component/qrcode": "~1.1.0", + "@rc-component/tour": "~1.15.1", + "@rc-component/trigger": "^2.3.0", + "classnames": "^2.5.1", + "copy-to-clipboard": "^3.3.3", + "dayjs": "^1.11.11", + "rc-cascader": "~3.34.0", + "rc-checkbox": "~3.5.0", + "rc-collapse": "~3.9.0", + "rc-dialog": "~9.6.0", + "rc-drawer": "~7.3.0", + "rc-dropdown": "~4.2.1", + "rc-field-form": "~2.7.1", + "rc-image": "~7.12.0", + "rc-input": "~1.8.0", + "rc-input-number": "~9.5.0", + "rc-mentions": "~2.20.0", + "rc-menu": "~9.16.1", + "rc-motion": "^2.9.5", + "rc-notification": "~5.6.4", + "rc-pagination": "~5.1.0", + "rc-picker": "~4.11.3", + "rc-progress": "~4.0.0", + "rc-rate": "~2.13.1", + "rc-resize-observer": "^1.4.3", + "rc-segmented": "~2.7.0", + "rc-select": "~14.16.8", + "rc-slider": "~11.1.9", + "rc-steps": "~6.0.1", + "rc-switch": "~4.1.0", + "rc-table": "~7.54.0", + "rc-tabs": "~15.7.0", + "rc-textarea": "~1.10.2", + "rc-tooltip": "~6.4.0", + "rc-tree": "~5.13.1", + "rc-tree-select": "~5.27.0", + "rc-upload": "~4.11.0", + "rc-util": "^5.44.4", + "scroll-into-view-if-needed": "^3.1.0", + "throttle-debounce": "^5.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ant-design" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/antd/node_modules/@ant-design/colors": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.2.1.tgz", + "integrity": "sha512-lCHDcEzieu4GA3n8ELeZ5VQ8pKQAWcGGLRTQ50aQM2iqPpq2evTxER84jfdPvsPAtEcZ7m44NI45edFMo8oOYQ==", + "license": "MIT", + "dependencies": { + "@ant-design/fast-color": "^2.0.6" + } + }, + "node_modules/antd/node_modules/@ant-design/fast-color": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-2.0.6.tgz", + "integrity": "sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7" + }, + "engines": { + "node": ">=8.x" + } + }, + "node_modules/antd/node_modules/@ant-design/icons": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz", + "integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^7.0.0", + "@ant-design/icons-svg": "^4.4.0", + "@babel/runtime": "^7.24.8", + "classnames": "^2.2.6", + "rc-util": "^5.31.1" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "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/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/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "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/caniuse-lite": { + "version": "1.0.30001777", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz", + "integrity": "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "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/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/compute-scroll-into-view": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz", + "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==", + "license": "MIT" + }, + "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/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "license": "MIT", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, + "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/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.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "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/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/echarts": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz", + "integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "2.3.0", + "zrender": "6.0.0" + } + }, + "node_modules/echarts-for-react": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/echarts-for-react/-/echarts-for-react-3.0.6.tgz", + "integrity": "sha512-4zqLgTGWS3JvkQDXjzkR1k1CHRdpd6by0988TWMJgnvDytegWLbeP/VNZmMa+0VJx2eD7Y632bi2JquXDgiGJg==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "size-sensor": "^1.0.1" + }, + "peerDependencies": { + "echarts": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0", + "react": "^15.0.0 || >=16.0.0" + } + }, + "node_modules/echarts/node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.307", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", + "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "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.5", + "@eslint/js": "9.39.4", + "@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-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "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": "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/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/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/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==", + "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.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", + "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", + "dev": true, + "license": "ISC" + }, + "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/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "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": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "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/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/is-mobile": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-mobile/-/is-mobile-5.0.0.tgz", + "integrity": "sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ==", + "license": "MIT" + }, + "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-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "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/json2mq": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz", + "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==", + "license": "MIT", + "dependencies": { + "string-convert": "^0.2.0" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "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.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", + "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", + "license": "MIT" + }, + "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/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "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/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "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/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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-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/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/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/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "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/rc-cascader": { + "version": "3.34.0", + "resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.34.0.tgz", + "integrity": "sha512-KpXypcvju9ptjW9FaN2NFcA2QH9E9LHKq169Y0eWtH4e/wHQ5Wh5qZakAgvb8EKZ736WZ3B0zLLOBsrsja5Dag==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.7", + "classnames": "^2.3.1", + "rc-select": "~14.16.2", + "rc-tree": "~5.13.0", + "rc-util": "^5.43.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-checkbox": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/rc-checkbox/-/rc-checkbox-3.5.0.tgz", + "integrity": "sha512-aOAQc3E98HteIIsSqm6Xk2FPKIER6+5vyEFMZfo73TqM+VVAIqOkHoPjgKLqSNtVLWScoaM7vY2ZrGEheI79yg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.3.2", + "rc-util": "^5.25.2" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-collapse": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/rc-collapse/-/rc-collapse-3.9.0.tgz", + "integrity": "sha512-swDdz4QZ4dFTo4RAUMLL50qP0EY62N2kvmk2We5xYdRwcRn8WcYtuetCJpwpaCbUfUt5+huLpVxhvmnK+PHrkA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "rc-motion": "^2.3.4", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-dialog": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/rc-dialog/-/rc-dialog-9.6.0.tgz", + "integrity": "sha512-ApoVi9Z8PaCQg6FsUzS8yvBEQy0ZL2PkuvAgrmohPkN3okps5WZ5WQWPc1RNuiOKaAYv8B97ACdsFU5LizzCqg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/portal": "^1.0.0-8", + "classnames": "^2.2.6", + "rc-motion": "^2.3.0", + "rc-util": "^5.21.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-drawer": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/rc-drawer/-/rc-drawer-7.3.0.tgz", + "integrity": "sha512-DX6CIgiBWNpJIMGFO8BAISFkxiuKitoizooj4BDyee8/SnBn0zwO2FHrNDpqqepj0E/TFTDpmEBCyFuTgC7MOg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@rc-component/portal": "^1.1.1", + "classnames": "^2.2.6", + "rc-motion": "^2.6.1", + "rc-util": "^5.38.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-dropdown": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/rc-dropdown/-/rc-dropdown-4.2.1.tgz", + "integrity": "sha512-YDAlXsPv3I1n42dv1JpdM7wJ+gSUBfeyPK59ZpBD9jQhK9jVuxpjj3NmWQHOBceA1zEPVX84T2wbdb2SD0UjmA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.2.6", + "rc-util": "^5.44.1" + }, + "peerDependencies": { + "react": ">=16.11.0", + "react-dom": ">=16.11.0" + } + }, + "node_modules/rc-field-form": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rc-field-form/-/rc-field-form-2.7.1.tgz", + "integrity": "sha512-vKeSifSJ6HoLaAB+B8aq/Qgm8a3dyxROzCtKNCsBQgiverpc4kWDQihoUwzUj+zNWJOykwSY4dNX3QrGwtVb9A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0", + "@rc-component/async-validator": "^5.0.3", + "rc-util": "^5.32.2" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-image": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/rc-image/-/rc-image-7.12.0.tgz", + "integrity": "sha512-cZ3HTyyckPnNnUb9/DRqduqzLfrQRyi+CdHjdqgsyDpI3Ln5UX1kXnAhPBSJj9pVRzwRFgqkN7p9b6HBDjmu/Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.2", + "@rc-component/portal": "^1.0.2", + "classnames": "^2.2.6", + "rc-dialog": "~9.6.0", + "rc-motion": "^2.6.2", + "rc-util": "^5.34.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-input": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/rc-input/-/rc-input-1.8.0.tgz", + "integrity": "sha512-KXvaTbX+7ha8a/k+eg6SYRVERK0NddX8QX7a7AnRvUa/rEH0CNMlpcBzBkhI0wp2C8C4HlMoYl8TImSN+fuHKA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-util": "^5.18.1" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/rc-input-number": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-9.5.0.tgz", + "integrity": "sha512-bKaEvB5tHebUURAEXw35LDcnRZLq3x1k7GxfAqBMzmpHkDGzjAtnUL8y4y5N15rIFIg5IJgwr211jInl3cipag==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/mini-decimal": "^1.0.1", + "classnames": "^2.2.5", + "rc-input": "~1.8.0", + "rc-util": "^5.40.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-mentions": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/rc-mentions/-/rc-mentions-2.20.0.tgz", + "integrity": "sha512-w8HCMZEh3f0nR8ZEd466ATqmXFCMGMN5UFCzEUL0bM/nGw/wOS2GgRzKBcm19K++jDyuWCOJOdgcKGXU3fXfbQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.22.5", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.2.6", + "rc-input": "~1.8.0", + "rc-menu": "~9.16.0", + "rc-textarea": "~1.10.0", + "rc-util": "^5.34.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-menu": { + "version": "9.16.1", + "resolved": "https://registry.npmjs.org/rc-menu/-/rc-menu-9.16.1.tgz", + "integrity": "sha512-ghHx6/6Dvp+fw8CJhDUHFHDJ84hJE3BXNCzSgLdmNiFErWSOaZNsihDAsKq9ByTALo/xkNIwtDFGIl6r+RPXBg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/trigger": "^2.0.0", + "classnames": "2.x", + "rc-motion": "^2.4.3", + "rc-overflow": "^1.3.1", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-motion": { + "version": "2.9.5", + "resolved": "https://registry.npmjs.org/rc-motion/-/rc-motion-2.9.5.tgz", + "integrity": "sha512-w+XTUrfh7ArbYEd2582uDrEhmBHwK1ZENJiSJVb7uRxdE7qJSYjbO2eksRXmndqyKqKoYPc9ClpPh5242mV1vA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-util": "^5.44.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-notification": { + "version": "5.6.4", + "resolved": "https://registry.npmjs.org/rc-notification/-/rc-notification-5.6.4.tgz", + "integrity": "sha512-KcS4O6B4qzM3KH7lkwOB7ooLPZ4b6J+VMmQgT51VZCeEcmghdeR4IrMcFq0LG+RPdnbe/ArT086tGM8Snimgiw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "rc-motion": "^2.9.0", + "rc-util": "^5.20.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-overflow": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/rc-overflow/-/rc-overflow-1.5.0.tgz", + "integrity": "sha512-Lm/v9h0LymeUYJf0x39OveU52InkdRXqnn2aYXfWmo8WdOonIKB2kfau+GF0fWq6jPgtdO9yMqveGcK6aIhJmg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.37.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-pagination": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/rc-pagination/-/rc-pagination-5.1.0.tgz", + "integrity": "sha512-8416Yip/+eclTFdHXLKTxZvn70duYVGTvUUWbckCCZoIl3jagqke3GLsFrMs0bsQBikiYpZLD9206Ej4SOdOXQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.3.2", + "rc-util": "^5.38.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-picker": { + "version": "4.11.3", + "resolved": "https://registry.npmjs.org/rc-picker/-/rc-picker-4.11.3.tgz", + "integrity": "sha512-MJ5teb7FlNE0NFHTncxXQ62Y5lytq6sh5nUw0iH8OkHL/TjARSEvSHpr940pWgjGANpjCwyMdvsEV55l5tYNSg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.2.1", + "rc-overflow": "^1.3.2", + "rc-resize-observer": "^1.4.0", + "rc-util": "^5.43.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "date-fns": ">= 2.x", + "dayjs": ">= 1.x", + "luxon": ">= 3.x", + "moment": ">= 2.x", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + }, + "peerDependenciesMeta": { + "date-fns": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + } + } + }, + "node_modules/rc-progress": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/rc-progress/-/rc-progress-4.0.0.tgz", + "integrity": "sha512-oofVMMafOCokIUIBnZLNcOZFsABaUw8PPrf1/y0ZBvKZNpOiu5h4AO9vv11Sw0p4Hb3D0yGWuEattcQGtNJ/aw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.6", + "rc-util": "^5.16.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-rate": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/rc-rate/-/rc-rate-2.13.1.tgz", + "integrity": "sha512-QUhQ9ivQ8Gy7mtMZPAjLbxBt5y9GRp65VcUyGUMF3N3fhiftivPHdpuDIaWIMOTEprAjZPC08bls1dQB+I1F2Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.5", + "rc-util": "^5.0.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-resize-observer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/rc-resize-observer/-/rc-resize-observer-1.4.3.tgz", + "integrity": "sha512-YZLjUbyIWox8E9i9C3Tm7ia+W7euPItNWSPX5sCcQTYbnwDb5uNpnLHQCG1f22oZWUhLw4Mv2tFmeWe68CDQRQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.7", + "classnames": "^2.2.1", + "rc-util": "^5.44.1", + "resize-observer-polyfill": "^1.5.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-segmented": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rc-segmented/-/rc-segmented-2.7.1.tgz", + "integrity": "sha512-izj1Nw/Dw2Vb7EVr+D/E9lUTkBe+kKC+SAFSU9zqr7WV2W5Ktaa9Gc7cB2jTqgk8GROJayltaec+DBlYKc6d+g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-motion": "^2.4.4", + "rc-util": "^5.17.0" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/rc-select": { + "version": "14.16.8", + "resolved": "https://registry.npmjs.org/rc-select/-/rc-select-14.16.8.tgz", + "integrity": "sha512-NOV5BZa1wZrsdkKaiK7LHRuo5ZjZYMDxPP6/1+09+FB4KoNi8jcG1ZqLE3AVCxEsYMBe65OBx71wFoHRTP3LRg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/trigger": "^2.1.1", + "classnames": "2.x", + "rc-motion": "^2.0.1", + "rc-overflow": "^1.3.1", + "rc-util": "^5.16.1", + "rc-virtual-list": "^3.5.2" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/rc-slider": { + "version": "11.1.9", + "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-11.1.9.tgz", + "integrity": "sha512-h8IknhzSh3FEM9u8ivkskh+Ef4Yo4JRIY2nj7MrH6GQmrwV6mcpJf5/4KgH5JaVI1H3E52yCdpOlVyGZIeph5A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.5", + "rc-util": "^5.36.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-steps": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rc-steps/-/rc-steps-6.0.1.tgz", + "integrity": "sha512-lKHL+Sny0SeHkQKKDJlAjV5oZ8DwCdS2hFhAkIjuQt1/pB81M0cA0ErVFdHq9+jmPmFw1vJB2F5NBzFXLJxV+g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.16.7", + "classnames": "^2.2.3", + "rc-util": "^5.16.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-switch": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/rc-switch/-/rc-switch-4.1.0.tgz", + "integrity": "sha512-TI8ufP2Az9oEbvyCeVE4+90PDSljGyuwix3fV58p7HV2o4wBnVToEyomJRVyTaZeqNPAp+vqeo4Wnj5u0ZZQBg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.21.0", + "classnames": "^2.2.1", + "rc-util": "^5.30.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-table": { + "version": "7.54.0", + "resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.54.0.tgz", + "integrity": "sha512-/wDTkki6wBTjwylwAGjpLKYklKo9YgjZwAU77+7ME5mBoS32Q4nAwoqhA2lSge6fobLW3Tap6uc5xfwaL2p0Sw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/context": "^1.4.0", + "classnames": "^2.2.5", + "rc-resize-observer": "^1.1.0", + "rc-util": "^5.44.3", + "rc-virtual-list": "^3.14.2" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-tabs": { + "version": "15.7.0", + "resolved": "https://registry.npmjs.org/rc-tabs/-/rc-tabs-15.7.0.tgz", + "integrity": "sha512-ZepiE+6fmozYdWf/9gVp7k56PKHB1YYoDsKeQA1CBlJ/POIhjkcYiv0AGP0w2Jhzftd3AVvZP/K+V+Lpi2ankA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.2", + "classnames": "2.x", + "rc-dropdown": "~4.2.0", + "rc-menu": "~9.16.0", + "rc-motion": "^2.6.2", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.34.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-textarea": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/rc-textarea/-/rc-textarea-1.10.2.tgz", + "integrity": "sha512-HfaeXiaSlpiSp0I/pvWpecFEHpVysZ9tpDLNkxQbMvMz6gsr7aVZ7FpWP9kt4t7DB+jJXesYS0us1uPZnlRnwQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.1", + "rc-input": "~1.8.0", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-tooltip": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/rc-tooltip/-/rc-tooltip-6.4.0.tgz", + "integrity": "sha512-kqyivim5cp8I5RkHmpsp1Nn/Wk+1oeloMv9c7LXNgDxUpGm+RbXJGL+OPvDlcRnx9DBeOe4wyOIl4OKUERyH1g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.2", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.3.1", + "rc-util": "^5.44.3" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-tree": { + "version": "5.13.1", + "resolved": "https://registry.npmjs.org/rc-tree/-/rc-tree-5.13.1.tgz", + "integrity": "sha512-FNhIefhftobCdUJshO7M8uZTA9F4OPGVXqGfZkkD/5soDeOhwO06T/aKTrg0WD8gRg/pyfq+ql3aMymLHCTC4A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "rc-motion": "^2.0.1", + "rc-util": "^5.16.1", + "rc-virtual-list": "^3.5.1" + }, + "engines": { + "node": ">=10.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/rc-tree-select": { + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/rc-tree-select/-/rc-tree-select-5.27.0.tgz", + "integrity": "sha512-2qTBTzwIT7LRI1o7zLyrCzmo5tQanmyGbSaGTIf7sYimCklAToVVfpMC6OAldSKolcnjorBYPNSKQqJmN3TCww==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.7", + "classnames": "2.x", + "rc-select": "~14.16.2", + "rc-tree": "~5.13.0", + "rc-util": "^5.43.0" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/rc-upload": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/rc-upload/-/rc-upload-4.11.0.tgz", + "integrity": "sha512-ZUyT//2JAehfHzjWowqROcwYJKnZkIUGWaTE/VogVrepSl7AFNbQf4+zGfX4zl9Vrj/Jm8scLO0R6UlPDKK4wA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "classnames": "^2.2.5", + "rc-util": "^5.2.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-util": { + "version": "5.44.4", + "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.44.4.tgz", + "integrity": "sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "react-is": "^18.2.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-virtual-list": { + "version": "3.19.2", + "resolved": "https://registry.npmjs.org/rc-virtual-list/-/rc-virtual-list-3.19.2.tgz", + "integrity": "sha512-Ys6NcjwGkuwkeaWBDqfI3xWuZ7rDiQXlH1o2zLfFzATfEgXcqpk8CkgMfbJD81McqjcJVez25a3kPxCR807evA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.0", + "classnames": "^2.2.6", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.36.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz", + "integrity": "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.1.tgz", + "integrity": "sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==", + "license": "MIT", + "dependencies": { + "react-router": "7.13.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/reactcss": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz", + "integrity": "sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==", + "license": "MIT", + "dependencies": { + "lodash": "^4.0.1" + } + }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", + "license": "MIT" + }, + "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.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/scroll-into-view-if-needed": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", + "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==", + "license": "MIT", + "dependencies": { + "compute-scroll-into-view": "^3.0.2" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "license": "MIT" + }, + "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/size-sensor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/size-sensor/-/size-sensor-1.0.3.tgz", + "integrity": "sha512-+k9mJ2/rQMiRmQUcjn+qznch260leIXY8r4FyYKKyRBO/s5UoeMAHGkCJyE1R/4wrIhTJONfyloY55SkE7ve3A==", + "license": "ISC" + }, + "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==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-convert": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz", + "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==", + "license": "MIT" + }, + "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/stylis": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "license": "MIT" + }, + "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/swr": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.4.1.tgz", + "integrity": "sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/throttle-debounce": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz", + "integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==", + "license": "MIT", + "engines": { + "node": ">=12.22" + } + }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==", + "license": "MIT" + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "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/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.0.tgz", + "integrity": "sha512-W8GcigEMEeB07xEZol8oJ26rigm3+bfPHxHvwbYUlu1fUDsGuQ7Hiskx5xGW/xM4USc9Ephe3jtv7ZYPQntHeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.57.0", + "@typescript-eslint/parser": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0", + "@typescript-eslint/utils": "8.57.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.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "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/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "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/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.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/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "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" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, + "node_modules/zrender": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-6.0.0.tgz", + "integrity": "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==", + "license": "BSD-3-Clause", + "dependencies": { + "tslib": "2.3.0" + } + }, + "node_modules/zrender/node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + }, + "node_modules/zustand": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.11.tgz", + "integrity": "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..100534f --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,39 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@ant-design/icons": "^6.1.0", + "@ant-design/pro-components": "^2.8.10", + "@tanstack/react-query": "^5.90.21", + "antd": "^5.29.3", + "dayjs": "^1.11.19", + "echarts": "^6.0.0", + "echarts-for-react": "^3.0.6", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-router-dom": "^7.13.1", + "zustand": "^5.0.11" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.48.0", + "vite": "^7.3.1" + } +} diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..52c1b1b --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,6 @@ +import { useRoutes } from 'react-router-dom'; +import { routes } from './routes'; + +export default function App() { + return useRoutes(routes); +} diff --git a/frontend/src/global.css b/frontend/src/global.css new file mode 100644 index 0000000..cce5efc --- /dev/null +++ b/frontend/src/global.css @@ -0,0 +1,26 @@ +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html, body, #root { + height: 100%; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', + Arial, 'Noto Sans SC', sans-serif; + -webkit-font-smoothing: antialiased; +} + +::-webkit-scrollbar { + width: 6px; + height: 6px; +} +::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.15); + border-radius: 3px; +} +::-webkit-scrollbar-track { + background: transparent; +} diff --git a/frontend/src/layouts/MainLayout.tsx b/frontend/src/layouts/MainLayout.tsx new file mode 100644 index 0000000..9adbe60 --- /dev/null +++ b/frontend/src/layouts/MainLayout.tsx @@ -0,0 +1,120 @@ +import React, { useState } from 'react'; +import { Outlet, useNavigate, useLocation } from 'react-router-dom'; +import { Layout, Menu, Typography, theme, Space } from 'antd'; +import { + FolderOpenOutlined, + DashboardOutlined, + FileImageOutlined, + SwapOutlined, + ApartmentOutlined, + AuditOutlined, + FileTextOutlined, + SafetyCertificateOutlined, +} from '@ant-design/icons'; + +const { Header, Sider, Content } = Layout; + +const menuItems = [ + { key: '/cases', icon: , label: '案件管理' }, + { key: '/cases/1/workspace', icon: , label: '工作台' }, + { key: '/cases/1/screenshots', icon: , label: '截图管理' }, + { key: '/cases/1/transactions', icon: , label: '交易归并' }, + { key: '/cases/1/analysis', icon: , label: '资金分析' }, + { key: '/cases/1/review', icon: , label: '认定复核' }, + { key: '/cases/1/reports', icon: , label: '报告导出' }, +]; + +const MainLayout: React.FC = () => { + const [collapsed, setCollapsed] = useState(false); + const navigate = useNavigate(); + const location = useLocation(); + const { token } = theme.useToken(); + + const selectedKey = menuItems + .map((m) => m.key) + .filter((k) => location.pathname.startsWith(k)) + .sort((a, b) => b.length - a.length)[0] || '/cases'; + + return ( + + +
+ + + {!collapsed && ( + + 智析反诈 + + )} + +
+ navigate(key)} + style={{ border: 'none' }} + /> + + +
+ + 受害人被骗金额归集智能体 + + + 演示环境 · v0.1.0 + +
+ + + +
+ + ); +}; + +export default MainLayout; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..a861d2b --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; +import { ConfigProvider, App as AntApp } from 'antd'; +import { QueryClientProvider } from '@tanstack/react-query'; +import zhCN from 'antd/locale/zh_CN'; +import dayjs from 'dayjs'; +import 'dayjs/locale/zh-cn'; +import App from './App'; +import { queryClient } from './services/queryClient'; +import './global.css'; + +dayjs.locale('zh-cn'); + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + + + + + + + + + , +); diff --git a/frontend/src/mock/data.ts b/frontend/src/mock/data.ts new file mode 100644 index 0000000..17a7159 --- /dev/null +++ b/frontend/src/mock/data.ts @@ -0,0 +1,198 @@ +import type { + CaseRecord, + EvidenceImage, + TransactionRecord, + FraudAssessment, + FundFlowNode, + FundFlowEdge, + ExportReport, +} from '../types'; + +export const mockCases: CaseRecord[] = [ + { + id: '1', + caseNo: 'ZA-2026-001538', + title: '张某被电信诈骗案', + victimName: '张某某', + handler: '李警官', + status: 'reviewing', + imageCount: 24, + totalAmount: 186500.0, + createdAt: '2026-03-08 09:30:00', + updatedAt: '2026-03-10 14:20:00', + }, + { + id: '2', + caseNo: 'ZA-2026-001612', + title: '王某被投资诈骗案', + victimName: '王某', + handler: '李警官', + status: 'analyzing', + imageCount: 18, + totalAmount: 0, + createdAt: '2026-03-09 11:00:00', + updatedAt: '2026-03-10 16:45:00', + }, + { + id: '3', + caseNo: 'ZA-2026-001705', + title: '刘某某被冒充客服诈骗案', + victimName: '刘某某', + handler: '陈警官', + status: 'completed', + imageCount: 12, + totalAmount: 53200.0, + createdAt: '2026-03-05 15:20:00', + updatedAt: '2026-03-07 10:30:00', + }, + { + id: '4', + caseNo: 'ZA-2026-001821', + title: '赵某被刷单诈骗案', + victimName: '赵某', + handler: '王警官', + status: 'pending', + imageCount: 0, + totalAmount: 0, + createdAt: '2026-03-10 08:15:00', + updatedAt: '2026-03-10 08:15:00', + }, + { + id: '5', + caseNo: 'ZA-2026-001890', + title: '陈某被杀猪盘诈骗案', + victimName: '陈某', + handler: '李警官', + status: 'uploading', + imageCount: 8, + totalAmount: 0, + createdAt: '2026-03-11 07:45:00', + updatedAt: '2026-03-11 08:00:00', + }, +]; + +export const mockImages: EvidenceImage[] = [ + { id: 'img-1', caseId: '1', url: '', thumbUrl: '', sourceApp: 'wechat', pageType: 'bill_list', ocrStatus: 'done', hash: 'a1b2', uploadedAt: '2026-03-08 09:35:00' }, + { id: 'img-2', caseId: '1', url: '', thumbUrl: '', sourceApp: 'wechat', pageType: 'bill_detail', ocrStatus: 'done', hash: 'c3d4', uploadedAt: '2026-03-08 09:35:00' }, + { id: 'img-3', caseId: '1', url: '', thumbUrl: '', sourceApp: 'alipay', pageType: 'bill_list', ocrStatus: 'done', hash: 'e5f6', uploadedAt: '2026-03-08 09:36:00' }, + { id: 'img-4', caseId: '1', url: '', thumbUrl: '', sourceApp: 'alipay', pageType: 'transfer_receipt', ocrStatus: 'done', hash: 'g7h8', uploadedAt: '2026-03-08 09:36:00' }, + { id: 'img-5', caseId: '1', url: '', thumbUrl: '', sourceApp: 'bank', pageType: 'bill_detail', ocrStatus: 'done', hash: 'i9j0', uploadedAt: '2026-03-08 09:37:00' }, + { id: 'img-6', caseId: '1', url: '', thumbUrl: '', sourceApp: 'bank', pageType: 'sms_notice', ocrStatus: 'processing', hash: 'k1l2', uploadedAt: '2026-03-08 09:37:00' }, + { id: 'img-7', caseId: '1', url: '', thumbUrl: '', sourceApp: 'digital_wallet', pageType: 'bill_list', ocrStatus: 'done', hash: 'm3n4', uploadedAt: '2026-03-08 09:38:00' }, + { id: 'img-8', caseId: '1', url: '', thumbUrl: '', sourceApp: 'wechat', pageType: 'bill_detail', ocrStatus: 'failed', hash: 'o5p6', uploadedAt: '2026-03-08 09:38:00' }, +]; + +export const mockTransactions: TransactionRecord[] = [ + { + id: 'tx-1', caseId: '1', sourceApp: 'bank', tradeTime: '2026-03-06 10:15:00', + amount: 50000, direction: 'out', counterpartyName: '支付宝-张某某', + counterpartyAccount: '', selfAccountTailNo: '6621', orderNo: 'BK20260306001', + remark: '转账至支付宝', evidenceImageId: 'img-5', confidence: 0.95, + clusterId: 'cl-1', isDuplicate: false, isTransit: true, + }, + { + id: 'tx-2', caseId: '1', sourceApp: 'alipay', tradeTime: '2026-03-06 10:16:00', + amount: 50000, direction: 'in', counterpartyName: '银行卡(6621)', + counterpartyAccount: '', selfAccountTailNo: '', orderNo: 'AL20260306001', + remark: '银行卡转入', evidenceImageId: 'img-3', confidence: 0.92, + clusterId: 'cl-1', isDuplicate: true, isTransit: true, + }, + { + id: 'tx-3', caseId: '1', sourceApp: 'alipay', tradeTime: '2026-03-06 10:25:00', + amount: 50000, direction: 'out', counterpartyName: '李*华', + counterpartyAccount: '138****5678', selfAccountTailNo: '', orderNo: 'AL20260306002', + remark: '投资款', evidenceImageId: 'img-4', confidence: 0.97, + isDuplicate: false, isTransit: false, + }, + { + id: 'tx-4', caseId: '1', sourceApp: 'wechat', tradeTime: '2026-03-07 14:30:00', + amount: 30000, direction: 'out', counterpartyName: '财富管家-客服', + counterpartyAccount: '', selfAccountTailNo: '', orderNo: 'WX20260307001', + remark: '手续费', evidenceImageId: 'img-2', confidence: 0.88, + isDuplicate: false, isTransit: false, + }, + { + id: 'tx-5', caseId: '1', sourceApp: 'wechat', tradeTime: '2026-03-07 16:00:00', + amount: 20000, direction: 'out', counterpartyName: '李*华', + counterpartyAccount: '138****5678', selfAccountTailNo: '', orderNo: 'WX20260307002', + remark: '追加保证金', evidenceImageId: 'img-1', confidence: 0.91, + isDuplicate: false, isTransit: false, + }, + { + id: 'tx-6', caseId: '1', sourceApp: 'digital_wallet', tradeTime: '2026-03-08 09:00:00', + amount: 86500, direction: 'out', counterpartyName: 'USDT-TRC20地址', + counterpartyAccount: 'T9yD...Xk3m', selfAccountTailNo: '', orderNo: 'DW20260308001', + remark: '提币', evidenceImageId: 'img-7', confidence: 0.85, + isDuplicate: false, isTransit: false, + }, + { + id: 'tx-7', caseId: '1', sourceApp: 'bank', tradeTime: '2026-03-07 20:00:00', + amount: 86500, direction: 'out', counterpartyName: '某数字钱包充值', + counterpartyAccount: '', selfAccountTailNo: '6621', orderNo: 'BK20260307002', + remark: '充值', evidenceImageId: 'img-5', confidence: 0.90, + clusterId: 'cl-2', isDuplicate: false, isTransit: true, + }, +]; + +export const mockAssessments: FraudAssessment[] = [ + { + id: 'fa-1', caseId: '1', transactionId: 'tx-3', + transaction: mockTransactions[2], + confidenceLevel: 'high', assessedAmount: 50000, + reason: '受害人经支付宝向涉诈账户"李*华(138****5678)"转账5万元,标注为"投资款",与笔录中描述的对方诱导投资场景吻合。', + reviewStatus: 'confirmed', reviewedBy: '李警官', reviewedAt: '2026-03-10 14:00:00', + }, + { + id: 'fa-2', caseId: '1', transactionId: 'tx-4', + transaction: mockTransactions[3], + confidenceLevel: 'high', assessedAmount: 30000, + reason: '受害人经微信向"财富管家-客服"转账3万元,标注为"手续费",属于诈骗常见话术诱导收取的费用。', + reviewStatus: 'confirmed', reviewedBy: '李警官', reviewedAt: '2026-03-10 14:05:00', + }, + { + id: 'fa-3', caseId: '1', transactionId: 'tx-5', + transaction: mockTransactions[4], + confidenceLevel: 'high', assessedAmount: 20000, + reason: '受害人经微信向同一涉诈账户"李*华"追加转账2万元,标注为"追加保证金",与笔录描述一致。', + reviewStatus: 'pending', + }, + { + id: 'fa-4', caseId: '1', transactionId: 'tx-6', + transaction: mockTransactions[5], + confidenceLevel: 'medium', assessedAmount: 86500, + reason: '受害人通过数字钱包向链上地址提币86500元,该操作发生在诈骗持续期间。但链上地址与其他已知涉诈线索暂无直接关联,建议结合链上追踪结果确认。', + excludeReason: '如经查实该提币为受害人个人操作(如套现自用),应排除。', + reviewStatus: 'pending', + }, + { + id: 'fa-5', caseId: '1', transactionId: 'tx-1', + transaction: mockTransactions[0], + confidenceLevel: 'low', assessedAmount: 0, + reason: '该笔为受害人本人银行卡向支付宝的转账,属于本人账户间资金中转,不直接计入被骗损失。已在资金路径中标记为中转节点。', + excludeReason: '本人账户间互转,仅作为资金路径展示。', + reviewStatus: 'confirmed', reviewedBy: '李警官', reviewedAt: '2026-03-10 14:10:00', + }, +]; + +export const mockFlowNodes: FundFlowNode[] = [ + { id: 'n-bank', label: '银行卡(6621)', type: 'self' }, + { id: 'n-alipay', label: '支付宝', type: 'self' }, + { id: 'n-wechat', label: '微信支付', type: 'self' }, + { id: 'n-wallet', label: '数字钱包', type: 'self' }, + { id: 'n-suspect1', label: '李*华\n138****5678', type: 'suspect' }, + { id: 'n-suspect2', label: '财富管家-客服', type: 'suspect' }, + { id: 'n-chain', label: 'USDT地址\nT9yD...Xk3m', type: 'suspect' }, +]; + +export const mockFlowEdges: FundFlowEdge[] = [ + { source: 'n-bank', target: 'n-alipay', amount: 50000, count: 1, tradeTime: '2026-03-06 10:15' }, + { source: 'n-alipay', target: 'n-suspect1', amount: 50000, count: 1, tradeTime: '2026-03-06 10:25' }, + { source: 'n-wechat', target: 'n-suspect2', amount: 30000, count: 1, tradeTime: '2026-03-07 14:30' }, + { source: 'n-wechat', target: 'n-suspect1', amount: 20000, count: 1, tradeTime: '2026-03-07 16:00' }, + { source: 'n-bank', target: 'n-wallet', amount: 86500, count: 1, tradeTime: '2026-03-07 20:00' }, + { source: 'n-wallet', target: 'n-chain', amount: 86500, count: 1, tradeTime: '2026-03-08 09:00' }, +]; + +export const mockReports: ExportReport[] = [ + { id: 'rpt-1', caseId: '1', type: 'excel', url: '#', createdAt: '2026-03-10 15:00:00', version: 1 }, +]; diff --git a/frontend/src/pages/analysis/Analysis.tsx b/frontend/src/pages/analysis/Analysis.tsx new file mode 100644 index 0000000..fb111fa --- /dev/null +++ b/frontend/src/pages/analysis/Analysis.tsx @@ -0,0 +1,327 @@ +import React, { useMemo } from 'react'; +import { useParams } from 'react-router-dom'; +import { useQuery } from '@tanstack/react-query'; +import { Card, Row, Col, Tag, Typography, Space, Timeline, Statistic, Divider } from 'antd'; +import { + ApartmentOutlined, + ClockCircleOutlined, + ArrowUpOutlined, + ArrowDownOutlined, +} from '@ant-design/icons'; +import ReactECharts from 'echarts-for-react'; +import { fetchTransactions, fetchFlows } from '../../services/api'; + +const nodeColorMap: Record = { + self: '#1677ff', + suspect: '#cf1322', + transit: '#fa8c16', + unknown: '#8c8c8c', +}; + +const Analysis: React.FC = () => { + const { id = '1' } = useParams(); + + const { data: txData } = useQuery({ + queryKey: ['transactions', id], + queryFn: () => fetchTransactions(id), + }); + const { data: flowData } = useQuery({ + queryKey: ['flows', id], + queryFn: () => fetchFlows(id), + }); + + const mockTransactions = txData?.items ?? []; + const mockFlowNodes = flowData?.nodes ?? []; + const mockFlowEdges = flowData?.edges ?? []; + + const flowChartOption = useMemo(() => { + const nodes = mockFlowNodes.map((n: any) => ({ + name: n.label, + symbolSize: n.type === 'suspect' ? 60 : 50, + itemStyle: { color: nodeColorMap[n.type] }, + label: { show: true, fontSize: 11 }, + category: n.type === 'self' ? 0 : n.type === 'suspect' ? 1 : 2, + })); + + const edges = mockFlowEdges.map((e: any) => { + const src = mockFlowNodes.find((n: any) => n.id === e.source); + const tgt = mockFlowNodes.find((n: any) => n.id === e.target); + return { + source: src?.label || '', + target: tgt?.label || '', + value: e.amount, + lineStyle: { + width: Math.max(2, Math.min(8, e.amount / 20000)), + curveness: 0.2, + }, + label: { + show: true, + formatter: `¥${e.amount.toLocaleString()}`, + fontSize: 11, + }, + }; + }); + + return { + tooltip: { trigger: 'item' }, + legend: { + data: ['本人账户', '涉诈账户', '中转账户'], + bottom: 10, + }, + series: [ + { + type: 'graph', + layout: 'force', + roam: true, + draggable: true, + force: { + repulsion: 400, + edgeLength: [120, 200], + }, + categories: [ + { name: '本人账户' }, + { name: '涉诈账户' }, + { name: '中转账户' }, + ], + data: nodes, + links: edges, + edgeSymbol: ['none', 'arrow'], + edgeSymbolSize: 10, + emphasis: { focus: 'adjacency' }, + }, + ], + }; + }, [mockFlowNodes, mockFlowEdges]); + + const timelineChartOption = useMemo(() => { + const sorted = [...mockTransactions] + .filter((t) => !t.isDuplicate) + .sort((a, b) => a.tradeTime.localeCompare(b.tradeTime)); + + return { + tooltip: { + trigger: 'axis', + formatter: (params: any) => { + const p = params[0]; + return `${p.axisValue}
金额: ¥${Math.abs(p.value).toLocaleString()}`; + }, + }, + grid: { left: 80, right: 40, top: 40, bottom: 60 }, + xAxis: { + type: 'category', + data: sorted.map((t) => t.tradeTime.slice(5, 16)), + axisLabel: { rotate: 30, fontSize: 11 }, + }, + yAxis: { + type: 'value', + name: '金额(元)', + axisLabel: { + formatter: (v: number) => `¥${(v / 1000).toFixed(0)}K`, + }, + }, + series: [ + { + type: 'bar', + data: sorted.map((t) => ({ + value: t.direction === 'out' ? -t.amount : t.amount, + itemStyle: { + color: t.direction === 'out' ? '#cf1322' : '#52c41a', + }, + })), + barWidth: 30, + }, + ], + }; + }, [mockTransactions]); + + const validTx = mockTransactions.filter((t) => !t.isDuplicate); + const totalFraud = validTx + .filter((t) => t.direction === 'out' && !t.isTransit) + .reduce((s, t) => s + t.amount, 0); + + const sortedTx = [...validTx].sort((a, b) => + a.tradeTime.localeCompare(b.tradeTime), + ); + + return ( +
+ + + + + + + + + + + + + + + + + + + + + + + 资金流转关系图 + + } + style={{ marginBottom: 24 }} + extra={ + + 本人账户 + 涉诈账户 + 中转账户 + + } + > + + + + + + 交易时间轴 + + } + > + + + 红色柱表示支出,绿色柱表示收入。中转和重复记录已排除。 + + + + + + + ({ + color: tx.direction === 'out' + ? tx.isTransit + ? 'orange' + : 'red' + : 'green', + children: ( +
+ + {tx.tradeTime} + +
+ + + {tx.sourceApp === 'wechat' + ? '微信' + : tx.sourceApp === 'alipay' + ? '支付宝' + : tx.sourceApp === 'bank' + ? '银行' + : '数字钱包'} + + {tx.isTransit && 中转} + +
+ + {tx.direction === 'out' ? '-' : '+'}¥ + {tx.amount.toLocaleString()} + + + → {tx.counterpartyName} + + {tx.remark && ( + <> +
+ + {tx.remark} + + + )} +
+ ), + }))} + /> +
+ + + + {[ + { name: '李*华 (138****5678)', amount: 70000, count: 2, risk: 'high' }, + { name: 'USDT地址 T9yD...Xk3m', amount: 86500, count: 1, risk: 'medium' }, + { name: '财富管家-客服', amount: 30000, count: 1, risk: 'high' }, + ].map((item, idx) => ( + + + + {item.name} +
+ + {item.count} 笔交易 + + + + + + ¥{item.amount.toLocaleString()} + + + {item.risk === 'high' ? '高风险' : '中风险'} + + + +
+
+ ))} +
+
+ +
+
+ ); +}; + +export default Analysis; diff --git a/frontend/src/pages/cases/CaseList.tsx b/frontend/src/pages/cases/CaseList.tsx new file mode 100644 index 0000000..8faf8e3 --- /dev/null +++ b/frontend/src/pages/cases/CaseList.tsx @@ -0,0 +1,256 @@ +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { + Card, + Table, + Button, + Tag, + Space, + Input, + Typography, + Modal, + Form, + Row, + Col, + Statistic, + message, +} from 'antd'; +import { + PlusOutlined, + SearchOutlined, + FolderOpenOutlined, + ClockCircleOutlined, + CheckCircleOutlined, + ExclamationCircleOutlined, +} from '@ant-design/icons'; +import type { ColumnsType } from 'antd/es/table'; +import type { CaseRecord, CaseStatus } from '../../types'; +import { fetchCases, createCase } from '../../services/api'; + +const statusConfig: Record = { + pending: { color: 'default', label: '待处理' }, + uploading: { color: 'processing', label: '上传中' }, + analyzing: { color: 'blue', label: '分析中' }, + reviewing: { color: 'orange', label: '待复核' }, + completed: { color: 'green', label: '已完成' }, +}; + +const CaseList: React.FC = () => { + const navigate = useNavigate(); + const qc = useQueryClient(); + const [createOpen, setCreateOpen] = useState(false); + const [form] = Form.useForm(); + const [search, setSearch] = useState(''); + + const { data, isLoading } = useQuery({ + queryKey: ['cases', search], + queryFn: () => fetchCases({ search: search || undefined }), + }); + + const cases = data?.items ?? []; + + const createMutation = useMutation({ + mutationFn: createCase, + onSuccess: () => { + message.success('案件创建成功'); + qc.invalidateQueries({ queryKey: ['cases'] }); + setCreateOpen(false); + form.resetFields(); + }, + }); + + const totalCases = cases.length; + const pendingReview = cases.filter((c) => c.status === 'reviewing').length; + const completedCount = cases.filter((c) => c.status === 'completed').length; + const analyzingCount = cases.filter( + (c) => c.status === 'analyzing' || c.status === 'uploading', + ).length; + + const columns: ColumnsType = [ + { + title: '案件编号', + dataIndex: 'caseNo', + width: 180, + render: (text, record) => ( + navigate(`/cases/${record.id}/workspace`)}>{text} + ), + }, + { title: '案件名称', dataIndex: 'title', ellipsis: true }, + { title: '受害人', dataIndex: 'victimName', width: 100 }, + { title: '承办人', dataIndex: 'handler', width: 100 }, + { + title: '状态', + dataIndex: 'status', + width: 100, + render: (s: CaseStatus) => ( + {statusConfig[s].label} + ), + }, + { + title: '截图数', + dataIndex: 'imageCount', + width: 80, + align: 'center', + }, + { + title: '识别金额(元)', + dataIndex: 'totalAmount', + width: 140, + align: 'right', + render: (v: number) => + v > 0 ? ( + + ¥{v.toLocaleString('zh-CN', { minimumFractionDigits: 2 })} + + ) : ( + - + ), + }, + { + title: '更新时间', + dataIndex: 'updatedAt', + width: 170, + }, + { + title: '操作', + width: 160, + render: (_, record) => ( + + + + ), + }, + ]; + + return ( +
+ + + + } + /> + + + + + } + valueStyle={{ color: '#1677ff' }} + /> + + + + + } + valueStyle={{ color: '#fa8c16' }} + /> + + + + + } + valueStyle={{ color: '#52c41a' }} + /> + + + + + + } + style={{ width: 240 }} + allowClear + onPressEnter={(e) => setSearch((e.target as HTMLInputElement).value)} + onChange={(e) => !e.target.value && setSearch('')} + /> + + + } + > + `共 ${t} 条` }} + /> + + + setCreateOpen(false)} + onOk={() => { + form.validateFields().then((values) => createMutation.mutate(values)); + }} + confirmLoading={createMutation.isPending} + okText="创建" + cancelText="取消" + destroyOnClose + > +
+ + + + + + + +
+ + + + + + + + + + + + + + ); +}; + +export default CaseList; diff --git a/frontend/src/pages/reports/Reports.tsx b/frontend/src/pages/reports/Reports.tsx new file mode 100644 index 0000000..30ada0a --- /dev/null +++ b/frontend/src/pages/reports/Reports.tsx @@ -0,0 +1,329 @@ +import React, { useState } from 'react'; +import { useParams } from 'react-router-dom'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { + Card, + Button, + Space, + Typography, + Row, + Col, + Statistic, + Table, + Tag, + Divider, + Descriptions, + Select, + Checkbox, + message, + Steps, + Result, +} from 'antd'; +import { + FileTextOutlined, + FileExcelOutlined, + FilePdfOutlined, + FileWordOutlined, + DownloadOutlined, + PrinterOutlined, + HistoryOutlined, + CheckCircleOutlined, +} from '@ant-design/icons'; +import type { ColumnsType } from 'antd/es/table'; +import { fetchCase, fetchAssessments, fetchReports, generateReport, getReportDownloadUrl } from '../../services/api'; + +const Reports: React.FC = () => { + const { id = '1' } = useParams(); + const qc = useQueryClient(); + const [generated, setGenerated] = useState(false); + + const { data: currentCase } = useQuery({ queryKey: ['case', id], queryFn: () => fetchCase(id) }); + const { data: assessData } = useQuery({ queryKey: ['assessments', id], queryFn: () => fetchAssessments(id) }); + const { data: reportsData } = useQuery({ queryKey: ['reports', id], queryFn: () => fetchReports(id) }); + + const allAssessments = assessData?.items ?? []; + const reportsList = reportsData?.items ?? []; + + const confirmedAssessments = allAssessments.filter( + (a) => a.reviewStatus === 'confirmed' && a.assessedAmount > 0, + ); + const totalConfirmed = confirmedAssessments.reduce( + (s, a) => s + a.assessedAmount, + 0, + ); + + const genMutation = useMutation({ + mutationFn: (reportType: string) => generateReport(id, { report_type: reportType }), + onSuccess: () => { + setGenerated(true); + qc.invalidateQueries({ queryKey: ['reports', id] }); + message.success('报告生成成功'); + }, + }); + + if (!currentCase) return null; + + const historyColumns: ColumnsType<(typeof mockReports)[0]> = [ + { + title: '类型', + dataIndex: 'type', + width: 100, + render: (t: string) => { + const map: Record = { + pdf: { icon: , label: 'PDF', color: 'red' }, + excel: { icon: , label: 'Excel', color: 'green' }, + word: { icon: , label: 'Word', color: 'blue' }, + }; + const cfg = map[t] || map.pdf; + return {cfg.label}; + }, + }, + { + title: '版本', + dataIndex: 'version', + width: 80, + render: (v: number) => `v${v}`, + }, + { + title: '生成时间', + dataIndex: 'createdAt', + width: 180, + }, + { + title: '操作', + width: 120, + render: () => ( + + + + ), + }, + ]; + + return ( +
+ +
+ + + + + + + } + valueStyle={{ color: '#52c41a' }} + /> + + + + + } + /> + + + + + + + + + 生成报告 + + } + style={{ marginBottom: 24 }} + > + + + {currentCase.caseNo} + + + {currentCase.title} + + + {currentCase.victimName} + + + {currentCase.handler} + + + + ¥{totalConfirmed.toLocaleString('zh-CN', { minimumFractionDigits: 2 })} + + + + {confirmedAssessments.length} 笔 + + + + + + 导出格式: +
+ + + +
+ Excel 汇总表 +
+
+ 选择 +
+
+ + +
+ PDF 报告 +
+
+ 选择 +
+
+ + +
+ Word 文书 +
+
+ 选择 +
+
+
+
+ + 报告内容: +
+ + 被骗金额汇总表 + 交易明细清单(含证据索引) + 资金流转路径图 + 交易时间轴 + 认定理由与排除说明 + 笔录辅助问询建议 + 原始截图附件 + +
+ + {!generated ? ( + + ) : ( + } + onClick={() => message.info('演示模式:下载 Excel')} + > + 下载 Excel + , + , + , + ]} + /> + )} +
+ + + + + + 历史报告 + + } + style={{ marginBottom: 24 }} + > +
+ + + +
+ + + {generated + ? '点击左侧"下载"查看完整报告' + : '生成报告后可在此预览'} + +
+
+ + + + ); +}; + +export default Reports; diff --git a/frontend/src/pages/review/Review.tsx b/frontend/src/pages/review/Review.tsx new file mode 100644 index 0000000..d347efa --- /dev/null +++ b/frontend/src/pages/review/Review.tsx @@ -0,0 +1,443 @@ +import React, { useState } from 'react'; +import { useParams } from 'react-router-dom'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { + Card, + Table, + Tag, + Typography, + Space, + Button, + Modal, + Input, + Select, + Descriptions, + Row, + Col, + Statistic, + Alert, + Segmented, + Tooltip, + message, + Divider, +} from 'antd'; +import { + AuditOutlined, + CheckCircleOutlined, + CloseCircleOutlined, + QuestionCircleOutlined, + ExclamationCircleOutlined, + SafetyCertificateOutlined, + EyeOutlined, + FileTextOutlined, +} from '@ant-design/icons'; +import type { ColumnsType } from 'antd/es/table'; +import type { FraudAssessment, ConfidenceLevel } from '../../types'; +import { fetchAssessments, submitReview, fetchInquirySuggestions } from '../../services/api'; + +const confidenceConfig: Record< + ConfidenceLevel, + { color: string; label: string } +> = { + high: { color: 'green', label: '高置信' }, + medium: { color: 'orange', label: '中置信' }, + low: { color: 'default', label: '低置信' }, +}; + +const reviewStatusConfig: Record = { + pending: { color: 'orange', label: '待复核', icon: }, + confirmed: { color: 'green', label: '已确认', icon: }, + rejected: { color: 'red', label: '已排除', icon: }, + needs_info: { color: 'blue', label: '需补充', icon: }, +}; + +const Review: React.FC = () => { + const { id = '1' } = useParams(); + const qc = useQueryClient(); + const [filterLevel, setFilterLevel] = useState('all'); + const [reviewModal, setReviewModal] = useState(null); + const [reviewAction, setReviewAction] = useState('confirmed'); + const [reviewNote, setReviewNote] = useState(''); + + const { data: assessData } = useQuery({ + queryKey: ['assessments', id], + queryFn: () => fetchAssessments(id), + }); + const { data: suggestionsData } = useQuery({ + queryKey: ['suggestions', id], + queryFn: () => fetchInquirySuggestions(id), + }); + + const allAssessments = assessData?.items ?? []; + const suggestions = suggestionsData?.suggestions ?? []; + + const reviewMutation = useMutation({ + mutationFn: (params: { assessmentId: string; body: any }) => + submitReview(params.assessmentId, params.body), + onSuccess: () => { + message.success('复核结果已保存'); + qc.invalidateQueries({ queryKey: ['assessments', id] }); + setReviewModal(null); + }, + }); + + const data = + filterLevel === 'all' + ? allAssessments + : allAssessments.filter((a) => a.confidenceLevel === filterLevel); + + const totalConfirmed = allAssessments + .filter((a) => a.reviewStatus === 'confirmed' && a.assessedAmount > 0) + .reduce((s, a) => s + a.assessedAmount, 0); + const totalPending = allAssessments + .filter((a) => a.reviewStatus === 'pending') + .reduce((s, a) => s + a.assessedAmount, 0); + const pendingCount = allAssessments.filter( + (a) => a.reviewStatus === 'pending', + ).length; + const confirmedCount = allAssessments.filter( + (a) => a.reviewStatus === 'confirmed', + ).length; + + const columns: ColumnsType = [ + { + title: '交易时间', + width: 170, + render: (_, r) => r.transaction.tradeTime, + sorter: (a, b) => + a.transaction.tradeTime.localeCompare(b.transaction.tradeTime), + defaultSortOrder: 'ascend', + }, + { + title: '来源', + width: 90, + render: (_, r) => { + const app = r.transaction.sourceApp; + const m: Record = { + wechat: { l: '微信', c: 'green' }, + alipay: { l: '支付宝', c: 'blue' }, + bank: { l: '银行', c: 'purple' }, + digital_wallet: { l: '钱包', c: 'orange' }, + other: { l: '其他', c: 'default' }, + }; + return {m[app].l}; + }, + }, + { + title: '认定金额(元)', + dataIndex: 'assessedAmount', + width: 140, + align: 'right', + render: (v: number) => + v > 0 ? ( + + ¥{v.toLocaleString('zh-CN', { minimumFractionDigits: 2 })} + + ) : ( + 不计入 + ), + }, + { + title: '对方', + render: (_, r) => r.transaction.counterpartyName, + ellipsis: true, + }, + { + title: '置信度', + dataIndex: 'confidenceLevel', + width: 90, + render: (level: ConfidenceLevel) => ( + + {confidenceConfig[level].label} + + ), + }, + { + title: '认定理由', + dataIndex: 'reason', + ellipsis: true, + width: 280, + render: (text: string) => ( + + {text} + + ), + }, + { + title: '复核状态', + dataIndex: 'reviewStatus', + width: 100, + render: (s: string) => { + const cfg = reviewStatusConfig[s]; + return ( + + {cfg.label} + + ); + }, + }, + { + title: '操作', + width: 100, + render: (_, r) => + r.reviewStatus === 'pending' ? ( + + ) : ( + + ), + }, + ]; + + return ( +
+ +
+ + + + + + + + + + + + } + valueStyle={{ color: '#52c41a' }} + /> + + + + + } + valueStyle={{ color: '#fa8c16' }} + /> + + + + + {pendingCount > 0 && ( + + )} + + + + 认定复核 + + } + extra={ + setFilterLevel(v as string)} + options={[ + { label: '全部', value: 'all' }, + { label: '高置信', value: 'high' }, + { label: '中置信', value: 'medium' }, + { label: '低置信', value: 'low' }, + ]} + /> + } + > +
+ + + + + + + 笔录辅助问询建议 + + } + style={{ background: '#fffbe6', borderColor: '#ffe58f' }} + > + + 基于当前识别和分析结果,建议在笔录中向受害人追问以下问题: + +
    + {suggestions.map((s, idx) => ( +
  1. + {s} +
  2. + ))} +
+
+ + setReviewModal(null)} + footer={ + reviewModal?.reviewStatus === 'pending' + ? [ + , + , + ] + : [ + , + ] + } + width={600} + destroyOnClose + > + {reviewModal && ( + <> + + + {reviewModal.transaction.tradeTime} + + + + ¥{reviewModal.assessedAmount.toLocaleString('zh-CN', { minimumFractionDigits: 2 })} + + + + {reviewModal.transaction.counterpartyName} + + + {reviewModal.transaction.sourceApp} + + + + {confidenceConfig[reviewModal.confidenceLevel].label} + + + + + + 认定理由: +
+ {reviewModal.reason} +
+ + {reviewModal.excludeReason && ( + + 排除条件: +
+ {reviewModal.excludeReason} +
+ )} + + {reviewModal.reviewStatus === 'pending' && ( + <> + + + 复核决定: + ({ + label: `${v.label} (${appCounts[k] || 0})`, + value: k, + })), + ]} + /> + + + } + > + + {filtered.map((img) => ( +
+ handleSelect(img)} + styles={{ + body: { padding: 12 }, + }} + cover={ +
+ + + 截图预览区 + +
+ {ocrStatusIcon[img.ocrStatus]} +
+
+ + {appLabel[img.sourceApp].label} + +
+
+ } + > + + {pageTypeLabel[img.pageType]} + +
+ + {img.uploadedAt} + +
+ + ))} + + + {filtered.length === 0 && } + + + + + {appLabel[selectedImage.sourceApp].label} + + {pageTypeLabel[selectedImage.pageType]} + + + ) : '截图详情' + } + placement="right" + width={560} + open={drawerOpen} + onClose={() => setDrawerOpen(false)} + > + {selectedImage && ( + <> +
+ + + + 原始截图预览 + + + +
+ + OCR 提取字段 + + 以下为系统自动提取结果,可手工修正 + + + ( + + = 0.95 + ? 'green' + : item.confidence >= 0.85 + ? 'orange' + : 'red' + } + > + {(item.confidence * 100).toFixed(0)}% + + + + } + > + + {item.label} + + } + description={ + {item.value} + } + /> + + )} + /> + + + + + + {selectedImage.id} + + + {selectedImage.hash} + + + {selectedImage.uploadedAt} + + + + )} +
+ + ); +}; + +export default Screenshots; diff --git a/frontend/src/pages/transactions/Transactions.tsx b/frontend/src/pages/transactions/Transactions.tsx new file mode 100644 index 0000000..7f209f5 --- /dev/null +++ b/frontend/src/pages/transactions/Transactions.tsx @@ -0,0 +1,335 @@ +import React, { useState } from 'react'; +import { useParams } from 'react-router-dom'; +import { useQuery } from '@tanstack/react-query'; +import { + Card, + Table, + Tag, + Typography, + Space, + Select, + Tooltip, + Badge, + Drawer, + Descriptions, + Button, + Alert, + Row, + Col, + Statistic, +} from 'antd'; +import { + SwapOutlined, + WarningOutlined, + LinkOutlined, + EyeOutlined, + ArrowUpOutlined, + ArrowDownOutlined, +} from '@ant-design/icons'; +import type { ColumnsType } from 'antd/es/table'; +import type { TransactionRecord, SourceApp } from '../../types'; +import { fetchTransactions } from '../../services/api'; + +const appTag: Record = { + wechat: { label: '微信', color: 'green' }, + alipay: { label: '支付宝', color: 'blue' }, + bank: { label: '银行', color: 'purple' }, + digital_wallet: { label: '数字钱包', color: 'orange' }, + other: { label: '其他', color: 'default' }, +}; + +const Transactions: React.FC = () => { + const { id = '1' } = useParams(); + const [filterDuplicate, setFilterDuplicate] = useState('all'); + const [detail, setDetail] = useState(null); + + const { data: txData } = useQuery({ + queryKey: ['transactions', id], + queryFn: () => fetchTransactions(id), + }); + const allTransactions = txData?.items ?? []; + + const data = + filterDuplicate === 'all' + ? allTransactions + : filterDuplicate === 'unique' + ? allTransactions.filter((t) => !t.isDuplicate) + : allTransactions.filter((t) => t.isDuplicate); + + const totalOut = allTransactions + .filter((t) => t.direction === 'out' && !t.isDuplicate) + .reduce((s, t) => s + t.amount, 0); + const totalIn = allTransactions + .filter((t) => t.direction === 'in' && !t.isDuplicate) + .reduce((s, t) => s + t.amount, 0); + const duplicateCount = allTransactions.filter((t) => t.isDuplicate).length; + const transitCount = allTransactions.filter((t) => t.isTransit).length; + + const columns: ColumnsType = [ + { + title: '交易时间', + dataIndex: 'tradeTime', + width: 170, + sorter: (a, b) => a.tradeTime.localeCompare(b.tradeTime), + defaultSortOrder: 'ascend', + }, + { + title: '来源', + dataIndex: 'sourceApp', + width: 100, + render: (app: SourceApp) => ( + {appTag[app].label} + ), + }, + { + title: '金额(元)', + dataIndex: 'amount', + width: 140, + align: 'right', + render: (v: number, r) => ( + + {r.direction === 'out' ? '-' : '+'}¥{v.toLocaleString('zh-CN', { minimumFractionDigits: 2 })} + + ), + }, + { + title: '方向', + dataIndex: 'direction', + width: 70, + align: 'center', + render: (d: string) => + d === 'out' ? ( + + ) : ( + + ), + }, + { + title: '对方', + dataIndex: 'counterpartyName', + ellipsis: true, + }, + { + title: '备注', + dataIndex: 'remark', + width: 120, + ellipsis: true, + }, + { + title: '标记', + width: 130, + render: (_, r) => ( + + {r.isDuplicate && ( + + 重复 + + )} + {r.isTransit && ( + + 中转 + + )} + {!r.isDuplicate && !r.isTransit && ( + 有效 + )} + + ), + }, + { + title: '置信度', + dataIndex: 'confidence', + width: 80, + align: 'center', + render: (v: number) => ( + = 0.9 ? '#52c41a' : v >= 0.8 ? '#faad14' : '#ff4d4f'} + text={`${(v * 100).toFixed(0)}%`} + /> + ), + }, + { + title: '操作', + width: 80, + render: (_, r) => ( + + ), + }, + ]; + + return ( +
+ +
+ + + + + + + + + + + + } + /> + + + + + } + /> + + + + + {duplicateCount > 0 && ( + + )} + + + + 交易明细 + {allTransactions.length} 笔 + + } + extra={ +
+ r.isDuplicate + ? 'row-duplicate' + : r.isTransit + ? 'row-transit' + : '' + } + size="middle" + /> + + + setDetail(null)} + > + {detail && ( + + + {detail.tradeTime} + + + + {appTag[detail.sourceApp].label} + + + + + {detail.direction === 'out' ? '-' : '+'}¥ + {detail.amount.toLocaleString('zh-CN', { minimumFractionDigits: 2 })} + + + + {detail.counterpartyName} + + + {detail.counterpartyAccount || '-'} + + + {detail.selfAccountTailNo || '-'} + + + {detail.orderNo} + + {detail.remark} + + {(detail.confidence * 100).toFixed(0)}% + + + + + + {detail.clusterId || '独立交易'} + + + + {detail.isDuplicate && 重复} + {detail.isTransit && 中转} + {!detail.isDuplicate && !detail.isTransit && ( + 有效 + )} + + + + )} + + + ); +}; + +export default Transactions; diff --git a/frontend/src/pages/workspace/Workspace.tsx b/frontend/src/pages/workspace/Workspace.tsx new file mode 100644 index 0000000..f74bc84 --- /dev/null +++ b/frontend/src/pages/workspace/Workspace.tsx @@ -0,0 +1,300 @@ +import React, { useState } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { useQuery } from '@tanstack/react-query'; +import { + Card, + Steps, + Row, + Col, + Statistic, + Typography, + Upload, + Button, + Space, + Tag, + Descriptions, + Progress, + Alert, + Divider, + message, +} from 'antd'; +import { + CloudUploadOutlined, + ScanOutlined, + MergeCellsOutlined, + ApartmentOutlined, + AuditOutlined, + FileTextOutlined, + InboxOutlined, + RightOutlined, +} from '@ant-design/icons'; +import { fetchCase, fetchImages, fetchTransactions, fetchAssessments, uploadImages } from '../../services/api'; + +const { Dragger } = Upload; + +const Workspace: React.FC = () => { + const { id = '1' } = useParams(); + const navigate = useNavigate(); + const [currentStep, setCurrentStep] = useState(3); + + const { data: currentCase } = useQuery({ queryKey: ['case', id], queryFn: () => fetchCase(id) }); + const { data: imagesData } = useQuery({ queryKey: ['images', id], queryFn: () => fetchImages(id) }); + const { data: txData } = useQuery({ queryKey: ['transactions', id], queryFn: () => fetchTransactions(id) }); + const { data: assessData } = useQuery({ queryKey: ['assessments', id], queryFn: () => fetchAssessments(id) }); + + const images = imagesData ?? []; + const txList = txData?.items ?? []; + const assessments = assessData?.items ?? []; + + const highConfirm = assessments.filter((a) => a.confidenceLevel === 'high').length; + const pendingReview = assessments.filter((a) => a.reviewStatus === 'pending').length; + + if (!currentCase) return null; + + const steps = [ + { + title: '上传截图', + icon: , + description: `${images.length} 张已上传`, + }, + { + title: 'OCR识别', + icon: , + description: `${images.filter((i: any) => i.ocrStatus === 'done').length}/${images.length} 已完成`, + }, + { + title: '交易归并', + icon: , + description: `${txList.length} 笔交易`, + }, + { + title: '资金分析', + icon: , + description: '已生成路径图', + }, + { + title: '认定复核', + icon: , + description: `${pendingReview} 笔待复核`, + }, + { + title: '报告导出', + icon: , + description: '待生成', + }, + ]; + + return ( +
+ + +
+ + + + {currentCase.title} + + 待复核 + + + {currentCase.caseNo} · 承办人:{currentCase.handler} · 受害人:{currentCase.victimName} + + + + + + + + + + + setCurrentStep(v)} + /> + + + + + + 支持 JPG/PNG,可批量拖拽 + + } + > + { + uploadImages(id, fileList as unknown as File[]) + .then(() => message.success('截图上传成功')) + .catch(() => message.error('上传失败')); + return false; + }} + style={{ padding: '20px 0' }} + > +

+ +

+

+ 点击或拖拽手机账单截图到此区域 +

+

+ 支持微信、支付宝、银行APP、数字钱包等多种来源截图 +

+
+
+ + + + +
+ i.ocrStatus === 'done').length / + images.length) * + 100 : 0, + )} + size={80} + /> +
+ OCR 识别率 +
+
+ + +
+ !t.isDuplicate).length / + txList.length) * + 100 : 0, + )} + size={80} + strokeColor="#52c41a" + /> +
+ 去重有效率 +
+
+ + +
+ +
+ 高置信占比 +
+
+ + + + + + + + + + {images.length} 张 + + + + 微信 + 支付宝 + 银行 + 数字钱包 + + + + {txList.length} 笔 + + + {txList.filter((t) => !t.isDuplicate).length} 笔 + + 2 个 + + + {pendingReview} 笔 + + + + + + + + + + + + + + + {pendingReview > 0 && ( + navigate('/cases/1/review')} + > + 立即复核 + + } + /> + )} + + + + ); +}; + +export default Workspace; diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx new file mode 100644 index 0000000..f161395 --- /dev/null +++ b/frontend/src/routes.tsx @@ -0,0 +1,44 @@ +import React, { lazy, Suspense } from 'react'; +import type { RouteObject } from 'react-router-dom'; +import { Navigate } from 'react-router-dom'; +import { Spin } from 'antd'; +import MainLayout from './layouts/MainLayout'; + +const CaseList = lazy(() => import('./pages/cases/CaseList')); +const Workspace = lazy(() => import('./pages/workspace/Workspace')); +const Screenshots = lazy(() => import('./pages/screenshots/Screenshots')); +const Transactions = lazy(() => import('./pages/transactions/Transactions')); +const Analysis = lazy(() => import('./pages/analysis/Analysis')); +const Review = lazy(() => import('./pages/review/Review')); +const Reports = lazy(() => import('./pages/reports/Reports')); + +const Loading = () => ( +
+ +
+); + +function withSuspense(Component: React.LazyExoticComponent) { + return ( + }> + + + ); +} + +export const routes: RouteObject[] = [ + { + path: '/', + element: , + children: [ + { index: true, element: }, + { path: 'cases', element: withSuspense(CaseList) }, + { path: 'cases/:id/workspace', element: withSuspense(Workspace) }, + { path: 'cases/:id/screenshots', element: withSuspense(Screenshots) }, + { path: 'cases/:id/transactions', element: withSuspense(Transactions) }, + { path: 'cases/:id/analysis', element: withSuspense(Analysis) }, + { path: 'cases/:id/review', element: withSuspense(Review) }, + { path: 'cases/:id/reports', element: withSuspense(Reports) }, + ], + }, +]; diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts new file mode 100644 index 0000000..66b6c70 --- /dev/null +++ b/frontend/src/services/api.ts @@ -0,0 +1,197 @@ +/** + * Centralized API client with automatic fallback to mock data. + * + * When the backend is available, all calls go to /api/v1/... + * When the backend is down (e.g. demo-only mode), responses fall back + * to the mock data in ../mock/data.ts so the frontend always works. + */ +import type { + CaseRecord, + EvidenceImage, + TransactionRecord, + FraudAssessment, + ExportReport, +} from '../types'; +import { + mockCases, + mockImages, + mockTransactions, + mockAssessments, + mockFlowNodes, + mockFlowEdges, + mockReports, +} from '../mock/data'; + +const BASE = '/api/v1'; + +async function request(url: string, options?: RequestInit): Promise { + const resp = await fetch(url, { + headers: { 'Content-Type': 'application/json', ...options?.headers }, + ...options, + }); + if (!resp.ok) throw new Error(`API Error ${resp.status}`); + return resp.json(); +} + +// ── helpers ── + +let backendAlive: boolean | null = null; + +async function isBackendUp(): Promise { + if (backendAlive !== null) return backendAlive; + try { + const r = await fetch('/health', { signal: AbortSignal.timeout(2000) }); + backendAlive = r.ok; + } catch { + backendAlive = false; + } + return backendAlive; +} + +export function resetBackendCheck() { + backendAlive = null; +} + +// ── Cases ── + +export async function fetchCases(params?: { + offset?: number; + limit?: number; + status?: string; + search?: string; +}): Promise<{ items: CaseRecord[]; total: number }> { + if (!(await isBackendUp())) return { items: mockCases, total: mockCases.length }; + const qs = new URLSearchParams(); + if (params?.offset) qs.set('offset', String(params.offset)); + if (params?.limit) qs.set('limit', String(params.limit)); + if (params?.status) qs.set('status', params.status); + if (params?.search) qs.set('search', params.search); + return request(`${BASE}/cases?${qs}`); +} + +export async function createCase(body: { + case_no: string; + title: string; + victim_name: string; + handler?: string; +}): Promise { + if (!(await isBackendUp())) return mockCases[0]; + return request(`${BASE}/cases`, { method: 'POST', body: JSON.stringify(body) }); +} + +export async function fetchCase(id: string): Promise { + if (!(await isBackendUp())) return mockCases.find((c) => c.id === id) || mockCases[0]; + return request(`${BASE}/cases/${id}`); +} + +// ── Images ── + +export async function fetchImages( + caseId: string, + params?: { source_app?: string; page_type?: string }, +): Promise { + if (!(await isBackendUp())) return mockImages.filter((i) => i.caseId === caseId || caseId === '1'); + const qs = new URLSearchParams(); + if (params?.source_app) qs.set('source_app', params.source_app); + if (params?.page_type) qs.set('page_type', params.page_type); + return request(`${BASE}/cases/${caseId}/images?${qs}`); +} + +export async function uploadImages(caseId: string, files: File[]): Promise { + if (!(await isBackendUp())) return mockImages.slice(0, files.length); + const form = new FormData(); + files.forEach((f) => form.append('files', f)); + const resp = await fetch(`${BASE}/cases/${caseId}/images`, { method: 'POST', body: form }); + if (!resp.ok) throw new Error(`Upload failed ${resp.status}`); + return resp.json(); +} + +// ── Analysis ── + +export async function triggerAnalysis(caseId: string): Promise<{ task_id: string; message: string }> { + if (!(await isBackendUp())) return { task_id: 'mock', message: '分析任务已提交' }; + return request(`${BASE}/cases/${caseId}/analyze`, { method: 'POST' }); +} + +export async function fetchAnalysisStatus( + caseId: string, +): Promise<{ status: string; progress: number; current_step: string }> { + if (!(await isBackendUp())) return { status: 'reviewing', progress: 85, current_step: '待复核' }; + return request(`${BASE}/cases/${caseId}/analyze/status`); +} + +// ── Transactions ── + +export async function fetchTransactions( + caseId: string, + filterType?: string, +): Promise<{ items: TransactionRecord[]; total: number }> { + if (!(await isBackendUp())) return { items: mockTransactions, total: mockTransactions.length }; + const qs = filterType ? `?filter_type=${filterType}` : ''; + return request(`${BASE}/cases/${caseId}/transactions${qs}`); +} + +// ── Flows ── + +export async function fetchFlows( + caseId: string, +): Promise<{ nodes: any[]; edges: any[] }> { + if (!(await isBackendUp())) return { nodes: mockFlowNodes, edges: mockFlowEdges }; + return request(`${BASE}/cases/${caseId}/flows`); +} + +// ── Assessments ── + +export async function fetchAssessments( + caseId: string, + confidenceLevel?: string, +): Promise<{ items: FraudAssessment[]; total: number }> { + if (!(await isBackendUp())) return { items: mockAssessments, total: mockAssessments.length }; + const qs = confidenceLevel ? `?confidence_level=${confidenceLevel}` : ''; + return request(`${BASE}/cases/${caseId}/assessments${qs}`); +} + +export async function submitReview( + assessmentId: string, + body: { review_status: string; review_note?: string; reviewed_by?: string }, +): Promise { + if (!(await isBackendUp())) return mockAssessments[0]; + return request(`${BASE}/assessments/${assessmentId}/review`, { + method: 'POST', + body: JSON.stringify(body), + }); +} + +export async function fetchInquirySuggestions(caseId: string): Promise<{ suggestions: string[] }> { + if (!(await isBackendUp())) + return { + suggestions: [ + '请向受害人核实是否受对方诱导操作转账。', + '是否还有未截图的转账记录?', + '涉案金额中是否有已追回款项?', + ], + }; + return request(`${BASE}/cases/${caseId}/inquiry-suggestions`); +} + +// ── Reports ── + +export async function generateReport( + caseId: string, + body: { report_type: string }, +): Promise { + if (!(await isBackendUp())) return mockReports[0]; + return request(`${BASE}/cases/${caseId}/reports`, { + method: 'POST', + body: JSON.stringify(body), + }); +} + +export async function fetchReports(caseId: string): Promise<{ items: ExportReport[]; total: number }> { + if (!(await isBackendUp())) return { items: mockReports, total: mockReports.length }; + return request(`${BASE}/cases/${caseId}/reports`); +} + +export function getReportDownloadUrl(reportId: string): string { + return `${BASE}/reports/${reportId}/download`; +} diff --git a/frontend/src/services/queryClient.ts b/frontend/src/services/queryClient.ts new file mode 100644 index 0000000..d5e93b4 --- /dev/null +++ b/frontend/src/services/queryClient.ts @@ -0,0 +1,10 @@ +import { QueryClient } from '@tanstack/react-query'; + +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 30_000, + retry: 1, + }, + }, +}); diff --git a/frontend/src/store/caseStore.ts b/frontend/src/store/caseStore.ts new file mode 100644 index 0000000..8692feb --- /dev/null +++ b/frontend/src/store/caseStore.ts @@ -0,0 +1,12 @@ +import { create } from 'zustand'; +import type { CaseRecord } from '../types'; + +interface CaseState { + currentCase: CaseRecord | null; + setCurrentCase: (c: CaseRecord | null) => void; +} + +export const useCaseStore = create((set) => ({ + currentCase: null, + setCurrentCase: (c) => set({ currentCase: c }), +})); diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts new file mode 100644 index 0000000..eea0fa1 --- /dev/null +++ b/frontend/src/types/index.ts @@ -0,0 +1,88 @@ +export type CaseStatus = 'pending' | 'uploading' | 'analyzing' | 'reviewing' | 'completed'; + +export interface CaseRecord { + id: string; + caseNo: string; + title: string; + victimName: string; + handler: string; + status: CaseStatus; + imageCount: number; + totalAmount: number; + createdAt: string; + updatedAt: string; +} + +export type PageType = 'bill_list' | 'bill_detail' | 'transfer_receipt' | 'sms_notice' | 'balance' | 'unknown'; +export type SourceApp = 'wechat' | 'alipay' | 'bank' | 'digital_wallet' | 'other'; + +export interface EvidenceImage { + id: string; + caseId: string; + url: string; + thumbUrl: string; + sourceApp: SourceApp; + pageType: PageType; + ocrStatus: 'pending' | 'processing' | 'done' | 'failed'; + hash: string; + uploadedAt: string; +} + +export interface TransactionRecord { + id: string; + caseId: string; + sourceApp: SourceApp; + tradeTime: string; + amount: number; + direction: 'in' | 'out'; + counterpartyName: string; + counterpartyAccount: string; + selfAccountTailNo: string; + orderNo: string; + remark: string; + evidenceImageId: string; + confidence: number; + clusterId?: string; + isDuplicate: boolean; + isTransit: boolean; +} + +export type ConfidenceLevel = 'high' | 'medium' | 'low'; + +export interface FraudAssessment { + id: string; + caseId: string; + transactionId: string; + transaction: TransactionRecord; + confidenceLevel: ConfidenceLevel; + assessedAmount: number; + reason: string; + excludeReason?: string; + reviewStatus: 'pending' | 'confirmed' | 'rejected' | 'needs_info'; + reviewNote?: string; + reviewedBy?: string; + reviewedAt?: string; +} + +export interface FundFlowNode { + id: string; + label: string; + type: 'self' | 'suspect' | 'transit' | 'unknown'; +} + +export interface FundFlowEdge { + source: string; + target: string; + amount: number; + count: number; + tradeTime: string; +} + +export interface ExportReport { + id: string; + caseId: string; + type: 'pdf' | 'excel' | 'word'; + url: string; + createdAt: string; + version: number; +} diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json new file mode 100644 index 0000000..a9b5a59 --- /dev/null +++ b/frontend/tsconfig.app.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..8a67f62 --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..afdd12f --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], + server: { + proxy: { + '/api': { + target: 'http://localhost:8000', + changeOrigin: true, + }, + }, + }, +}) diff --git a/infra/docker/docker-compose.yml b/infra/docker/docker-compose.yml new file mode 100644 index 0000000..16934fe --- /dev/null +++ b/infra/docker/docker-compose.yml @@ -0,0 +1,29 @@ +services: + postgres: + image: postgres:16-alpine + environment: + POSTGRES_USER: fundtracer + POSTGRES_PASSWORD: fundtracer_dev + POSTGRES_DB: fundtracer + ports: + - "5432:5432" + volumes: + - pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U fundtracer"] + interval: 5s + timeout: 3s + retries: 5 + + redis: + image: redis:7-alpine + ports: + - "6379:6379" + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 3s + retries: 5 + +volumes: + pgdata: diff --git a/infra/scripts/start-dev.sh b/infra/scripts/start-dev.sh new file mode 100755 index 0000000..742bcbe --- /dev/null +++ b/infra/scripts/start-dev.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +echo "==> Starting infrastructure (PostgreSQL + Redis)..." +cd "$PROJECT_ROOT/infra/docker" +docker compose up -d +echo " Waiting for services to be healthy..." +sleep 3 + +echo "==> Installing backend dependencies..." +cd "$PROJECT_ROOT/backend" +pip install -e ".[dev]" -q + +echo "==> Running database migrations..." +cd "$PROJECT_ROOT/backend" +alembic upgrade head 2>/dev/null || echo " (run 'alembic revision --autogenerate' first if no migrations exist)" + +echo "==> Seeding demo data..." +cd "$PROJECT_ROOT/backend" +python -m scripts.seed 2>/dev/null || echo " (seed may fail if data already exists)" + +echo "==> Starting backend (uvicorn)..." +cd "$PROJECT_ROOT/backend" +uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 & +BACKEND_PID=$! + +echo "==> Starting frontend (vite)..." +cd "$PROJECT_ROOT/frontend" +npm run dev & +FRONTEND_PID=$! + +echo "" +echo "======================================" +echo " Backend: http://localhost:8000" +echo " Frontend: http://localhost:5173" +echo " API Docs: http://localhost:8000/docs" +echo "======================================" +echo "" +echo "Press Ctrl+C to stop all services." + +trap "kill $BACKEND_PID $FRONTEND_PID 2>/dev/null; docker compose -f $PROJECT_ROOT/infra/docker/docker-compose.yml down" EXIT +wait