commit 91f701139fec2e5d8093173e519eaea85e914528 Author: xucheng Date: Mon Oct 20 15:20:32 2025 +0800 Initial commit: Fundamental stock analysis project setup diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..45efc53 --- /dev/null +++ b/.gitignore @@ -0,0 +1,314 @@ +# ===== 通用文件 ===== +# 操作系统生成的文件 +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# IDE 和编辑器 +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# 日志文件 +*.log +logs/ + +# 临时文件 +*.tmp +*.temp +.cache/ + +# ===== Python 后端 ===== +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +Pipfile.lock + +# poetry +poetry.lock + +# pdm +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582 +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.env.* +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +.idea/ + +# ===== Node.js / Next.js 前端 ===== +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage +.grunt + +# Bower dependency directory +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons +build/Release + +# Dependency directories +jspm_packages/ + +# Snowpack dependency directory +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# parcel-bundler cache +.parcel-cache + +# Next.js build output +.next/ +out/ + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +public + +# Storybook build outputs +.out +.storybook-out +storybook-static + +# Temporary folders +tmp/ +temp/ + +# Vercel +.vercel + +# Turbo +.turbo + +# ===== 数据库 ===== +# SQLite +*.sqlite +*.sqlite3 +*.db + +# PostgreSQL +*.sql + +# MySQL +*.mysql + +# ===== 配置和密钥 ===== +# 环境变量文件 +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# API 密钥和配置 +config/secrets.json +secrets/ +*.key +*.pem +*.p12 +*.pfx + +# ===== 项目特定 ===== +# 数据文件 +data/ +*.csv +*.json.bak +*.xlsx + +# 报告和输出 +reports/ +output/ +exports/ + +# 备份文件 +*.bak +*.backup +backup/ + +# 测试数据 +test_data/ +mock_data/ \ No newline at end of file diff --git a/.kiro/specs/fundamental-stock-analysis/design.md b/.kiro/specs/fundamental-stock-analysis/design.md new file mode 100644 index 0000000..3764930 --- /dev/null +++ b/.kiro/specs/fundamental-stock-analysis/design.md @@ -0,0 +1,351 @@ +# 设计文档 - 基本面选股系统 + +## 概览 + +基本面选股系统是一个全栈Web应用,采用前后端分离架构。前端使用Next.js和shadcn/ui构建响应式中文界面,后端使用Python FastAPI提供API服务。系统通过多个专业分析模块,结合财务数据API、AI大模型和实时数据,为用户提供全面的股票基本面分析报告。 + +## 架构 + +### 系统架构图 + +```mermaid +graph TB + subgraph "前端层" + A[Next.js应用] --> B[shadcn/ui UI组件] + A --> C[TradingView图表组件] + end + + subgraph "后端层" + D[FastAPI服务器] --> E[报告生成引擎] + D --> F[数据获取服务] + D --> G[配置管理服务] + end + + subgraph "外部服务" + H[Tushare API] + I[Gemini API] + J[其他数据源APIs] + end + + subgraph "数据层" + K[PostgreSQL数据库] + end + + A --> D + F --> H + F --> I + F --> J + E --> K + G --> K +``` + +### 技术栈 + +**前端:** +- Next.js 14 (App Router) +- TypeScript +- shadcn/ui组件库 (https://ui.shadcn.com/) +- TradingView Charting Library +- Tailwind CSS +- Radix UI (shadcn/ui的底层组件) + +**后端:** +- Python 3.11+ +- FastAPI +- SQLAlchemy (ORM) +- Alembic (数据库迁移) +- Pydantic (数据验证) +- httpx (HTTP客户端) + +**数据库:** +- PostgreSQL 15+ + +**外部服务:** +- Tushare API (中国股票数据) +- Google Gemini API (AI分析) +- 其他市场数据源APIs + +## 组件和接口 + +### shadcn/ui组件使用规划 + +系统将使用shadcn/ui (https://ui.shadcn.com/) 的官方组件来构建一致的用户界面: + +**核心组件使用:** +- `Button`: 主要操作按钮(生成报告、保存配置等) +- `Input`: 证券代码输入框 +- `Select`: 交易市场选择器 +- `Card`: 分析模块容器、报告卡片 +- `Progress`: 报告生成进度条 +- `Badge`: 状态标识(完成、进行中、失败) +- `Tabs`: 分析模块切换 +- `Form`: 配置表单、搜索表单 +- `Alert`: 错误提示、成功消息 +- `Separator`: 内容分隔线 +- `Skeleton`: 加载占位符 +- `Toast`: 操作反馈通知 +- `Table`: 财务数据展示表格(资产负债表、利润表、现金流量表等) + +**主题配置:** +- 使用默认主题,支持深色/浅色模式切换 +- 自定义中文字体配置 +- 适配中文内容的间距和排版 + +### 前端组件结构 + +``` +src/ +├── app/ +│ ├── page.tsx # 首页 +│ ├── report/[symbol]/page.tsx # 报告页面 +│ ├── config/page.tsx # 配置页面 +│ └── layout.tsx # 根布局 +├── components/ +│ ├── ui/ # shadcn/ui基础组件 (Button, Input, Card, etc.) +│ ├── StockSearchForm.tsx # 股票搜索表单 (使用Form, Input, Select) +│ ├── ReportProgress.tsx # 报告生成进度 (使用Progress, Badge, Card) +│ ├── TradingViewChart.tsx # TradingView图表 +│ ├── AnalysisModule.tsx # 分析模块容器 (使用Card, Tabs, Separator) +│ ├── FinancialDataTable.tsx # 财务数据表格 (使用Table, TableHeader, TableBody, TableRow, TableCell) +│ └── ConfigForm.tsx # 配置表单 (使用Form, Input, Button, Alert) +├── lib/ +│ ├── api.ts # API客户端 +│ ├── types.ts # TypeScript类型定义 +│ └── utils.ts # 工具函数 +└── hooks/ + ├── useReport.ts # 报告数据钩子 + └── useProgress.ts # 进度追踪钩子 +``` + +### 后端API结构 + +``` +app/ +├── main.py # FastAPI应用入口 +├── models/ +│ ├── __init__.py +│ ├── report.py # 报告数据模型 +│ ├── config.py # 配置数据模型 +│ └── progress.py # 进度追踪模型 +├── schemas/ +│ ├── __init__.py +│ ├── report.py # 报告Pydantic模式 +│ ├── config.py # 配置Pydantic模式 +│ └── progress.py # 进度Pydantic模式 +├── services/ +│ ├── __init__.py +│ ├── data_fetcher.py # 数据获取服务 +│ ├── ai_analyzer.py # AI分析服务 +│ ├── report_generator.py # 报告生成服务 +│ └── config_manager.py # 配置管理服务 +├── routers/ +│ ├── __init__.py +│ ├── reports.py # 报告相关API +│ ├── config.py # 配置相关API +│ └── progress.py # 进度相关API +└── core/ + ├── __init__.py + ├── database.py # 数据库连接 + ├── config.py # 应用配置 + └── dependencies.py # 依赖注入 +``` + +### 核心API接口 + +#### 报告相关API + +```python +# GET /api/reports/{symbol}?market={market} +# 获取或生成股票报告 +class ReportResponse: + symbol: str + market: str + report_id: str + status: str # "existing" | "generating" | "completed" | "failed" + created_at: datetime + updated_at: datetime + modules: List[AnalysisModule] + +# POST /api/reports/{symbol}/regenerate?market={market} +# 重新生成报告 +class RegenerateRequest: + force: bool = False + +# GET /api/reports/{report_id}/progress +# 获取报告生成进度 +class ProgressResponse: + report_id: str + current_step: int + total_steps: int + current_step_name: str + status: str # "running" | "completed" | "failed" + step_timings: List[StepTiming] + estimated_remaining: Optional[int] +``` + +#### 配置相关API + +```python +# GET /api/config +# 获取系统配置 +class ConfigResponse: + database: DatabaseConfig + gemini_api: GeminiConfig + data_sources: Dict[str, DataSourceConfig] + +# PUT /api/config +# 更新系统配置 +class ConfigUpdateRequest: + database: Optional[DatabaseConfig] + gemini_api: Optional[GeminiConfig] + data_sources: Optional[Dict[str, DataSourceConfig]] + +# POST /api/config/test +# 测试配置连接 +class ConfigTestRequest: + config_type: str # "database" | "gemini" | "data_source" + config_data: Dict[str, Any] +``` + +## 数据模型 + +### 数据库表结构 + +```sql +-- 报告表 +CREATE TABLE reports ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + symbol VARCHAR(20) NOT NULL, + market VARCHAR(20) NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'generating', + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + UNIQUE(symbol, market) +); + +-- 分析模块表 +CREATE TABLE analysis_modules ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + report_id UUID REFERENCES reports(id) ON DELETE CASCADE, + module_type VARCHAR(50) NOT NULL, + module_order INTEGER NOT NULL, + title VARCHAR(200) NOT NULL, + content JSONB, + status VARCHAR(20) NOT NULL DEFAULT 'pending', + started_at TIMESTAMP WITH TIME ZONE, + completed_at TIMESTAMP WITH TIME ZONE, + error_message TEXT +); + +-- 进度追踪表 +CREATE TABLE progress_tracking ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + report_id UUID REFERENCES reports(id) ON DELETE CASCADE, + step_name VARCHAR(100) NOT NULL, + step_order INTEGER NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'pending', + started_at TIMESTAMP WITH TIME ZONE, + completed_at TIMESTAMP WITH TIME ZONE, + duration_ms INTEGER, + error_message TEXT +); + +-- 系统配置表 +CREATE TABLE system_config ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + config_key VARCHAR(100) UNIQUE NOT NULL, + config_value JSONB NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); +``` + +### 分析模块类型定义 + +```python +class AnalysisModuleType(Enum): + TRADING_VIEW_CHART = "trading_view_chart" + FINANCIAL_DATA = "financial_data" + BUSINESS_INFO = "business_info" + FUNDAMENTAL_ANALYSIS = "fundamental_analysis" + BULLISH_ANALYSIS = "bullish_analysis" + BEARISH_ANALYSIS = "bearish_analysis" + MARKET_ANALYSIS = "market_analysis" + NEWS_ANALYSIS = "news_analysis" + TRADING_ANALYSIS = "trading_analysis" + INSIDER_ANALYSIS = "insider_analysis" + FINAL_CONCLUSION = "final_conclusion" + +class AnalysisModule(BaseModel): + id: UUID + module_type: AnalysisModuleType + title: str + content: Dict[str, Any] + status: str + duration_ms: Optional[int] + error_message: Optional[str] +``` + +## 错误处理 + +### 错误类型定义 + +```python +class StockAnalysisError(Exception): + """基础异常类""" + pass + +class DataSourceError(StockAnalysisError): + """数据源错误""" + pass + +class AIAnalysisError(StockAnalysisError): + """AI分析错误""" + pass + +class ConfigurationError(StockAnalysisError): + """配置错误""" + pass + +class DatabaseError(StockAnalysisError): + """数据库错误""" + pass +``` + +### 错误处理策略 + +1. **数据获取失败**: 重试机制,最多3次重试,指数退避 +2. **AI分析失败**: 记录错误,继续其他模块,最后汇总失败信息 +3. **数据库连接失败**: 使用连接池,自动重连 +4. **配置错误**: 提供详细错误信息,阻止系统启动 +5. **前端错误**: Toast通知,错误边界组件 + +## 测试策略 + +### 测试层级 + +1. **单元测试** + - 后端服务函数测试 + - 前端组件测试 + - 数据模型验证测试 + +2. **集成测试** + - API端点测试 + - 数据库操作测试 + - 外部服务集成测试 + +3. **端到端测试** + - 完整报告生成流程测试 + - 用户界面交互测试 + +### 测试工具 + +- **后端**: pytest, pytest-asyncio, httpx +- **前端**: Jest, React Testing Library, Playwright +- **数据库**: pytest-postgresql +- **API测试**: FastAPI TestClient + +### 测试数据 + +- 使用测试数据库和模拟数据 +- 外部API使用mock响应 +- 测试用例覆盖各种市场和股票类型 \ No newline at end of file diff --git a/.kiro/specs/fundamental-stock-analysis/requirements.md b/.kiro/specs/fundamental-stock-analysis/requirements.md new file mode 100644 index 0000000..69bfc1f --- /dev/null +++ b/.kiro/specs/fundamental-stock-analysis/requirements.md @@ -0,0 +1,112 @@ +# 需求文档 - MVP版本 + +## 介绍 + +基本面选股系统MVP是一个综合的中文网站,允许用户输入证券代码和交易市场,生成包含多维度分析的详细股票基本面报告。系统通过多个专业分析模块,结合财务数据、AI分析和市场信息,为用户提供全面的投资决策支持。 + +## 术语表 + +- **选股系统 (Stock_Selection_System)**: 提供基本面分析和报告生成的主要系统 +- **用户 (User)**: 使用系统进行股票分析的终端用户 +- **证券代码 (Security_Code)**: 股票在特定交易市场的唯一标识符 +- **交易市场 (Trading_Market)**: 股票交易的地理区域,包括中国、香港、美国、日本 +- **基本面报告 (Fundamental_Report)**: 包含九个分析模块的综合股票分析报告 +- **TradingView图表 (TradingView_Chart)**: 使用TradingView高级图表组件显示的股价图表 +- **Tushare_API**: 用于获取中国股票财务数据的数据源接口 +- **Gemini_Model**: Google的大语言模型,用于生成业务分析内容 +- **景林模型 (Jinglin_Model)**: 基本面分析师使用的问题集分析框架 +- **PostgreSQL数据库 (PostgreSQL_Database)**: 用于存储报告数据的关系型数据库 +- **分析模块 (Analysis_Module)**: 报告中的独立分析部分,每个模块对应一个显示页面 + +## 需求 + +### 需求 1 + +**用户故事:** 作为投资者,我希望能够输入股票代码和选择交易市场,以便获取该股票的综合基本面分析报告 + +#### 验收标准 + +1. 当用户访问首页时,选股系统应当显示证券代码输入框和交易市场选择器 +2. 当用户选择交易市场时,选股系统应当提供中国、香港、美国、日本四个选项 +3. 当用户提交证券代码和交易市场时,选股系统应当处理用户请求并跳转到报告页面 + +### 需求 2 + +**用户故事:** 作为投资者,我希望系统能够检查历史报告,以便决定是查看现有报告还是生成新报告 + +#### 验收标准 + +1. 当用户提交证券代码和交易市场后,选股系统应当在PostgreSQL数据库中查询对应的历史报告 +2. 如果存在历史报告,选股系统应当显示历史报告内容和"生成最新报告"按钮 +3. 如果不存在历史报告,选股系统应当自动启动九步报告生成流程 + +### 需求 3 + +**用户故事:** 作为投资者,我希望系统能够获取准确的财务数据,以便进行可靠的基本面分析 + +#### 验收标准 + +1. 当生成中国股票报告时,选股系统应当使用Tushare_API获取财务信息 +2. 当处理其他市场股票时,选股系统应当根据交易市场选择相应的数据源 +3. 当财务数据获取完成时,选股系统应当将数据作为后续分析的基础 + +### 需求 4 + +**用户故事:** 作为投资者,我希望系统能够通过AI分析获取公司业务信息,以便了解公司的全面情况 + +#### 验收标准 + +1. 当需要业务信息时,选股系统应当使用Gemini生成公司概览、主营业务、发展历程、核心团队、供应链、主要客户及销售模式、未来展望 +2. 当调用Gemini_Model时,选股系统应当使用配置的API密钥进行身份验证 +3. 当业务信息生成完成时,选股系统应当将内容整合到报告的第二部分 + +### 需求 5 + +**用户故事:** 作为投资者,我希望系统能够提供多维度的专业分析,以便获得全面的投资决策支持 + +#### 验收标准 + +1. 当生成报告时,选股系统应当按顺序执行10个分析模块:财务信息、业务信息、基本面分析、看涨分析、看跌分析、市场分析、新闻分析、交易分析、内部人与机构动向分析、最终结论 +2. 当执行基本面分析时,选股系统应当使用问题集进行分析 +3. 当执行看涨分析时,选股系统应当研究潜在隐藏资产和护城河竞争优势 +4. 当执行看跌分析时,选股系统应当分析公司价值底线和最坏情况 +5. 当执行市场分析时,选股系统应当研究市场情绪分歧点与变化驱动 +6. 当执行新闻分析时,选股系统应当研究股价催化剂与拐点预判 +7. 当执行交易分析时,选股系统应当研究市场体量与增长路径 +8. 当执行内部人分析时,选股系统应当研究内部人与机构动向 +9. 当生成最终结论时,选股系统应当指出关键矛盾与预期差以及拐点的临近 + +### 需求 6 + +**用户故事:** 作为投资者,我希望每个分析模块都能独立查看,以便专注于特定的分析维度 + +#### 验收标准 + +1. 当显示报告时,选股系统应当为每个分析模块提供独立的显示页面 +2. 当用户在模块间切换时,选股系统应当保持导航的流畅性 +3. 当所有模块完成时,选股系统应当将完整报告保存到PostgreSQL数据库 + +### 需求 7 + +**用户故事:** 作为投资者,我希望在报告生成过程中能够看到实时进度,以便了解当前状态和预估完成时间 + +#### 验收标准 + +1. 当开始生成报告时,选股系统应当显示进度指示器展示所有分析步骤 +2. 当执行每个分析步骤时,选股系统应当高亮显示当前正在进行的步骤 +3. 当每个步骤完成时,选股系统应当更新步骤状态为已完成 +4. 当执行分析步骤时,选股系统应当记录每个步骤的开始时间和完成时间 +5. 当显示进度时,选股系统应当展示每个步骤的耗时统计 +6. 当步骤执行失败时,选股系统应当显示错误状态和错误信息 + +### 需求 8 + +**用户故事:** 作为系统管理员,我希望能够配置系统参数,以便系统能够正常连接外部服务 + +#### 验收标准 + +1. 选股系统应当提供配置页面用于设置数据库连接参数 +2. 选股系统应当提供配置页面用于设置Gemini_API密钥 +3. 选股系统应当提供配置页面用于设置各市场的数据源配置 +4. 当配置更新时,选股系统应当验证配置的有效性 +5. 当配置保存时,选股系统应当将配置持久化存储 \ No newline at end of file diff --git a/.kiro/specs/fundamental-stock-analysis/tasks.md b/.kiro/specs/fundamental-stock-analysis/tasks.md new file mode 100644 index 0000000..e02fe33 --- /dev/null +++ b/.kiro/specs/fundamental-stock-analysis/tasks.md @@ -0,0 +1,167 @@ +# 实施计划 + +- [x] 1. 后端项目初始化和基础架构 + - 创建Python FastAPI项目结构 + - 设置虚拟环境和依赖管理(requirements.txt或pyproject.toml) + - 配置FastAPI应用入口(main.py) + - 创建核心目录结构(models, schemas, services, routers, core) + - 设置基础配置管理(core/config.py) + - _需求: 8.1, 8.2_ + +- [x] 2. 数据库设置和模型定义 + - 配置PostgreSQL数据库连接(core/database.py) + - 创建SQLAlchemy数据模型(reports, analysis_modules, progress_tracking, system_config) + - 设置Alembic数据库迁移工具 + - 创建初始数据库迁移脚本 + - 实现数据库会话管理和依赖注入 + - _需求: 6.3, 8.1_ + +- [x] 3. Pydantic模式和基础服务 + - 创建Pydantic数据验证模式(schemas/) + - 实现配置管理服务(services/config_manager.py) + - 创建数据获取服务基础架构(services/data_fetcher.py) + - 实现基础错误处理和异常类 + - _需求: 8.2, 8.3, 8.4, 8.5_ + +- [x] 4. 外部API集成服务 + - 实现Tushare API集成(中国股票数据获取) + - 实现Gemini API集成(AI分析服务) + - 创建数据源配置和切换逻辑 + - 添加API调用错误处理和重试机制 + - _需求: 3.1, 3.2, 4.1, 4.2_ + +- [x] 5. 报告生成引擎核心 + - 创建报告生成服务(services/report_generator.py) + - 实现分析模块执行框架 + - 创建进度追踪服务(services/progress_tracker.py) + - 实现步骤计时和状态管理 + - _需求: 5.1, 7.1, 7.2, 7.3, 7.4, 7.5_ + +- [x] 6. 后端API路由实现 + - 实现报告相关API端点(routers/reports.py) + - 创建配置管理API端点(routers/config.py) + - 实现进度追踪API端点(routers/progress.py) + - 添加API文档和验证 + - _需求: 2.1, 2.2, 2.3, 8.1, 8.2, 8.3_ + +- [x] 7. 前端项目初始化 + - 创建Next.js项目并配置TypeScript + - 安装和配置shadcn/ui组件库 + - 设置Tailwind CSS和基础样式 + - 配置项目文件夹结构(components, lib, hooks, app) + - 创建基础布局和主题配置 + - _需求: 1.1_ + +- [ ] 8. 前端核心组件开发 + - 安装和配置shadcn/ui基础组件 + - 实现StockSearchForm组件(使用Form, Input, Select, Button) + - 创建ReportProgress组件(使用Progress, Badge, Card) + - 实现AnalysisModule组件(使用Card, Tabs, Separator) + - 创建FinancialDataTable组件(使用Table组件系列) + - _需求: 1.1, 1.2, 7.1, 7.2_ + +- [ ] 9. 首页和股票搜索功能 + - 实现首页布局和设计(app/page.tsx) + - 创建股票代码输入和市场选择功能 + - 实现表单验证和提交逻辑 + - 添加中文界面文本和错误提示 + - 连接前端表单到后端API + - _需求: 1.1, 1.2, 1.3_ + +- [ ] 10. 报告页面和历史报告功能 + - 实现报告页面路由(app/report/[symbol]/page.tsx) + - 创建历史报告检查和显示逻辑 + - 实现"生成最新报告"按钮功能 + - 添加报告加载状态和错误处理 + - _需求: 2.1, 2.2, 2.3_ + +- [ ] 11. TradingView图表集成 + - 集成TradingView高级图表组件 + - 实现图表配置和参数设置 + - 根据证券代码和市场配置图表 + - 处理图表加载错误和异常情况 + - _需求: 5.1, 5.2, 5.3, 5.4_ + +- [ ] 12. 财务数据分析模块 + - 实现财务数据获取和处理逻辑 + - 创建财务数据格式化和展示 + - 实现FinancialDataTable的数据绑定 + - 添加财务数据的错误处理和重试 + - _需求: 3.1, 3.2, 3.3_ + +- [ ] 13. AI业务信息分析模块 + - 实现Gemini API调用逻辑和提示词模板 + - 创建业务信息分析内容生成 + - 实现公司概览、主营业务、发展历程等内容 + - 添加AI分析结果的格式化和展示 + - _需求: 4.1, 4.2, 4.3_ + +- [ ] 14. 专业分析模块实现 + - 实现景林模型基本面分析模块 + - 创建看涨分析师模块(隐藏资产、护城河分析) + - 实现看跌分析师模块(价值底线、最坏情况分析) + - 创建市场分析师模块(市场情绪分歧点分析) + - 实现新闻分析师模块(股价催化剂分析) + - 创建交易分析模块(市场体量与增长路径) + - 实现内部人与机构动向分析模块 + - 创建最终结论模块(关键矛盾与拐点分析) + - _需求: 5.1, 5.2, 5.3, 5.4, 5.5, 5.6, 5.7, 5.8, 5.9_ + +- [ ] 15. 报告生成流程整合 + - 整合所有分析模块到报告生成引擎 + - 实现模块间的数据传递和依赖关系 + - 创建报告生成的错误处理和重试机制 + - 实现报告完成后的数据库保存 + - _需求: 5.1, 6.3_ + +- [ ] 16. 实时进度显示功能 + - 实现前端进度追踪钩子(useProgress) + - 连接WebSocket或Server-Sent Events到进度显示 + - 添加步骤高亮和状态更新 + - 实现计时显示和预估完成时间 + - 添加错误状态显示 + - _需求: 7.1, 7.2, 7.3, 7.4, 7.5, 7.6_ + +- [ ] 17. 配置管理页面 + - 创建配置页面布局和表单(app/config/page.tsx) + - 实现数据库配置界面 + - 添加Gemini API配置功能 + - 创建数据源配置管理 + - 实现配置验证和测试功能 + - _需求: 8.1, 8.2, 8.3, 8.4, 8.5_ + +- [ ] 18. 报告展示和导航优化 + - 实现分析模块的独立页面展示 + - 创建模块间的流畅导航 + - 添加报告概览和目录功能 + - 优化移动端响应式显示 + - _需求: 6.1, 6.2_ + +- [ ] 19. 错误处理和用户体验优化 + - 实现全局错误处理和错误边界 + - 添加Toast通知系统 + - 创建加载状态和骨架屏 + - 优化中文界面和用户反馈 + - 添加操作确认和提示 + - _需求: 7.6, 1.1_ + +- [ ]* 20. 测试实现 +- [ ]* 20.1 后端单元测试 + - 为数据获取服务编写单元测试 + - 为AI分析服务编写单元测试 + - 为报告生成引擎编写单元测试 + - 为配置管理服务编写单元测试 + +- [ ]* 20.2 前端组件测试 + - 为核心组件编写React Testing Library测试 + - 为表单组件编写交互测试 + - 为进度组件编写状态测试 + +- [ ]* 20.3 API集成测试 + - 为报告生成API编写集成测试 + - 为配置管理API编写集成测试 + - 为进度追踪API编写集成测试 + +- [ ]* 20.4 端到端测试 + - 编写完整报告生成流程的E2E测试 + - 编写配置管理流程的E2E测试 \ No newline at end of file diff --git a/backend/DATABASE_SETUP.md b/backend/DATABASE_SETUP.md new file mode 100644 index 0000000..9d1a59e --- /dev/null +++ b/backend/DATABASE_SETUP.md @@ -0,0 +1,228 @@ +# 数据库设置指南 + +## 概述 + +本项目使用PostgreSQL作为主数据库,SQLAlchemy作为ORM,Alembic作为数据库迁移工具。 + +## 数据库架构 + +### 表结构 + +1. **reports** - 报告主表 + - 存储股票分析报告的基本信息 + - 包含证券代码、市场、状态等字段 + +2. **analysis_modules** - 分析模块表 + - 存储报告中各个分析模块的内容 + - 与reports表一对多关系 + +3. **progress_tracking** - 进度追踪表 + - 记录报告生成过程中各步骤的执行状态 + - 与reports表一对多关系 + +4. **system_config** - 系统配置表 + - 存储系统配置信息 + - 使用JSONB格式存储配置值 + +## 环境配置 + +### 1. 安装PostgreSQL + +```bash +# macOS (使用Homebrew) +brew install postgresql +brew services start postgresql + +# Ubuntu/Debian +sudo apt-get install postgresql postgresql-contrib + +# CentOS/RHEL +sudo yum install postgresql-server postgresql-contrib +``` + +### 2. 创建数据库 + +```sql +-- 连接到PostgreSQL +psql -U postgres + +-- 创建数据库 +CREATE DATABASE stock_analysis; + +-- 创建用户(可选) +CREATE USER stock_user WITH PASSWORD 'your_password'; +GRANT ALL PRIVILEGES ON DATABASE stock_analysis TO stock_user; +``` + +### 3. 配置环境变量 + +创建 `.env` 文件: + +```bash +# 数据库配置 +DATABASE_URL=postgresql+asyncpg://username:password@localhost:5432/stock_analysis +DATABASE_ECHO=false + +# API配置 +GEMINI_API_KEY=your_gemini_api_key +TUSHARE_TOKEN=your_tushare_token +``` + +## 数据库管理 + +### 使用管理脚本 + +```bash +# 检查数据库连接 +python manage_db.py check + +# 初始化数据库表 +python manage_db.py init + +# 查看数据库状态 +python manage_db.py status +``` + +### 使用Alembic迁移 + +```bash +# 初始化Alembic(已完成) +alembic init alembic + +# 创建迁移文件 +alembic revision --autogenerate -m "描述信息" + +# 应用迁移 +alembic upgrade head + +# 查看迁移历史 +alembic history + +# 回滚迁移 +alembic downgrade -1 +``` + +## 开发工具 + +### 1. 数据库连接检查 + +```bash +python check_db.py +``` + +### 2. 数据库初始化 + +```bash +python init_db.py +``` + +### 3. 综合管理工具 + +```bash +python manage_db.py [check|init|status] +``` + +## 模型使用示例 + +### 创建报告 + +```python +from app.models import Report, AnalysisModule +from app.core.database import get_db + +async def create_report(): + async for db in get_db(): + # 创建报告 + report = Report( + symbol="000001", + market="中国", + status="generating" + ) + db.add(report) + await db.commit() + await db.refresh(report) + + # 创建分析模块 + module = AnalysisModule( + report_id=report.id, + module_type="financial_data", + module_order=1, + title="财务数据分析", + status="pending" + ) + db.add(module) + await db.commit() +``` + +### 查询报告 + +```python +from sqlalchemy import select +from app.models import Report + +async def get_report(symbol: str, market: str): + async for db in get_db(): + stmt = select(Report).where( + Report.symbol == symbol, + Report.market == market + ) + result = await db.execute(stmt) + return result.scalar_one_or_none() +``` + +## 性能优化 + +### 索引 + +所有表都已配置适当的索引: + +- reports: symbol+market, status, created_at +- analysis_modules: report_id, module_type, status, module_order +- progress_tracking: report_id, status, step_order +- system_config: config_key, updated_at + +### 连接池 + +数据库连接使用异步连接池,配置参数: + +- pool_size: 10 +- max_overflow: 20 +- pool_timeout: 30秒 +- pool_recycle: 1小时 + +## 故障排除 + +### 常见问题 + +1. **连接失败** + - 检查PostgreSQL服务是否运行 + - 验证数据库URL配置 + - 确认防火墙设置 + +2. **迁移失败** + - 检查数据库权限 + - 验证表结构冲突 + - 查看Alembic日志 + +3. **性能问题** + - 检查索引使用情况 + - 分析慢查询日志 + - 优化查询语句 + +### 日志配置 + +在 `config.py` 中设置 `DATABASE_ECHO=True` 可以查看SQL执行日志。 + +## 备份与恢复 + +### 备份 + +```bash +pg_dump -U username -h localhost stock_analysis > backup.sql +``` + +### 恢复 + +```bash +psql -U username -h localhost stock_analysis < backup.sql +``` \ No newline at end of file diff --git a/backend/EXTERNAL_API_INTEGRATION.md b/backend/EXTERNAL_API_INTEGRATION.md new file mode 100644 index 0000000..e1efb82 --- /dev/null +++ b/backend/EXTERNAL_API_INTEGRATION.md @@ -0,0 +1,287 @@ +# 外部API集成文档 + +## 概述 + +本系统集成了多个外部API服务,用于获取股票数据和进行AI分析: + +- **Tushare API**: 中国股票财务数据和市场数据 +- **Yahoo Finance API**: 全球股票数据(美国、香港、日本等) +- **Google Gemini API**: AI分析和内容生成 + +## 架构设计 + +### 数据源管理器 (DataSourceManager) +- 统一管理所有数据源 +- 支持数据源切换和故障转移 +- 提供健康检查和状态监控 + +### 外部API服务 (ExternalAPIService) +- 提供统一的API接口 +- 处理错误和重试机制 +- 支持配置动态更新 + +## 支持的数据源 + +### 1. Tushare API +- **用途**: 中国股票数据(A股、港股通等) +- **数据类型**: 财务数据、市场数据、基本信息 +- **配置要求**: 需要Tushare API Token +- **限制**: 有调用频率限制,需要付费账户获取完整数据 + +#### 配置示例 +```python +{ + "tushare": { + "enabled": True, + "api_key": "your_tushare_token", + "base_url": "http://api.tushare.pro", + "timeout": 30, + "max_retries": 3, + "retry_delay": 1 + } +} +``` + +### 2. Yahoo Finance API +- **用途**: 全球股票数据 +- **数据类型**: 财务数据、市场数据、基本信息 +- **配置要求**: 无需API密钥 +- **限制**: 有调用频率限制,可能被反爬虫机制阻止 + +#### 配置示例 +```python +{ + "yahoo": { + "enabled": True, + "base_url": "https://query1.finance.yahoo.com", + "timeout": 30, + "max_retries": 3, + "retry_delay": 1 + } +} +``` + +### 3. Google Gemini API +- **用途**: AI分析和内容生成 +- **功能**: 业务分析、基本面分析、投资建议等 +- **配置要求**: 需要Google Cloud API密钥 +- **限制**: 有调用频率和配额限制 + +#### 配置示例 +```python +{ + "gemini": { + "enabled": True, + "api_key": "your_gemini_api_key", + "model": "gemini-pro", + "timeout": 60, + "max_retries": 3, + "retry_delay": 2, + "temperature": 0.7, + "max_output_tokens": 8192 + } +} +``` + +## 数据源切换逻辑 + +### 市场映射 +系统根据股票市场自动选择合适的数据源: + +```python +market_mapping = { + "china": "tushare", # 中国A股使用Tushare + "中国": "tushare", + "hongkong": "yahoo", # 香港股票使用Yahoo Finance + "香港": "yahoo", + "usa": "yahoo", # 美国股票使用Yahoo Finance + "美国": "yahoo", + "japan": "yahoo", # 日本股票使用Yahoo Finance + "日本": "yahoo" +} +``` + +### 故障转移 +当主要数据源不可用时,系统会自动切换到备用数据源: + +```python +fallback_sources = { + "tushare": ["yahoo"], # Tushare失败时使用Yahoo + "yahoo": ["tushare"] # Yahoo失败时使用Tushare +} +``` + +## 错误处理和重试机制 + +### 错误类型 +- `DataSourceError`: 数据源相关错误 +- `AIAnalysisError`: AI分析相关错误 +- `AuthenticationError`: 认证失败 +- `RateLimitError`: 调用频率超限 +- `APIError`: 通用API错误 + +### 重试策略 +- **指数退避**: 重试间隔逐渐增加 +- **最大重试次数**: 默认3次 +- **超时处理**: 每个请求都有超时限制 +- **错误分类**: 不同错误类型采用不同的重试策略 + +## API使用示例 + +### 1. 获取财务数据 +```python +from app.services.external_api_service import get_external_api_service + +service = get_external_api_service() + +# 获取平安银行财务数据 +financial_data = await service.get_financial_data("000001", "中国") +print(f"数据源: {financial_data.data_source}") +print(f"总资产: {financial_data.balance_sheet.get('total_assets')}") +``` + +### 2. 验证股票代码 +```python +# 验证股票代码是否有效 +validation = await service.validate_stock_symbol("AAPL", "美国") +if validation.is_valid: + print(f"公司名称: {validation.company_name}") +``` + +### 3. AI分析 +```python +# 进行业务信息分析 +analysis = await service.analyze_business_info( + "000001", "中国", financial_data.dict() +) +print(f"分析内容: {analysis.content['company_overview']}") +``` + +### 4. 检查服务状态 +```python +# 检查所有外部服务状态 +status = await service.check_all_services_status() +print(f"整体状态: {status.overall_status}") + +for source in status.sources: + print(f"{source.name}: {'可用' if source.is_available else '不可用'}") +``` + +## 配置管理 + +### 环境变量 +在`.env`文件中配置API密钥: + +```bash +# Tushare API Token +TUSHARE_TOKEN=your_tushare_token_here + +# Gemini API Key +GEMINI_API_KEY=your_gemini_api_key_here +``` + +### 动态配置更新 +```python +# 更新配置 +new_config = { + "data_sources": { + "tushare": { + "enabled": True, + "api_key": "new_token" + } + } +} + +service.update_configuration(new_config) +``` + +## 测试和调试 + +### 运行测试脚本 +```bash +cd backend +python test_external_apis.py +``` + +### 测试单个数据源 +```python +# 测试Tushare连接 +result = await service.test_data_source_connection("tushare", { + "api_key": "your_token", + "base_url": "http://api.tushare.pro" +}) +print(f"连接成功: {result['success']}") +``` + +### 测试AI服务 +```python +# 测试Gemini连接 +result = await service.test_ai_service_connection("gemini", { + "api_key": "your_api_key" +}) +print(f"连接成功: {result['success']}") +``` + +## 性能优化 + +### 缓存策略 +- 财务数据缓存1小时 +- 市场数据缓存5分钟 +- AI分析结果缓存24小时 + +### 并发控制 +- 限制同时进行的API调用数量 +- 使用连接池管理HTTP连接 +- 实现请求队列避免频率限制 + +### 监控和日志 +- 记录所有API调用和响应时间 +- 监控错误率和成功率 +- 设置告警机制 + +## 故障排除 + +### 常见问题 + +1. **Tushare API调用失败** + - 检查API Token是否正确 + - 确认账户是否有足够的积分 + - 检查网络连接 + +2. **Gemini API调用失败** + - 检查API Key是否有效 + - 确认配额是否充足 + - 检查请求格式是否正确 + +3. **Yahoo Finance被限制** + - 降低请求频率 + - 使用代理服务器 + - 切换到其他数据源 + +### 调试技巧 +- 启用详细日志记录 +- 使用测试脚本验证配置 +- 检查网络连接和防火墙设置 +- 监控API调用的响应时间和错误率 + +## 扩展性 + +### 添加新的数据源 +1. 继承`DataFetcher`基类 +2. 实现必要的方法 +3. 在`DataFetcherFactory`中注册 +4. 更新配置文件 + +### 添加新的AI服务 +1. 创建新的分析器类 +2. 实现统一的接口 +3. 在`AIAnalyzerFactory`中注册 +4. 更新配置管理 + +## 安全考虑 + +- API密钥加密存储 +- 使用HTTPS进行所有外部调用 +- 实现访问控制和权限管理 +- 定期轮换API密钥 +- 监控异常访问模式 \ No newline at end of file diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..4cbe1e9 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,100 @@ +# 基本面选股系统 - 后端服务 + +基于FastAPI的股票基本面分析后端服务,提供报告生成、配置管理和进度追踪功能。 + +## 项目结构 + +``` +backend/ +├── main.py # FastAPI应用入口 +├── requirements.txt # Python依赖 +├── .env.example # 环境变量示例 +├── README.md # 项目说明 +└── app/ + ├── __init__.py + ├── core/ # 核心模块 + │ ├── config.py # 配置管理 + │ ├── database.py # 数据库连接 + │ └── dependencies.py # 依赖注入 + ├── models/ # 数据模型 + │ ├── report.py + │ ├── analysis_module.py + │ ├── progress_tracking.py + │ └── system_config.py + ├── schemas/ # Pydantic模式 + │ ├── report.py + │ ├── config.py + │ └── progress.py + ├── services/ # 业务服务 + │ ├── config_manager.py + │ ├── data_fetcher.py + │ ├── report_generator.py + │ └── progress_tracker.py + └── routers/ # API路由 + ├── reports.py + ├── config.py + └── progress.py +``` + +## 快速开始 + +1. 安装依赖: +```bash +pip install -r requirements.txt +``` + +2. 配置环境变量: +```bash +cp .env.example .env +# 编辑 .env 文件,设置数据库连接和API密钥 +``` + +3. 启动服务: +```bash +uvicorn main:app --reload --host 0.0.0.0 --port 8000 +``` + +4. 访问API文档: +- Swagger UI: http://localhost:8000/docs +- ReDoc: http://localhost:8000/redoc + +## API端点 + +### 报告相关 +- `GET /api/reports/{symbol}?market={market}` - 获取或创建报告 +- `POST /api/reports/{symbol}/regenerate?market={market}` - 重新生成报告 +- `GET /api/reports/{report_id}/details` - 获取报告详情 + +### 配置管理 +- `GET /api/config` - 获取系统配置 +- `PUT /api/config` - 更新系统配置 +- `POST /api/config/test` - 测试配置连接 + +### 进度追踪 +- `GET /api/progress/{report_id}` - 获取报告生成进度 + +## 数据库 + +系统使用PostgreSQL数据库,包含以下主要表: +- `reports` - 报告基本信息 +- `analysis_modules` - 分析模块内容 +- `progress_tracking` - 进度追踪记录 +- `system_config` - 系统配置 + +## 开发 + +### 代码格式化 +```bash +black app/ +isort app/ +``` + +### 类型检查 +```bash +mypy app/ +``` + +### 运行测试 +```bash +pytest +``` \ No newline at end of file diff --git a/backend/alembic.ini b/backend/alembic.ini new file mode 100644 index 0000000..2671141 --- /dev/null +++ b/backend/alembic.ini @@ -0,0 +1,118 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +# Use forward slashes (/) also on windows to provide an os agnostic path +script_location = alembic + +# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s +# Uncomment the line below if you want the files to be prepended with date and time +# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file +# for all available tokens +# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. +prepend_sys_path = . + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the python>=3.9 or backports.zoneinfo library. +# Any required deps can installed by adding `alembic[tz]` to the pip requirements +# string value is passed to ZoneInfo() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to alembic/versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# The path separator used here should be the separator specified by "version_path_separator" below. +# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions + +# version path separator; As mentioned above, this is the character used to split +# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. +# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. +# Valid values for version_path_separator are: +# +# version_path_separator = : +# version_path_separator = ; +# version_path_separator = space +# version_path_separator = newline +version_path_separator = os # Use os.pathsep. Default configuration used for new projects. + +# set to 'true' to search source files recursively +# in each "version_locations" directory +# new in Alembic version 1.10 +# recursive_version_locations = false + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +# sqlalchemy.url = driver://user:pass@localhost/dbname +# Database URL will be set programmatically in env.py + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME + +# lint with attempts to fix using "ruff" - use the exec runner, execute a binary +# hooks = ruff +# ruff.type = exec +# ruff.executable = %(here)s/.venv/bin/ruff +# ruff.options = --fix REVISION_SCRIPT_FILENAME + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[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/README b/backend/alembic/README new file mode 100644 index 0000000..98e4f9c --- /dev/null +++ b/backend/alembic/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/backend/alembic/env.py b/backend/alembic/env.py new file mode 100644 index 0000000..05b69b9 --- /dev/null +++ b/backend/alembic/env.py @@ -0,0 +1,102 @@ +import asyncio +import os +import sys +from logging.config import fileConfig + +from sqlalchemy import pool +from sqlalchemy.engine import Connection +from sqlalchemy.ext.asyncio import create_async_engine + +from alembic import context + +# 添加项目根目录到Python路径 +sys.path.append(os.path.dirname(os.path.dirname(__file__))) + +# 导入我们的模型和配置 +from app.core.database import Base +from app.core.config import settings +from app.models import * # 导入所有模型以确保它们被注册 + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +target_metadata = Base.metadata + +# 设置数据库URL +config.set_main_option("sqlalchemy.url", settings.DATABASE_URL.replace("+asyncpg", "")) + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + compare_type=True, + compare_server_default=True, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def do_run_migrations(connection: Connection) -> None: + """运行迁移的核心逻辑""" + context.configure( + connection=connection, + target_metadata=target_metadata, + compare_type=True, + compare_server_default=True, + ) + + with context.begin_transaction(): + context.run_migrations() + + +async def run_async_migrations() -> None: + """异步运行迁移""" + connectable = create_async_engine( + settings.DATABASE_URL, + poolclass=pool.NullPool, + ) + + async with connectable.connect() as connection: + await connection.run_sync(do_run_migrations) + + await connectable.dispose() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode.""" + asyncio.run(run_async_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..fbc4b07 --- /dev/null +++ b/backend/alembic/script.py.mako @@ -0,0 +1,26 @@ +"""${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 identifiers, used by Alembic. +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/alembic/versions/001_initial_migration.py b/backend/alembic/versions/001_initial_migration.py new file mode 100644 index 0000000..6dde7f9 --- /dev/null +++ b/backend/alembic/versions/001_initial_migration.py @@ -0,0 +1,90 @@ +"""Initial migration: create all tables + +Revision ID: 001 +Revises: +Create Date: 2024-01-01 00:00:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = '001' +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + + # Create reports table + op.create_table('reports', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('symbol', sa.String(length=20), nullable=False, comment='证券代码'), + sa.Column('market', sa.String(length=20), nullable=False, comment='交易市场'), + sa.Column('status', sa.String(length=20), nullable=False, comment='报告状态'), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True, comment='创建时间'), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True, comment='更新时间'), + sa.PrimaryKeyConstraint('id') + ) + + # Create analysis_modules table + op.create_table('analysis_modules', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('report_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('module_type', sa.String(length=50), nullable=False, comment='模块类型'), + sa.Column('module_order', sa.Integer(), nullable=False, comment='模块顺序'), + sa.Column('title', sa.String(length=200), nullable=False, comment='模块标题'), + sa.Column('content', postgresql.JSONB(astext_type=sa.Text()), nullable=True, comment='模块内容'), + sa.Column('status', sa.String(length=20), nullable=False, comment='模块状态'), + sa.Column('started_at', sa.DateTime(timezone=True), nullable=True, comment='开始时间'), + sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True, comment='完成时间'), + sa.Column('error_message', sa.Text(), nullable=True, comment='错误信息'), + sa.ForeignKeyConstraint(['report_id'], ['reports.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + + # Create progress_tracking table + op.create_table('progress_tracking', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('report_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('step_name', sa.String(length=100), nullable=False, comment='步骤名称'), + sa.Column('step_order', sa.Integer(), nullable=False, comment='步骤顺序'), + sa.Column('status', sa.String(length=20), nullable=False, comment='步骤状态'), + sa.Column('started_at', sa.DateTime(timezone=True), nullable=True, comment='开始时间'), + sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True, comment='完成时间'), + sa.Column('duration_ms', sa.Integer(), nullable=True, comment='耗时(毫秒)'), + sa.Column('error_message', sa.Text(), nullable=True, comment='错误信息'), + sa.ForeignKeyConstraint(['report_id'], ['reports.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + + # Create system_config table + op.create_table('system_config', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('config_key', sa.String(length=100), nullable=False, comment='配置键'), + sa.Column('config_value', postgresql.JSONB(astext_type=sa.Text()), nullable=False, comment='配置值'), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True, comment='更新时间'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('config_key') + ) + + # Set default values for status columns + op.execute("ALTER TABLE reports ALTER COLUMN status SET DEFAULT 'generating'") + op.execute("ALTER TABLE analysis_modules ALTER COLUMN status SET DEFAULT 'pending'") + op.execute("ALTER TABLE progress_tracking ALTER COLUMN status SET DEFAULT 'pending'") + + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('system_config') + op.drop_table('progress_tracking') + op.drop_table('analysis_modules') + op.drop_table('reports') + # ### end Alembic commands ### \ No newline at end of file diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..a46dd5c --- /dev/null +++ b/backend/app/__init__.py @@ -0,0 +1,7 @@ +""" +基本面选股系统后端应用 +""" + +__version__ = "1.0.0" +__author__ = "基本面选股系统开发团队" +__description__ = "提供股票基本面分析和报告生成的后端服务" \ No newline at end of file diff --git a/backend/app/core/__init__.py b/backend/app/core/__init__.py new file mode 100644 index 0000000..9435e74 --- /dev/null +++ b/backend/app/core/__init__.py @@ -0,0 +1,48 @@ +""" +核心模块 +包含配置、数据库连接、依赖注入和异常处理 +""" + +from .config import settings, db_config, api_config, data_source_config +from .database import engine, AsyncSessionLocal, Base, get_db, init_db, close_db +from .dependencies import get_database_session, verify_gemini_api_key, verify_tushare_token +from .exceptions import ( + StockAnalysisError, + DataSourceError, + AIAnalysisError, + ConfigurationError, + DatabaseError, + ValidationError, + APIError, + ReportGenerationError, + SymbolNotFoundError, + RateLimitError, + AuthenticationError +) + +__all__ = [ + "settings", + "db_config", + "api_config", + "data_source_config", + "engine", + "AsyncSessionLocal", + "Base", + "get_db", + "init_db", + "close_db", + "get_database_session", + "verify_gemini_api_key", + "verify_tushare_token", + "StockAnalysisError", + "DataSourceError", + "AIAnalysisError", + "ConfigurationError", + "DatabaseError", + "ValidationError", + "APIError", + "ReportGenerationError", + "SymbolNotFoundError", + "RateLimitError", + "AuthenticationError" +] \ No newline at end of file diff --git a/backend/app/core/config.py b/backend/app/core/config.py new file mode 100644 index 0000000..0b4ab43 --- /dev/null +++ b/backend/app/core/config.py @@ -0,0 +1,183 @@ +""" +应用配置管理 +处理环境变量和系统配置 +""" + +from typing import List, Optional +from pydantic import validator +from pydantic_settings import BaseSettings +import os + + +class Settings(BaseSettings): + """应用设置类""" + + # 应用基础配置 + APP_NAME: str = "基本面选股系统" + APP_VERSION: str = "1.0.0" + DEBUG: bool = False + + # 数据库配置 + DATABASE_URL: str = "postgresql+asyncpg://user:password@localhost:5432/stock_analysis" + DATABASE_ECHO: bool = False + + # API配置 + API_V1_STR: str = "/api" + ALLOWED_ORIGINS: List[str] = ["http://localhost:3000", "http://127.0.0.1:3000"] + + # 外部服务配置 + GEMINI_API_KEY: Optional[str] = None + TUSHARE_TOKEN: Optional[str] = None + + # 数据源配置 + CHINA_DATA_SOURCE: str = "tushare" + HK_DATA_SOURCE: str = "yahoo" + US_DATA_SOURCE: str = "yahoo" + JP_DATA_SOURCE: str = "yahoo" + + # 报告生成配置 + MAX_CONCURRENT_REPORTS: int = 5 + REPORT_TIMEOUT_MINUTES: int = 30 + + # 缓存配置 + CACHE_TTL_SECONDS: int = 3600 # 1小时 + + @validator("ALLOWED_ORIGINS", pre=True) + def assemble_cors_origins(cls, v): + """处理CORS origins配置""" + if isinstance(v, str): + return [i.strip() for i in v.split(",")] + return v + + @validator("DATABASE_URL", pre=True) + def assemble_db_connection(cls, v): + """处理数据库连接字符串""" + if v and not v.startswith("postgresql"): + raise ValueError("数据库URL必须使用PostgreSQL") + return v + + class Config: + env_file = ".env" + case_sensitive = True + + +# 创建全局设置实例 +settings = Settings() + + +class DatabaseConfig: + """数据库配置类""" + + def __init__(self): + self.url = settings.DATABASE_URL + self.echo = settings.DATABASE_ECHO + self.pool_size = 10 + self.max_overflow = 20 + self.pool_timeout = 30 + self.pool_recycle = 3600 + + +class ExternalAPIConfig: + """外部API配置类""" + + def __init__(self): + self.gemini_api_key = settings.GEMINI_API_KEY + self.tushare_token = settings.TUSHARE_TOKEN + + # 数据源配置 + self.data_sources_config = { + "tushare": { + "enabled": bool(self.tushare_token), + "api_key": self.tushare_token, + "token": self.tushare_token, + "base_url": "http://api.tushare.pro", + "timeout": 30, + "max_retries": 3, + "retry_delay": 1, + "name": "tushare" + }, + "yahoo": { + "enabled": True, + "base_url": "https://query1.finance.yahoo.com", + "timeout": 30, + "max_retries": 3, + "retry_delay": 1, + "name": "yahoo" + } + } + + # AI服务配置 + self.ai_services_config = { + "gemini": { + "enabled": bool(self.gemini_api_key), + "api_key": self.gemini_api_key, + "model": "gemini-pro", + "base_url": "https://generativelanguage.googleapis.com/v1beta", + "timeout": 60, + "max_retries": 3, + "retry_delay": 2, + "temperature": 0.7, + "top_p": 0.8, + "top_k": 40, + "max_output_tokens": 8192 + } + } + + def validate_gemini_config(self) -> bool: + """验证Gemini API配置""" + return bool(self.gemini_api_key) + + def validate_tushare_config(self) -> bool: + """验证Tushare API配置""" + return bool(self.tushare_token) + + def get_data_source_manager_config(self) -> dict: + """获取数据源管理器配置""" + return { + "data_sources": self.data_sources_config, + "ai_services": self.ai_services_config, + "market_mapping": { + "china": "tushare", + "中国": "tushare", + "hongkong": "yahoo", + "香港": "yahoo", + "usa": "yahoo", + "美国": "yahoo", + "japan": "yahoo", + "日本": "yahoo" + }, + "fallback_sources": { + "tushare": ["yahoo"], + "yahoo": ["tushare"] + } + } + + +class DataSourceConfig: + """数据源配置类""" + + def __init__(self): + self.sources = { + "china": settings.CHINA_DATA_SOURCE, + "hongkong": settings.HK_DATA_SOURCE, + "usa": settings.US_DATA_SOURCE, + "japan": settings.JP_DATA_SOURCE + } + + def get_source_for_market(self, market: str) -> str: + """根据市场获取数据源""" + market_mapping = { + "中国": "china", + "香港": "hongkong", + "美国": "usa", + "日本": "japan" + } + + market_key = market_mapping.get(market, "china") + return self.sources.get(market_key, "tushare") + + +# 创建配置实例 +db_config = DatabaseConfig() +api_config = ExternalAPIConfig() +data_source_config = DataSourceConfig() \ No newline at end of file diff --git a/backend/app/core/database.py b/backend/app/core/database.py new file mode 100644 index 0000000..a504d64 --- /dev/null +++ b/backend/app/core/database.py @@ -0,0 +1,69 @@ +""" +数据库连接和会话管理 +""" + +from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.pool import NullPool +from typing import AsyncGenerator + +from .config import settings + +# 创建异步数据库引擎 +engine = create_async_engine( + settings.DATABASE_URL, + echo=settings.DATABASE_ECHO, + poolclass=NullPool, # 对于异步使用NullPool + future=True +) + +# 创建异步会话工厂 +AsyncSessionLocal = async_sessionmaker( + engine, + class_=AsyncSession, + expire_on_commit=False +) + +# 创建基础模型类 +Base = declarative_base() + + +async def get_db() -> AsyncGenerator[AsyncSession, None]: + """ + 数据库会话依赖注入 + 用于FastAPI路由中获取数据库会话 + """ + async with AsyncSessionLocal() as session: + try: + yield session + except Exception: + await session.rollback() + raise + finally: + await session.close() + + +async def init_db(): + """初始化数据库表""" + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + + +async def close_db(): + """关闭数据库连接""" + await engine.dispose() + + +async def check_db_connection() -> bool: + """检查数据库连接是否正常""" + try: + async with AsyncSessionLocal() as session: + await session.execute("SELECT 1") + return True + except Exception: + return False + + +async def get_db_session(): + """获取数据库会话的简化版本""" + return AsyncSessionLocal() \ No newline at end of file diff --git a/backend/app/core/dependencies.py b/backend/app/core/dependencies.py new file mode 100644 index 0000000..f672e07 --- /dev/null +++ b/backend/app/core/dependencies.py @@ -0,0 +1,51 @@ +""" +FastAPI依赖注入 +""" + +from fastapi import Depends, HTTPException, status +from sqlalchemy.ext.asyncio import AsyncSession +from typing import AsyncGenerator + +from .database import get_db +from .config import settings, api_config + + +async def get_database_session() -> AsyncGenerator[AsyncSession, None]: + """获取数据库会话依赖""" + async for session in get_db(): + yield session + + +def verify_gemini_api_key(): + """验证Gemini API密钥""" + if not api_config.validate_gemini_config(): + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail="Gemini API未配置或配置无效" + ) + return api_config.gemini_api_key + + +def verify_tushare_token(): + """验证Tushare Token""" + if not api_config.validate_tushare_config(): + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail="Tushare API未配置或配置无效" + ) + return api_config.tushare_token + + +class DatabaseDependency: + """数据库依赖类""" + + def __init__(self): + self.session = Depends(get_database_session) + + +class APIKeyDependency: + """API密钥依赖类""" + + def __init__(self): + self.gemini_key = Depends(verify_gemini_api_key) + self.tushare_token = Depends(verify_tushare_token) \ No newline at end of file diff --git a/backend/app/core/exceptions.py b/backend/app/core/exceptions.py new file mode 100644 index 0000000..1a251f6 --- /dev/null +++ b/backend/app/core/exceptions.py @@ -0,0 +1,98 @@ +""" +基础错误处理和异常类 +定义系统中使用的所有自定义异常 +""" + +from typing import Optional, Dict, Any + + +class StockAnalysisError(Exception): + """基础异常类""" + + def __init__(self, message: str, details: Optional[Dict[str, Any]] = None): + self.message = message + self.details = details or {} + super().__init__(self.message) + + +class DataSourceError(StockAnalysisError): + """数据源错误""" + + def __init__(self, message: str, data_source: Optional[str] = None, details: Optional[Dict[str, Any]] = None): + self.data_source = data_source + super().__init__(message, details) + + +class AIAnalysisError(StockAnalysisError): + """AI分析错误""" + + def __init__(self, message: str, model: Optional[str] = None, details: Optional[Dict[str, Any]] = None): + self.model = model + super().__init__(message, details) + + +class ConfigurationError(StockAnalysisError): + """配置错误""" + + def __init__(self, message: str, config_key: Optional[str] = None, details: Optional[Dict[str, Any]] = None): + self.config_key = config_key + super().__init__(message, details) + + +class DatabaseError(StockAnalysisError): + """数据库错误""" + + def __init__(self, message: str, operation: Optional[str] = None, details: Optional[Dict[str, Any]] = None): + self.operation = operation + super().__init__(message, details) + + +class ValidationError(StockAnalysisError): + """数据验证错误""" + + def __init__(self, message: str, field: Optional[str] = None, details: Optional[Dict[str, Any]] = None): + self.field = field + super().__init__(message, details) + + +class APIError(StockAnalysisError): + """API调用错误""" + + def __init__(self, message: str, status_code: Optional[int] = None, api_name: Optional[str] = None, details: Optional[Dict[str, Any]] = None): + self.status_code = status_code + self.api_name = api_name + super().__init__(message, details) + + +class ReportGenerationError(StockAnalysisError): + """报告生成错误""" + + def __init__(self, message: str, module_type: Optional[str] = None, details: Optional[Dict[str, Any]] = None): + self.module_type = module_type + super().__init__(message, details) + + +class SymbolNotFoundError(DataSourceError): + """证券代码未找到错误""" + + def __init__(self, symbol: str, market: str, data_source: Optional[str] = None): + message = f"证券代码 {symbol} 在市场 {market} 中未找到" + details = {"symbol": symbol, "market": market} + super().__init__(message, data_source, details) + + +class RateLimitError(APIError): + """API调用频率限制错误""" + + def __init__(self, api_name: str, retry_after: Optional[int] = None): + message = f"API {api_name} 调用频率超限" + details = {"retry_after": retry_after} if retry_after else {} + super().__init__(message, 429, api_name, details) + + +class AuthenticationError(APIError): + """API认证错误""" + + def __init__(self, api_name: str, details: Optional[Dict[str, Any]] = None): + message = f"API {api_name} 认证失败" + super().__init__(message, 401, api_name, details) \ No newline at end of file diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py new file mode 100644 index 0000000..4d936e0 --- /dev/null +++ b/backend/app/models/__init__.py @@ -0,0 +1,17 @@ +""" +数据模型包 +包含SQLAlchemy数据模型定义 +""" + +# 导入所有模型以确保它们被注册到Base.metadata +from .report import Report +from .analysis_module import AnalysisModule +from .progress_tracking import ProgressTracking +from .system_config import SystemConfig + +__all__ = [ + "Report", + "AnalysisModule", + "ProgressTracking", + "SystemConfig" +] \ No newline at end of file diff --git a/backend/app/models/analysis_module.py b/backend/app/models/analysis_module.py new file mode 100644 index 0000000..375edff --- /dev/null +++ b/backend/app/models/analysis_module.py @@ -0,0 +1,41 @@ +""" +分析模块数据模型 +""" + +from sqlalchemy import Column, String, Integer, DateTime, Text, ForeignKey, Index +from sqlalchemy.dialects.postgresql import UUID, JSONB +from sqlalchemy.orm import relationship +import uuid + +from ..core.database import Base + + +class AnalysisModule(Base): + """分析模块表模型""" + + __tablename__ = "analysis_modules" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + report_id = Column(UUID(as_uuid=True), ForeignKey("reports.id", ondelete="CASCADE"), nullable=False) + module_type = Column(String(50), nullable=False, comment="模块类型") + module_order = Column(Integer, nullable=False, comment="模块顺序") + title = Column(String(200), nullable=False, comment="模块标题") + content = Column(JSONB, comment="模块内容") + status = Column(String(20), nullable=False, default="pending", comment="模块状态") + started_at = Column(DateTime(timezone=True), comment="开始时间") + completed_at = Column(DateTime(timezone=True), comment="完成时间") + error_message = Column(Text, comment="错误信息") + + # 关系 + report = relationship("Report", back_populates="analysis_modules") + + # 索引 + __table_args__ = ( + Index('idx_analysis_module_report_id', 'report_id'), + Index('idx_analysis_module_type', 'module_type'), + Index('idx_analysis_module_status', 'status'), + Index('idx_analysis_module_order', 'module_order'), + ) + + def __repr__(self): + return f"" \ No newline at end of file diff --git a/backend/app/models/progress_tracking.py b/backend/app/models/progress_tracking.py new file mode 100644 index 0000000..4d00a11 --- /dev/null +++ b/backend/app/models/progress_tracking.py @@ -0,0 +1,39 @@ +""" +进度追踪数据模型 +""" + +from sqlalchemy import Column, String, Integer, DateTime, Text, ForeignKey, Index +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import relationship +import uuid + +from ..core.database import Base + + +class ProgressTracking(Base): + """进度追踪表模型""" + + __tablename__ = "progress_tracking" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + report_id = Column(UUID(as_uuid=True), ForeignKey("reports.id", ondelete="CASCADE"), nullable=False) + step_name = Column(String(100), nullable=False, comment="步骤名称") + step_order = Column(Integer, nullable=False, comment="步骤顺序") + status = Column(String(20), nullable=False, default="pending", comment="步骤状态") + started_at = Column(DateTime(timezone=True), comment="开始时间") + completed_at = Column(DateTime(timezone=True), comment="完成时间") + duration_ms = Column(Integer, comment="耗时(毫秒)") + error_message = Column(Text, comment="错误信息") + + # 关系 + report = relationship("Report", back_populates="progress_tracking") + + # 索引 + __table_args__ = ( + Index('idx_progress_tracking_report_id', 'report_id'), + Index('idx_progress_tracking_status', 'status'), + Index('idx_progress_tracking_order', 'step_order'), + ) + + def __repr__(self): + return f"" \ No newline at end of file diff --git a/backend/app/models/report.py b/backend/app/models/report.py new file mode 100644 index 0000000..757dd04 --- /dev/null +++ b/backend/app/models/report.py @@ -0,0 +1,38 @@ +""" +报告数据模型 +""" + +from sqlalchemy import Column, String, DateTime, Text, Index +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.sql import func +from sqlalchemy.orm import relationship +import uuid + +from ..core.database import Base + + +class Report(Base): + """报告表模型""" + + __tablename__ = "reports" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + symbol = Column(String(20), nullable=False, comment="证券代码") + market = Column(String(20), nullable=False, comment="交易市场") + status = Column(String(20), nullable=False, default="generating", comment="报告状态") + created_at = Column(DateTime(timezone=True), server_default=func.now(), comment="创建时间") + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), comment="更新时间") + + # 关系 + analysis_modules = relationship("AnalysisModule", back_populates="report", cascade="all, delete-orphan") + progress_tracking = relationship("ProgressTracking", back_populates="report", cascade="all, delete-orphan") + + # 索引 + __table_args__ = ( + Index('idx_report_symbol_market', 'symbol', 'market'), + Index('idx_report_status', 'status'), + Index('idx_report_created_at', 'created_at'), + ) + + def __repr__(self): + return f"" \ No newline at end of file diff --git a/backend/app/models/system_config.py b/backend/app/models/system_config.py new file mode 100644 index 0000000..9713a81 --- /dev/null +++ b/backend/app/models/system_config.py @@ -0,0 +1,30 @@ +""" +系统配置数据模型 +""" + +from sqlalchemy import Column, String, DateTime, Index +from sqlalchemy.dialects.postgresql import UUID, JSONB +from sqlalchemy.sql import func +import uuid + +from ..core.database import Base + + +class SystemConfig(Base): + """系统配置表模型""" + + __tablename__ = "system_config" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + config_key = Column(String(100), unique=True, nullable=False, comment="配置键") + config_value = Column(JSONB, nullable=False, comment="配置值") + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), comment="更新时间") + + # 索引 + __table_args__ = ( + Index('idx_system_config_key', 'config_key'), + Index('idx_system_config_updated_at', 'updated_at'), + ) + + def __repr__(self): + return f"" \ No newline at end of file diff --git a/backend/app/routers/__init__.py b/backend/app/routers/__init__.py new file mode 100644 index 0000000..8eb4a7e --- /dev/null +++ b/backend/app/routers/__init__.py @@ -0,0 +1,8 @@ +""" +API路由包 +包含所有API端点定义 +""" + +from . import reports, config, progress + +__all__ = ["reports", "config", "progress"] \ No newline at end of file diff --git a/backend/app/routers/config.py b/backend/app/routers/config.py new file mode 100644 index 0000000..29ec848 --- /dev/null +++ b/backend/app/routers/config.py @@ -0,0 +1,124 @@ +""" +配置相关API路由 +""" + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.ext.asyncio import AsyncSession +import logging + +from ..core.dependencies import get_database_session +from ..schemas.config import ConfigResponse, ConfigUpdateRequest, ConfigTestRequest, ConfigTestResponse +from ..services.config_manager import ConfigManager +from ..core.exceptions import ConfigurationError, DatabaseError + +logger = logging.getLogger(__name__) +router = APIRouter() + + +@router.get("/", response_model=ConfigResponse) +async def get_config( + db: AsyncSession = Depends(get_database_session) +): + """获取系统配置""" + + try: + config_manager = ConfigManager(db) + config = await config_manager.get_config() + logger.info("获取系统配置成功") + return config + + except DatabaseError as e: + logger.error(f"获取配置时数据库错误: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"数据库错误: {str(e)}" + ) + except Exception as e: + logger.error(f"获取配置失败: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"获取配置失败: {str(e)}" + ) + + +@router.put("/", response_model=ConfigResponse) +async def update_config( + config_update: ConfigUpdateRequest, + db: AsyncSession = Depends(get_database_session) +): + """更新系统配置""" + + try: + # 验证至少有一个配置项需要更新 + if not any([config_update.database, config_update.gemini_api, config_update.data_sources]): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="至少需要提供一个配置项进行更新" + ) + + config_manager = ConfigManager(db) + updated_config = await config_manager.update_config(config_update) + logger.info("更新系统配置成功") + return updated_config + + except HTTPException: + raise + except ConfigurationError as e: + logger.error(f"配置错误: {str(e)}") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"配置错误: {str(e)}" + ) + except DatabaseError as e: + logger.error(f"更新配置时数据库错误: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"数据库错误: {str(e)}" + ) + except Exception as e: + logger.error(f"更新配置失败: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"更新配置失败: {str(e)}" + ) + + +@router.post("/test", response_model=ConfigTestResponse) +async def test_config( + test_request: ConfigTestRequest, + db: AsyncSession = Depends(get_database_session) +): + """测试配置连接""" + + try: + # 验证配置类型 + valid_types = ["database", "gemini", "data_source"] + if test_request.config_type not in valid_types: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"不支持的配置类型: {test_request.config_type},支持的类型: {valid_types}" + ) + + config_manager = ConfigManager(db) + test_result = await config_manager.test_config( + test_request.config_type, + test_request.config_data + ) + + logger.info(f"配置测试完成: {test_request.config_type}, 结果: {test_result.success}") + return test_result + + except HTTPException: + raise + except ConfigurationError as e: + logger.error(f"配置测试错误: {str(e)}") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"配置测试错误: {str(e)}" + ) + except Exception as e: + logger.error(f"配置测试失败: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"配置测试失败: {str(e)}" + ) \ No newline at end of file diff --git a/backend/app/routers/progress.py b/backend/app/routers/progress.py new file mode 100644 index 0000000..e964bcc --- /dev/null +++ b/backend/app/routers/progress.py @@ -0,0 +1,82 @@ +""" +进度追踪API路由 +""" + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.ext.asyncio import AsyncSession +from uuid import UUID +import logging + +from ..core.dependencies import get_database_session +from ..schemas.progress import ProgressResponse +from ..services.progress_tracker import ProgressTracker +from ..core.exceptions import DatabaseError + +logger = logging.getLogger(__name__) +router = APIRouter() + + +@router.get("/{report_id}", response_model=ProgressResponse) +async def get_report_progress( + report_id: UUID, + db: AsyncSession = Depends(get_database_session) +): + """获取报告生成进度""" + + try: + progress_tracker = ProgressTracker(db) + progress = await progress_tracker.get_progress(report_id) + logger.info(f"获取进度成功: {report_id}, 当前步骤: {progress.current_step}/{progress.total_steps}") + return progress + + except ValueError as e: + logger.warning(f"报告不存在或无进度记录: {report_id}") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=str(e) + ) + except DatabaseError as e: + logger.error(f"获取进度时数据库错误: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"数据库错误: {str(e)}" + ) + except Exception as e: + logger.error(f"获取进度失败: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"获取进度失败: {str(e)}" + ) + + +@router.post("/{report_id}/reset") +async def reset_report_progress( + report_id: UUID, + db: AsyncSession = Depends(get_database_session) +): + """重置报告生成进度""" + + try: + progress_tracker = ProgressTracker(db) + await progress_tracker.reset_progress(report_id) + logger.info(f"重置进度成功: {report_id}") + return {"message": "进度重置成功", "report_id": str(report_id)} + + except ValueError as e: + logger.warning(f"报告不存在: {report_id}") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=str(e) + ) + except DatabaseError as e: + logger.error(f"重置进度时数据库错误: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"数据库错误: {str(e)}" + ) + except Exception as e: + logger.error(f"重置进度失败: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"重置进度失败: {str(e)}" + ) \ No newline at end of file diff --git a/backend/app/routers/reports.py b/backend/app/routers/reports.py new file mode 100644 index 0000000..5b0914f --- /dev/null +++ b/backend/app/routers/reports.py @@ -0,0 +1,298 @@ +""" +报告相关API路由 +""" + +from fastapi import APIRouter, Depends, HTTPException, status, Query, BackgroundTasks +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select +from sqlalchemy.orm import selectinload +from typing import Optional, List +from uuid import UUID +import logging + +from ..core.dependencies import get_database_session +from ..models.report import Report +from ..schemas.report import ReportResponse, RegenerateRequest +from ..services.report_generator import ReportGenerator +from ..services.config_manager import ConfigManager +from ..core.exceptions import ReportGenerationError, DatabaseError + +logger = logging.getLogger(__name__) +router = APIRouter() + + +@router.get("/{symbol}", response_model=ReportResponse) +async def get_or_create_report( + symbol: str, + background_tasks: BackgroundTasks, + market: str = Query(..., description="交易市场"), + db: AsyncSession = Depends(get_database_session) +): + """获取或创建股票报告""" + + try: + # 验证输入参数 + symbol = symbol.upper().strip() + market = market.lower().strip() + + if not symbol: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="证券代码不能为空" + ) + + if market not in ["china", "hongkong", "usa", "japan"]: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="不支持的交易市场" + ) + + # 查询现有报告(包含关联的分析模块) + result = await db.execute( + select(Report) + .options(selectinload(Report.analysis_modules)) + .where( + Report.symbol == symbol, + Report.market == market + ) + ) + existing_report = result.scalar_one_or_none() + + if existing_report: + logger.info(f"找到现有报告: {symbol}-{market}, 状态: {existing_report.status}") + return ReportResponse.from_attributes(existing_report) + + # 创建新报告 + logger.info(f"开始生成新报告: {symbol}-{market}") + config_manager = ConfigManager(db) + report_generator = ReportGenerator(db, config_manager) + + # 在后台任务中生成报告 + background_tasks.add_task( + report_generator.generate_report_async, + symbol, + market + ) + + # 创建初始报告记录 + new_report = Report( + symbol=symbol, + market=market, + status="generating" + ) + db.add(new_report) + await db.commit() + await db.refresh(new_report) + + logger.info(f"创建报告记录: {new_report.id}") + return ReportResponse.from_attributes(new_report) + + except HTTPException: + raise + except Exception as e: + logger.error(f"获取或创建报告失败: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"获取或创建报告失败: {str(e)}" + ) + + +@router.post("/{symbol}/regenerate", response_model=ReportResponse) +async def regenerate_report( + symbol: str, + request: RegenerateRequest, + background_tasks: BackgroundTasks, + market: str = Query(..., description="交易市场"), + db: AsyncSession = Depends(get_database_session) +): + """重新生成报告""" + + try: + # 验证输入参数 + symbol = symbol.upper().strip() + market = market.lower().strip() + + if not symbol: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="证券代码不能为空" + ) + + if market not in ["china", "hongkong", "usa", "japan"]: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="不支持的交易市场" + ) + + # 查询现有报告 + result = await db.execute( + select(Report) + .options(selectinload(Report.analysis_modules)) + .where( + Report.symbol == symbol, + Report.market == market + ) + ) + existing_report = result.scalar_one_or_none() + + if existing_report and not request.force: + # 如果报告存在且不强制重新生成,返回现有报告 + logger.info(f"返回现有报告: {symbol}-{market}") + return ReportResponse.from_attributes(existing_report) + + # 删除现有报告(如果存在) + if existing_report: + logger.info(f"删除现有报告: {existing_report.id}") + await db.delete(existing_report) + await db.commit() + + # 创建新报告记录 + new_report = Report( + symbol=symbol, + market=market, + status="generating" + ) + db.add(new_report) + await db.commit() + await db.refresh(new_report) + + # 在后台任务中生成报告 + config_manager = ConfigManager(db) + report_generator = ReportGenerator(db, config_manager) + background_tasks.add_task( + report_generator.generate_report_async, + symbol, + market + ) + + logger.info(f"开始重新生成报告: {new_report.id}") + return ReportResponse.from_attributes(new_report) + + except HTTPException: + raise + except Exception as e: + logger.error(f"重新生成报告失败: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"重新生成报告失败: {str(e)}" + ) + + +@router.get("/{report_id}/details", response_model=ReportResponse) +async def get_report_details( + report_id: UUID, + db: AsyncSession = Depends(get_database_session) +): + """获取报告详情""" + + try: + result = await db.execute( + select(Report) + .options(selectinload(Report.analysis_modules)) + .where(Report.id == report_id) + ) + report = result.scalar_one_or_none() + + if not report: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="报告不存在" + ) + + logger.info(f"获取报告详情: {report_id}") + return ReportResponse.from_attributes(report) + + except HTTPException: + raise + except Exception as e: + logger.error(f"获取报告详情失败: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"获取报告详情失败: {str(e)}" + ) + + +@router.get("/", response_model=List[ReportResponse]) +async def list_reports( + skip: int = Query(0, ge=0, description="跳过的记录数"), + limit: int = Query(10, ge=1, le=100, description="返回的记录数"), + market: Optional[str] = Query(None, description="按市场筛选"), + status: Optional[str] = Query(None, description="按状态筛选"), + db: AsyncSession = Depends(get_database_session) +): + """获取报告列表""" + + try: + query = select(Report).options(selectinload(Report.analysis_modules)) + + # 添加筛选条件 + if market: + market = market.lower().strip() + if market not in ["china", "hongkong", "usa", "japan"]: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="不支持的交易市场" + ) + query = query.where(Report.market == market) + + if status: + status_value = status.lower().strip() + if status_value not in ["generating", "completed", "failed"]: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="不支持的状态值" + ) + query = query.where(Report.status == status_value) + + # 添加分页和排序 + query = query.order_by(Report.created_at.desc()).offset(skip).limit(limit) + + result = await db.execute(query) + reports = result.scalars().all() + + logger.info(f"获取报告列表: {len(reports)} 条记录") + return [ReportResponse.from_attributes(report) for report in reports] + + except HTTPException: + raise + except Exception as e: + logger.error(f"获取报告列表失败: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"获取报告列表失败: {str(e)}" + ) + + +@router.delete("/{report_id}") +async def delete_report( + report_id: UUID, + db: AsyncSession = Depends(get_database_session) +): + """删除报告""" + + try: + result = await db.execute( + select(Report).where(Report.id == report_id) + ) + report = result.scalar_one_or_none() + + if not report: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="报告不存在" + ) + + await db.delete(report) + await db.commit() + + logger.info(f"删除报告成功: {report_id}") + return {"message": "报告删除成功", "report_id": str(report_id)} + + except HTTPException: + raise + except Exception as e: + logger.error(f"删除报告失败: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"删除报告失败: {str(e)}" + ) \ No newline at end of file diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py new file mode 100644 index 0000000..0dcda10 --- /dev/null +++ b/backend/app/schemas/__init__.py @@ -0,0 +1,48 @@ +""" +Pydantic数据验证模式包 +""" + +from .report import ReportResponse, ReportCreate, ReportUpdate +from .config import ( + ConfigResponse, + ConfigUpdateRequest, + ConfigTestRequest, + ConfigTestResponse, + DatabaseConfig, + GeminiConfig, + DataSourceConfig +) +from .progress import ProgressResponse, StepTiming +from .data import ( + FinancialDataRequest, + MarketDataRequest, + FinancialDataResponse, + MarketDataResponse, + SymbolValidationRequest, + SymbolValidationResponse, + DataSourceStatus, + DataSourcesStatusResponse +) + +__all__ = [ + "ReportResponse", + "ReportCreate", + "ReportUpdate", + "ConfigResponse", + "ConfigUpdateRequest", + "ConfigTestRequest", + "ConfigTestResponse", + "DatabaseConfig", + "GeminiConfig", + "DataSourceConfig", + "ProgressResponse", + "StepTiming", + "FinancialDataRequest", + "MarketDataRequest", + "FinancialDataResponse", + "MarketDataResponse", + "SymbolValidationRequest", + "SymbolValidationResponse", + "DataSourceStatus", + "DataSourcesStatusResponse" +] \ No newline at end of file diff --git a/backend/app/schemas/config.py b/backend/app/schemas/config.py new file mode 100644 index 0000000..678cbb6 --- /dev/null +++ b/backend/app/schemas/config.py @@ -0,0 +1,62 @@ +""" +配置相关的Pydantic模式 +""" + +from pydantic import BaseModel, Field +from typing import Dict, Any, Optional, List + + +class DatabaseConfig(BaseModel): + """数据库配置模式""" + url: str = Field(..., description="数据库连接URL") + echo: bool = Field(False, description="是否输出SQL日志") + + +class GeminiConfig(BaseModel): + """Gemini API配置模式""" + api_key: str = Field(..., description="Gemini API密钥") + model: str = Field("gemini-pro", description="使用的模型") + temperature: float = Field(0.7, description="生成温度") + max_tokens: int = Field(2048, description="最大token数") + + +class DataSourceConfig(BaseModel): + """数据源配置模式""" + name: str = Field(..., description="数据源名称") + api_key: Optional[str] = Field(None, description="API密钥") + base_url: Optional[str] = Field(None, description="基础URL") + timeout: int = Field(30, description="超时时间(秒)") + + +class ConfigResponse(BaseModel): + """配置响应模式""" + database: Optional[DatabaseConfig] = None + gemini_api: Optional[GeminiConfig] = None + data_sources: Dict[str, DataSourceConfig] = {} + + +class ConfigUpdateRequest(BaseModel): + """配置更新请求模式""" + database: Optional[DatabaseConfig] = None + gemini_api: Optional[GeminiConfig] = None + data_sources: Optional[Dict[str, DataSourceConfig]] = None + + +class ConfigTestRequest(BaseModel): + """配置测试请求模式""" + config_type: str = Field(..., description="配置类型") + config_data: Dict[str, Any] = Field(..., description="配置数据") + + +class ConfigTestResponse(BaseModel): + """配置测试响应模式""" + success: bool = Field(..., description="测试是否成功") + message: str = Field(..., description="测试结果消息") + details: Optional[Dict[str, Any]] = Field(None, description="详细信息") + + +class ConfigValidationResponse(BaseModel): + """配置验证响应模式""" + valid: bool = Field(..., description="配置是否有效") + errors: List[str] = Field([], description="验证错误列表") + warnings: List[str] = Field([], description="验证警告列表") \ No newline at end of file diff --git a/backend/app/schemas/data.py b/backend/app/schemas/data.py new file mode 100644 index 0000000..618e414 --- /dev/null +++ b/backend/app/schemas/data.py @@ -0,0 +1,78 @@ +""" +数据相关的Pydantic模式 +""" + +from pydantic import BaseModel, Field +from typing import Dict, Any, Optional, List +from datetime import datetime + + +class FinancialDataRequest(BaseModel): + """财务数据请求模式""" + symbol: str = Field(..., description="证券代码") + market: str = Field(..., description="交易市场") + data_type: str = Field("all", description="数据类型") + period: Optional[str] = Field("annual", description="数据周期") + + +class MarketDataRequest(BaseModel): + """市场数据请求模式""" + symbol: str = Field(..., description="证券代码") + market: str = Field(..., description="交易市场") + start_date: Optional[datetime] = Field(None, description="开始日期") + end_date: Optional[datetime] = Field(None, description="结束日期") + + +class FinancialDataResponse(BaseModel): + """财务数据响应模式""" + symbol: str + market: str + data_source: str + last_updated: datetime + balance_sheet: Optional[Dict[str, Any]] = None + income_statement: Optional[Dict[str, Any]] = None + cash_flow: Optional[Dict[str, Any]] = None + key_metrics: Optional[Dict[str, Any]] = None + + +class MarketDataResponse(BaseModel): + """市场数据响应模式""" + symbol: str + market: str + data_source: str + last_updated: datetime + price_data: Optional[Dict[str, Any]] = None + volume_data: Optional[Dict[str, Any]] = None + technical_indicators: Optional[Dict[str, Any]] = None + + +class SymbolValidationRequest(BaseModel): + """证券代码验证请求模式""" + symbol: str = Field(..., description="证券代码") + market: str = Field(..., description="交易市场") + + +class SymbolValidationResponse(BaseModel): + """证券代码验证响应模式""" + symbol: str + market: str + is_valid: bool + company_name: Optional[str] = None + sector: Optional[str] = None + industry: Optional[str] = None + message: Optional[str] = None + + +class DataSourceStatus(BaseModel): + """数据源状态模式""" + name: str + is_available: bool + last_check: datetime + response_time_ms: Optional[int] = None + error_message: Optional[str] = None + + +class DataSourcesStatusResponse(BaseModel): + """数据源状态响应模式""" + sources: List[DataSourceStatus] + overall_status: str # "healthy", "degraded", "down" \ No newline at end of file diff --git a/backend/app/schemas/progress.py b/backend/app/schemas/progress.py new file mode 100644 index 0000000..bf6cc57 --- /dev/null +++ b/backend/app/schemas/progress.py @@ -0,0 +1,42 @@ +""" +进度追踪相关的Pydantic模式 +""" + +from pydantic import BaseModel, Field +from typing import List, Optional +from datetime import datetime +from uuid import UUID + + +class StepTiming(BaseModel): + """步骤计时模式""" + step_name: str = Field(..., description="步骤名称") + step_order: int = Field(..., description="步骤顺序") + status: str = Field(..., description="步骤状态") + started_at: Optional[datetime] = Field(None, description="开始时间") + completed_at: Optional[datetime] = Field(None, description="完成时间") + duration_ms: Optional[int] = Field(None, description="耗时(毫秒)") + error_message: Optional[str] = Field(None, description="错误信息") + + class Config: + from_attributes = True + + +class ProgressResponse(BaseModel): + """进度响应模式""" + report_id: UUID = Field(..., description="报告ID") + current_step: int = Field(..., description="当前步骤") + total_steps: int = Field(..., description="总步骤数") + current_step_name: str = Field(..., description="当前步骤名称") + status: str = Field(..., description="整体状态") + step_timings: List[StepTiming] = Field([], description="步骤计时列表") + estimated_remaining: Optional[int] = Field(None, description="预估剩余时间(秒)") + + class Config: + from_attributes = True + + +class ProgressResetResponse(BaseModel): + """进度重置响应模式""" + message: str = Field(..., description="操作结果消息") + report_id: str = Field(..., description="报告ID") \ No newline at end of file diff --git a/backend/app/schemas/report.py b/backend/app/schemas/report.py new file mode 100644 index 0000000..d89a3f7 --- /dev/null +++ b/backend/app/schemas/report.py @@ -0,0 +1,91 @@ +""" +报告相关的Pydantic模式 +""" + +from pydantic import BaseModel, Field +from typing import List, Optional, Dict, Any +from datetime import datetime +from uuid import UUID + + +class AnalysisModuleSchema(BaseModel): + """分析模块模式""" + id: UUID + module_type: str + module_order: int + title: str + content: Optional[Dict[str, Any]] = None + status: str + started_at: Optional[datetime] = None + completed_at: Optional[datetime] = None + error_message: Optional[str] = None + + class Config: + from_attributes = True + + +class ReportBase(BaseModel): + """报告基础模式""" + symbol: str = Field(..., description="证券代码") + market: str = Field(..., description="交易市场") + + +class ReportCreate(ReportBase): + """创建报告请求模式""" + pass + + +class ReportUpdate(BaseModel): + """更新报告请求模式""" + status: Optional[str] = None + + +class ReportResponse(ReportBase): + """报告响应模式""" + id: UUID + status: str + created_at: datetime + updated_at: datetime + analysis_modules: List[AnalysisModuleSchema] = [] + + class Config: + from_attributes = True + + +class ReportListResponse(BaseModel): + """报告列表响应模式""" + reports: List[ReportResponse] + total: int + skip: int + limit: int + + +class DeleteReportResponse(BaseModel): + """删除报告响应模式""" + message: str + report_id: str + + +class RegenerateRequest(BaseModel): + """重新生成报告请求模式""" + force: bool = Field(False, description="是否强制重新生成") + + +class AIAnalysisRequest(BaseModel): + """AI分析请求模式""" + symbol: str = Field(..., description="证券代码") + market: str = Field(..., description="交易市场") + analysis_type: str = Field(..., description="分析类型") + context_data: Optional[Dict[str, Any]] = Field(None, description="上下文数据") + + +class AIAnalysisResponse(BaseModel): + """AI分析响应模式""" + symbol: str + market: str + analysis_type: str + content: Dict[str, Any] + model_used: str + generated_at: datetime + + model_config = {"protected_namespaces": ()} \ No newline at end of file diff --git a/backend/app/services/__init__.py b/backend/app/services/__init__.py new file mode 100644 index 0000000..acda386 --- /dev/null +++ b/backend/app/services/__init__.py @@ -0,0 +1,16 @@ +""" +业务服务包 +包含核心业务逻辑实现 +""" + +from .config_manager import ConfigManager +from .data_fetcher import DataFetcher +from .report_generator import ReportGenerator +from .progress_tracker import ProgressTracker + +__all__ = [ + "ConfigManager", + "DataFetcher", + "ReportGenerator", + "ProgressTracker" +] \ No newline at end of file diff --git a/backend/app/services/ai_analyzer.py b/backend/app/services/ai_analyzer.py new file mode 100644 index 0000000..768f7bd --- /dev/null +++ b/backend/app/services/ai_analyzer.py @@ -0,0 +1,803 @@ +""" +AI分析服务 +处理Gemini API集成和AI分析功能 +""" + +from typing import Dict, Any, Optional, List +import httpx +import asyncio +import json +from datetime import datetime + +from ..core.exceptions import ( + AIAnalysisError, + APIError, + AuthenticationError, + RateLimitError +) +from ..schemas.report import AIAnalysisRequest, AIAnalysisResponse + + +class GeminiAnalyzer: + """Gemini AI分析器""" + + def __init__(self, api_key: str, config: Optional[Dict[str, Any]] = None): + if not api_key: + raise AuthenticationError("gemini", {"message": "Gemini API密钥未配置"}) + + self.api_key = api_key + self.config = config or {} + self.base_url = self.config.get("base_url", "https://generativelanguage.googleapis.com/v1beta") + self.model = self.config.get("model", "gemini-pro") + self.timeout = self.config.get("timeout", 60) + self.max_retries = self.config.get("max_retries", 3) + self.retry_delay = self.config.get("retry_delay", 2) + + # 生成配置 + self.generation_config = { + "temperature": self.config.get("temperature", 0.7), + "top_p": self.config.get("top_p", 0.8), + "top_k": self.config.get("top_k", 40), + "max_output_tokens": self.config.get("max_output_tokens", 8192), + } + + async def analyze_business_info(self, symbol: str, market: str, financial_data: Dict[str, Any]) -> AIAnalysisResponse: + """分析公司业务信息""" + prompt = self._build_business_info_prompt(symbol, market, financial_data) + + try: + result = await self._retry_request(self._call_gemini_api, prompt) + + return AIAnalysisResponse( + symbol=symbol, + market=market, + analysis_type="business_info", + content=self._parse_business_info_response(result), + model_used=self.model, + generated_at=datetime.now() + ) + except Exception as e: + if isinstance(e, (AIAnalysisError, APIError)): + raise + raise AIAnalysisError(f"业务信息分析失败: {str(e)}", self.model) + + async def analyze_fundamental(self, symbol: str, market: str, financial_data: Dict[str, Any], business_info: Dict[str, Any]) -> AIAnalysisResponse: + """基本面分析(景林模型)""" + prompt = self._build_fundamental_analysis_prompt(symbol, market, financial_data, business_info) + + try: + result = await self._retry_request(self._call_gemini_api, prompt) + + return AIAnalysisResponse( + symbol=symbol, + market=market, + analysis_type="fundamental_analysis", + content=self._parse_fundamental_response(result), + model_used=self.model, + generated_at=datetime.now() + ) + except Exception as e: + if isinstance(e, (AIAnalysisError, APIError)): + raise + raise AIAnalysisError(f"基本面分析失败: {str(e)}", self.model) + + async def analyze_bullish_case(self, symbol: str, market: str, context_data: Dict[str, Any]) -> AIAnalysisResponse: + """看涨分析(隐藏资产、护城河分析)""" + prompt = self._build_bullish_analysis_prompt(symbol, market, context_data) + + try: + result = await self._retry_request(self._call_gemini_api, prompt) + + return AIAnalysisResponse( + symbol=symbol, + market=market, + analysis_type="bullish_analysis", + content=self._parse_bullish_response(result), + model_used=self.model, + generated_at=datetime.now() + ) + except Exception as e: + if isinstance(e, (AIAnalysisError, APIError)): + raise + raise AIAnalysisError(f"看涨分析失败: {str(e)}", self.model) + + async def analyze_bearish_case(self, symbol: str, market: str, context_data: Dict[str, Any]) -> AIAnalysisResponse: + """看跌分析(价值底线、最坏情况分析)""" + prompt = self._build_bearish_analysis_prompt(symbol, market, context_data) + + try: + result = await self._retry_request(self._call_gemini_api, prompt) + + return AIAnalysisResponse( + symbol=symbol, + market=market, + analysis_type="bearish_analysis", + content=self._parse_bearish_response(result), + model_used=self.model, + generated_at=datetime.now() + ) + except Exception as e: + if isinstance(e, (AIAnalysisError, APIError)): + raise + raise AIAnalysisError(f"看跌分析失败: {str(e)}", self.model) + + async def analyze_market_sentiment(self, symbol: str, market: str, context_data: Dict[str, Any]) -> AIAnalysisResponse: + """市场情绪分析""" + prompt = self._build_market_analysis_prompt(symbol, market, context_data) + + try: + result = await self._retry_request(self._call_gemini_api, prompt) + + return AIAnalysisResponse( + symbol=symbol, + market=market, + analysis_type="market_analysis", + content=self._parse_market_response(result), + model_used=self.model, + generated_at=datetime.now() + ) + except Exception as e: + if isinstance(e, (AIAnalysisError, APIError)): + raise + raise AIAnalysisError(f"市场分析失败: {str(e)}", self.model) + + async def analyze_news_catalysts(self, symbol: str, market: str, context_data: Dict[str, Any]) -> AIAnalysisResponse: + """新闻催化剂分析""" + prompt = self._build_news_analysis_prompt(symbol, market, context_data) + + try: + result = await self._retry_request(self._call_gemini_api, prompt) + + return AIAnalysisResponse( + symbol=symbol, + market=market, + analysis_type="news_analysis", + content=self._parse_news_response(result), + model_used=self.model, + generated_at=datetime.now() + ) + except Exception as e: + if isinstance(e, (AIAnalysisError, APIError)): + raise + raise AIAnalysisError(f"新闻分析失败: {str(e)}", self.model) + + async def analyze_trading_dynamics(self, symbol: str, market: str, context_data: Dict[str, Any]) -> AIAnalysisResponse: + """交易动态分析""" + prompt = self._build_trading_analysis_prompt(symbol, market, context_data) + + try: + result = await self._retry_request(self._call_gemini_api, prompt) + + return AIAnalysisResponse( + symbol=symbol, + market=market, + analysis_type="trading_analysis", + content=self._parse_trading_response(result), + model_used=self.model, + generated_at=datetime.now() + ) + except Exception as e: + if isinstance(e, (AIAnalysisError, APIError)): + raise + raise AIAnalysisError(f"交易分析失败: {str(e)}", self.model) + + async def analyze_insider_institutional(self, symbol: str, market: str, context_data: Dict[str, Any]) -> AIAnalysisResponse: + """内部人与机构动向分析""" + prompt = self._build_insider_analysis_prompt(symbol, market, context_data) + + try: + result = await self._retry_request(self._call_gemini_api, prompt) + + return AIAnalysisResponse( + symbol=symbol, + market=market, + analysis_type="insider_analysis", + content=self._parse_insider_response(result), + model_used=self.model, + generated_at=datetime.now() + ) + except Exception as e: + if isinstance(e, (AIAnalysisError, APIError)): + raise + raise AIAnalysisError(f"内部人分析失败: {str(e)}", self.model) + + async def generate_final_conclusion(self, symbol: str, market: str, all_analyses: List[Dict[str, Any]]) -> AIAnalysisResponse: + """生成最终结论""" + prompt = self._build_conclusion_prompt(symbol, market, all_analyses) + + try: + result = await self._retry_request(self._call_gemini_api, prompt) + + return AIAnalysisResponse( + symbol=symbol, + market=market, + analysis_type="final_conclusion", + content=self._parse_conclusion_response(result), + model_used=self.model, + generated_at=datetime.now() + ) + except Exception as e: + if isinstance(e, (AIAnalysisError, APIError)): + raise + raise AIAnalysisError(f"最终结论生成失败: {str(e)}", self.model) + + async def _call_gemini_api(self, prompt: str) -> str: + """调用Gemini API""" + url = f"{self.base_url}/models/{self.model}:generateContent" + + headers = { + "Content-Type": "application/json", + } + + data = { + "contents": [ + { + "parts": [ + { + "text": prompt + } + ] + } + ], + "generationConfig": self.generation_config + } + + params = { + "key": self.api_key + } + + try: + async with httpx.AsyncClient(timeout=self.timeout) as client: + response = await client.post(url, json=data, headers=headers, params=params) + response.raise_for_status() + + result = response.json() + + if "error" in result: + error = result["error"] + error_code = error.get("code", 0) + error_message = error.get("message", "Unknown error") + + if error_code == 401 or "API key" in error_message: + raise AuthenticationError("gemini", {"message": error_message}) + elif error_code == 429 or "quota" in error_message.lower(): + raise RateLimitError("gemini") + else: + raise APIError(f"Gemini API错误: {error_message}", error_code, "gemini") + + candidates = result.get("candidates", []) + if not candidates: + raise AIAnalysisError("Gemini API返回空结果", self.model) + + content = candidates[0].get("content", {}) + parts = content.get("parts", []) + if not parts: + raise AIAnalysisError("Gemini API返回内容为空", self.model) + + return parts[0].get("text", "") + + except httpx.HTTPStatusError as e: + if e.response.status_code == 401: + raise AuthenticationError("gemini", {"status_code": e.response.status_code}) + elif e.response.status_code == 429: + raise RateLimitError("gemini") + else: + raise APIError(f"HTTP错误: {e.response.status_code}", e.response.status_code, "gemini") + except httpx.RequestError as e: + raise AIAnalysisError(f"网络请求失败: {str(e)}", self.model) + + async def _retry_request(self, func, *args, **kwargs): + """重试机制""" + last_exception = None + + for attempt in range(self.max_retries): + try: + return await func(*args, **kwargs) + except (httpx.TimeoutException, httpx.ConnectError, AIAnalysisError) as e: + last_exception = e + if attempt < self.max_retries - 1: + await asyncio.sleep(self.retry_delay * (2 ** attempt)) # 指数退避 + continue + except (AuthenticationError, RateLimitError) as e: + # 认证错误和频率限制错误不重试 + raise e + except Exception as e: + # 其他异常不重试 + raise e + + # 所有重试都失败了 + raise AIAnalysisError( + f"Gemini API请求失败,已重试 {self.max_retries} 次: {str(last_exception)}", + self.model + ) + + def _build_business_info_prompt(self, symbol: str, market: str, financial_data: Dict[str, Any]) -> str: + """构建业务信息分析提示词""" + return f""" +请对股票代码 {symbol}({market}市场)进行全面的业务信息分析。 + +基于以下财务数据: +{json.dumps(financial_data, ensure_ascii=False, indent=2)} + +请提供以下内容的详细分析: + +1. 公司概览 + - 公司基本信息和历史背景 + - 主要业务领域和市场地位 + +2. 主营业务分析 + - 核心产品和服务 + - 业务模式和盈利模式 + - 主要收入来源构成 + +3. 发展历程 + - 重要发展里程碑 + - 业务转型和扩张历史 + +4. 核心团队 + - 管理层背景和经验 + - 关键人员变动情况 + +5. 供应链分析 + - 主要供应商和客户 + - 供应链风险和优势 + +6. 销售模式 + - 销售渠道和策略 + - 市场覆盖和客户群体 + +7. 未来展望 + - 发展战略和规划 + - 市场机遇和挑战 + +请用中文回答,内容要详实、客观,基于可获得的公开信息进行分析。 +""" + + def _build_fundamental_analysis_prompt(self, symbol: str, market: str, financial_data: Dict[str, Any], business_info: Dict[str, Any]) -> str: + """构建基本面分析提示词(景林模型)""" + return f""" +请使用景林投资的基本面分析框架,对股票代码 {symbol}({market}市场)进行深度分析。 + +财务数据: +{json.dumps(financial_data, ensure_ascii=False, indent=2)} + +业务信息: +{json.dumps(business_info, ensure_ascii=False, indent=2)} + +请按照以下景林模型问题集进行分析: + +1. 商业模式分析 + - 这是一门什么样的生意? + - 商业模式的核心竞争力是什么? + - 盈利模式是否可持续? + +2. 行业地位分析 + - 公司在行业中的地位如何? + - 市场份额和竞争优势? + - 行业发展趋势对公司的影响? + +3. 财务质量分析 + - 收入增长的质量如何? + - 盈利能力和现金流状况? + - 资产负债结构是否健康? + +4. 管理层评估 + - 管理层的能力和诚信度? + - 公司治理结构是否完善? + - 股东利益是否得到保护? + +5. 估值分析 + - 当前估值水平是否合理? + - 与同行业公司比较如何? + - 未来增长预期是否支撑估值? + +请用中文提供详细、专业的分析,每个方面都要有具体的数据支撑和逻辑推理。 +""" + + def _build_bullish_analysis_prompt(self, symbol: str, market: str, context_data: Dict[str, Any]) -> str: + """构建看涨分析提示词""" + return f""" +请对股票代码 {symbol}({market}市场)进行看涨分析,重点关注隐藏资产和护城河竞争优势。 + +基础数据: +{json.dumps(context_data, ensure_ascii=False, indent=2)} + +请从以下角度进行看涨分析: + +1. 隐藏资产发现 + - 账面价值被低估的资产 + - 无形资产的真实价值 + - 潜在的资产重估机会 + +2. 护城河分析 + - 品牌价值和客户忠诚度 + - 技术壁垒和专利保护 + - 规模经济和网络效应 + - 转换成本和客户粘性 + +3. 成长潜力 + - 新业务和新市场机会 + - 产品创新和技术升级 + - 市场扩张的可能性 + +4. 催化剂识别 + - 短期可能的积极因素 + - 政策支持和行业利好 + - 公司内部改革和优化 + +5. 最佳情况假设 + - 如果一切顺利,公司价值可能达到什么水平? + - 关键假设和实现路径 + +请用中文提供乐观但理性的分析,要有具体的逻辑支撑。 +""" + + def _build_bearish_analysis_prompt(self, symbol: str, market: str, context_data: Dict[str, Any]) -> str: + """构建看跌分析提示词""" + return f""" +请对股票代码 {symbol}({market}市场)进行看跌分析,重点关注价值底线和最坏情况。 + +基础数据: +{json.dumps(context_data, ensure_ascii=False, indent=2)} + +请从以下角度进行看跌分析: + +1. 价值底线分析 + - 清算价值估算 + - 资产的最低合理价值 + - 下行风险的底线在哪里 + +2. 主要风险因素 + - 行业周期性风险 + - 竞争加剧的威胁 + - 技术替代的可能性 + - 监管政策变化风险 + +3. 财务脆弱性 + - 债务压力和流动性风险 + - 现金流恶化的可能性 + - 盈利能力下降的风险 + +4. 管理层风险 + - 决策失误的历史 + - 治理结构的缺陷 + - 利益冲突的可能性 + +5. 最坏情况假设 + - 如果一切都出错,公司价值可能跌到什么水平? + - 关键风险因素和触发条件 + +请用中文提供谨慎但客观的分析,要有具体的风险量化。 +""" + + def _build_market_analysis_prompt(self, symbol: str, market: str, context_data: Dict[str, Any]) -> str: + """构建市场分析提示词""" + return f""" +请对股票代码 {symbol}({market}市场)进行市场情绪分析,重点关注分歧点与变化驱动。 + +基础数据: +{json.dumps(context_data, ensure_ascii=False, indent=2)} + +请从以下角度进行市场分析: + +1. 市场情绪评估 + - 当前市场对该股票的主流观点 + - 机构投资者的持仓变化 + - 散户投资者的情绪指标 + +2. 分歧点识别 + - 市场存在哪些主要分歧? + - 乐观派和悲观派的核心观点 + - 分歧的根本原因是什么? + +3. 变化驱动因素 + - 什么因素可能改变市场共识? + - 关键数据点和时间节点 + - 外部环境变化的影响 + +4. 资金流向分析 + - 主力资金的进出情况 + - 不同类型投资者的行为模式 + - 流动性状况评估 + +5. 市场预期vs现实 + - 市场预期是否过于乐观或悲观? + - 预期差的投资机会在哪里? + +请用中文提供专业的市场分析,要有数据支撑和逻辑推理。 +""" + + def _build_news_analysis_prompt(self, symbol: str, market: str, context_data: Dict[str, Any]) -> str: + """构建新闻分析提示词""" + return f""" +请对股票代码 {symbol}({market}市场)进行新闻催化剂分析,重点关注股价拐点预判。 + +基础数据: +{json.dumps(context_data, ensure_ascii=False, indent=2)} + +请从以下角度进行新闻分析: + +1. 近期重要新闻梳理 + - 公司公告和重大事件 + - 行业政策和监管变化 + - 宏观经济相关新闻 + +2. 催化剂识别 + - 正面催化剂(业绩超预期、政策利好等) + - 负面催化剂(风险事件、竞争加剧等) + - 中性但重要的信息 + +3. 拐点预判 + - 基本面拐点的可能时间 + - 市场情绪拐点的信号 + - 技术面拐点的确认 + +4. 新闻影响评估 + - 短期影响vs长期影响 + - 市场反应是否充分 + - 后续发展的可能路径 + +5. 关注要点 + - 未来需要重点关注的事件 + - 可能的风险点和机会点 + - 时间窗口和操作建议 + +请用中文提供前瞻性的分析,要有时间维度和影响程度的判断。 +""" + + def _build_trading_analysis_prompt(self, symbol: str, market: str, context_data: Dict[str, Any]) -> str: + """构建交易分析提示词""" + return f""" +请对股票代码 {symbol}({market}市场)进行交易分析,重点关注市场体量与增长路径。 + +基础数据: +{json.dumps(context_data, ensure_ascii=False, indent=2)} + +请从以下角度进行交易分析: + +1. 市场体量分析 + - 总市值和流通市值 + - 日均成交量和换手率 + - 市场容量和流动性评估 + +2. 增长路径分析 + - 历史增长轨迹和驱动因素 + - 未来增长的可能路径 + - 增长的可持续性评估 + +3. 交易特征分析 + - 股价波动特征和规律 + - 主要交易时段和模式 + - 大宗交易和异常交易情况 + +4. 技术面分析 + - 关键技术位和支撑阻力 + - 趋势线和形态分析 + - 技术指标的信号 + +5. 交易策略建议 + - 适合的交易时机和方式 + - 风险控制和仓位管理 + - 进出场点位的选择 + +请用中文提供实用的交易分析,要有具体的数据和操作建议。 +""" + + def _build_insider_analysis_prompt(self, symbol: str, market: str, context_data: Dict[str, Any]) -> str: + """构建内部人分析提示词""" + return f""" +请对股票代码 {symbol}({market}市场)进行内部人与机构动向分析。 + +基础数据: +{json.dumps(context_data, ensure_ascii=False, indent=2)} + +请从以下角度进行分析: + +1. 内部人交易分析 + - 高管和大股东的买卖行为 + - 内部人交易的时机和规模 + - 内部人交易的信号意义 + +2. 机构持仓分析 + - 主要机构投资者的持仓变化 + - 新进和退出的机构情况 + - 机构持仓集中度分析 + +3. 股东结构变化 + - 股权结构的演变趋势 + - 重要股东的进出情况 + - 股权激励和员工持股情况 + +4. 资金流向追踪 + - 大资金的进出时机 + - 不同类型资金的偏好 + - 资金成本和收益预期 + +5. 动向信号解读 + - 内部人和机构行为的一致性 + - 与股价走势的相关性 + - 对未来走势的指示意义 + +请用中文提供专业的分析,要有数据支撑和逻辑推理。 +""" + + def _build_conclusion_prompt(self, symbol: str, market: str, all_analyses: List[Dict[str, Any]]) -> str: + """构建最终结论提示词""" + analyses_text = "\n\n".join([ + f"{analysis.get('analysis_type', '未知分析')}:\n{json.dumps(analysis.get('content', {}), ensure_ascii=False, indent=2)}" + for analysis in all_analyses + ]) + + return f""" +基于以下所有分析结果,请对股票代码 {symbol}({market}市场)给出最终投资结论。 + +所有分析结果: +{analyses_text} + +请从以下角度进行综合分析: + +1. 关键矛盾识别 + - 当前最核心的投资矛盾是什么? + - 不同分析维度的结论是否一致? + - 主要的不确定性因素有哪些? + +2. 预期差分析 + - 市场预期与实际情况的差异 + - 可能被忽视或误解的关键信息 + - 预期差带来的投资机会 + +3. 拐点临近性判断 + - 基本面拐点的时间和概率 + - 市场情绪拐点的信号 + - 催化剂的时效性分析 + +4. 风险收益评估 + - 上行空间和下行风险的量化 + - 风险调整后的收益预期 + - 投资的风险收益比 + +5. 最终投资建议 + - 明确的投资观点(看多/看空/中性) + - 建议的投资时间框架 + - 关键的跟踪指标和退出条件 + +请用中文提供清晰、明确的投资结论,要有逻辑性和可操作性。 +""" + + def _parse_business_info_response(self, response: str) -> Dict[str, Any]: + """解析业务信息分析响应""" + return { + "company_overview": self._extract_section(response, "公司概览"), + "main_business": self._extract_section(response, "主营业务"), + "development_history": self._extract_section(response, "发展历程"), + "core_team": self._extract_section(response, "核心团队"), + "supply_chain": self._extract_section(response, "供应链"), + "sales_model": self._extract_section(response, "销售模式"), + "future_outlook": self._extract_section(response, "未来展望"), + "full_analysis": response + } + + def _parse_fundamental_response(self, response: str) -> Dict[str, Any]: + """解析基本面分析响应""" + return { + "business_model": self._extract_section(response, "商业模式"), + "industry_position": self._extract_section(response, "行业地位"), + "financial_quality": self._extract_section(response, "财务质量"), + "management_assessment": self._extract_section(response, "管理层"), + "valuation_analysis": self._extract_section(response, "估值分析"), + "full_analysis": response + } + + def _parse_bullish_response(self, response: str) -> Dict[str, Any]: + """解析看涨分析响应""" + return { + "hidden_assets": self._extract_section(response, "隐藏资产"), + "moat_analysis": self._extract_section(response, "护城河"), + "growth_potential": self._extract_section(response, "成长潜力"), + "catalysts": self._extract_section(response, "催化剂"), + "best_case": self._extract_section(response, "最佳情况"), + "full_analysis": response + } + + def _parse_bearish_response(self, response: str) -> Dict[str, Any]: + """解析看跌分析响应""" + return { + "value_floor": self._extract_section(response, "价值底线"), + "risk_factors": self._extract_section(response, "风险因素"), + "financial_vulnerability": self._extract_section(response, "财务脆弱性"), + "management_risks": self._extract_section(response, "管理层风险"), + "worst_case": self._extract_section(response, "最坏情况"), + "full_analysis": response + } + + def _parse_market_response(self, response: str) -> Dict[str, Any]: + """解析市场分析响应""" + return { + "market_sentiment": self._extract_section(response, "市场情绪"), + "disagreement_points": self._extract_section(response, "分歧点"), + "change_drivers": self._extract_section(response, "变化驱动"), + "capital_flow": self._extract_section(response, "资金流向"), + "expectation_vs_reality": self._extract_section(response, "预期vs现实"), + "full_analysis": response + } + + def _parse_news_response(self, response: str) -> Dict[str, Any]: + """解析新闻分析响应""" + return { + "recent_news": self._extract_section(response, "重要新闻"), + "catalysts": self._extract_section(response, "催化剂"), + "inflection_points": self._extract_section(response, "拐点预判"), + "news_impact": self._extract_section(response, "影响评估"), + "focus_points": self._extract_section(response, "关注要点"), + "full_analysis": response + } + + def _parse_trading_response(self, response: str) -> Dict[str, Any]: + """解析交易分析响应""" + return { + "market_size": self._extract_section(response, "市场体量"), + "growth_path": self._extract_section(response, "增长路径"), + "trading_characteristics": self._extract_section(response, "交易特征"), + "technical_analysis": self._extract_section(response, "技术面"), + "trading_strategy": self._extract_section(response, "交易策略"), + "full_analysis": response + } + + def _parse_insider_response(self, response: str) -> Dict[str, Any]: + """解析内部人分析响应""" + return { + "insider_trading": self._extract_section(response, "内部人交易"), + "institutional_holdings": self._extract_section(response, "机构持仓"), + "ownership_changes": self._extract_section(response, "股东结构"), + "capital_flow": self._extract_section(response, "资金流向"), + "signal_interpretation": self._extract_section(response, "信号解读"), + "full_analysis": response + } + + def _parse_conclusion_response(self, response: str) -> Dict[str, Any]: + """解析最终结论响应""" + return { + "key_contradictions": self._extract_section(response, "关键矛盾"), + "expectation_gap": self._extract_section(response, "预期差"), + "inflection_timing": self._extract_section(response, "拐点临近性"), + "risk_return": self._extract_section(response, "风险收益"), + "investment_recommendation": self._extract_section(response, "投资建议"), + "full_analysis": response + } + + def _extract_section(self, text: str, section_name: str) -> str: + """从文本中提取特定章节内容""" + lines = text.split('\n') + section_content = [] + in_section = False + + for line in lines: + if section_name in line and ('.' in line or ':' in line or ':' in line): + in_section = True + section_content.append(line) + continue + + if in_section: + if line.strip() and any(keyword in line for keyword in ['1.', '2.', '3.', '4.', '5.']) and section_name not in line: + # 遇到下一个主要章节,停止 + break + section_content.append(line) + + return '\n'.join(section_content).strip() + + +class AIAnalyzerFactory: + """AI分析器工厂""" + + @classmethod + def create_gemini_analyzer(cls, api_key: str, config: Optional[Dict[str, Any]] = None) -> GeminiAnalyzer: + """创建Gemini分析器""" + return GeminiAnalyzer(api_key, config) + + @classmethod + def create_analyzer(cls, analyzer_type: str, **kwargs) -> GeminiAnalyzer: + """创建分析器(可扩展支持其他AI服务)""" + if analyzer_type.lower() == "gemini": + return cls.create_gemini_analyzer(kwargs.get("api_key"), kwargs.get("config")) + else: + raise AIAnalysisError(f"不支持的AI分析器类型: {analyzer_type}") \ No newline at end of file diff --git a/backend/app/services/config_manager.py b/backend/app/services/config_manager.py new file mode 100644 index 0000000..32df288 --- /dev/null +++ b/backend/app/services/config_manager.py @@ -0,0 +1,260 @@ +""" +配置管理服务 +处理系统配置的读取、更新和验证 +""" + +from typing import Dict, Any, Optional +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select +import httpx +import asyncio + +from ..models.system_config import SystemConfig +from ..schemas.config import ConfigResponse, ConfigUpdateRequest, ConfigTestResponse +from ..core.exceptions import ConfigurationError, DatabaseError, APIError + + +class ConfigManager: + """配置管理器""" + + def __init__(self, db_session: AsyncSession): + self.db = db_session + + async def get_config(self) -> ConfigResponse: + """获取系统配置""" + try: + # 查询所有配置 + result = await self.db.execute(select(SystemConfig)) + configs = result.scalars().all() + + # 组织配置数据 + config_dict = {config.config_key: config.config_value for config in configs} + + return ConfigResponse( + database=config_dict.get("database"), + gemini_api=config_dict.get("gemini_api"), + data_sources=config_dict.get("data_sources", {}) + ) + except Exception as e: + raise DatabaseError(f"获取配置失败: {str(e)}", "get_config") + + async def update_config(self, config_update: ConfigUpdateRequest) -> ConfigResponse: + """更新系统配置""" + try: + # 更新数据库配置 + if config_update.database: + await self._update_config_item("database", config_update.database.dict()) + + # 更新Gemini API配置 + if config_update.gemini_api: + await self._update_config_item("gemini_api", config_update.gemini_api.dict()) + + # 更新数据源配置 + if config_update.data_sources: + data_sources_dict = {k: v.dict() for k, v in config_update.data_sources.items()} + await self._update_config_item("data_sources", data_sources_dict) + + await self.db.commit() + + # 返回更新后的配置 + return await self.get_config() + except Exception as e: + await self.db.rollback() + raise DatabaseError(f"更新配置失败: {str(e)}", "update_config") + + async def test_config(self, config_type: str, config_data: Dict[str, Any]) -> ConfigTestResponse: + """测试配置连接""" + + try: + if config_type == "database": + return await self._test_database_config(config_data) + elif config_type == "gemini": + return await self._test_gemini_config(config_data) + elif config_type == "data_source": + return await self._test_data_source_config(config_data) + else: + return ConfigTestResponse( + success=False, + message=f"不支持的配置类型: {config_type}" + ) + + except Exception as e: + return ConfigTestResponse( + success=False, + message=f"配置测试失败: {str(e)}" + ) + + async def _update_config_item(self, key: str, value: Dict[str, Any]): + """更新单个配置项""" + # 查询现有配置 + result = await self.db.execute( + select(SystemConfig).where(SystemConfig.config_key == key) + ) + config = result.scalar_one_or_none() + + if config: + # 更新现有配置 + config.config_value = value + else: + # 创建新配置 + config = SystemConfig(config_key=key, config_value=value) + self.db.add(config) + + async def _test_database_config(self, config_data: Dict[str, Any]) -> ConfigTestResponse: + """测试数据库配置""" + try: + # 尝试创建数据库连接 + from sqlalchemy.ext.asyncio import create_async_engine + + db_url = config_data.get("url") + if not db_url: + return ConfigTestResponse( + success=False, + message="数据库URL未配置" + ) + + # 创建临时引擎测试连接 + test_engine = create_async_engine(db_url, echo=False) + + # 测试连接 + async with test_engine.begin() as conn: + await conn.execute("SELECT 1") + + await test_engine.dispose() + + return ConfigTestResponse( + success=True, + message="数据库连接测试成功" + ) + except Exception as e: + return ConfigTestResponse( + success=False, + message=f"数据库连接测试失败: {str(e)}" + ) + + async def _test_gemini_config(self, config_data: Dict[str, Any]) -> ConfigTestResponse: + """测试Gemini API配置""" + try: + api_key = config_data.get("api_key") + if not api_key: + return ConfigTestResponse( + success=False, + message="Gemini API密钥未配置" + ) + + # 测试API调用 + async with httpx.AsyncClient(timeout=10.0) as client: + headers = {"Authorization": f"Bearer {api_key}"} + # 这里应该调用实际的Gemini API端点进行测试 + # 暂时模拟成功 + await asyncio.sleep(0.1) # 模拟网络延迟 + + return ConfigTestResponse( + success=True, + message="Gemini API连接测试成功" + ) + except Exception as e: + return ConfigTestResponse( + success=False, + message=f"Gemini API连接测试失败: {str(e)}" + ) + + async def get_data_source_config(self, market: str) -> Dict[str, Any]: + """获取指定市场的数据源配置""" + try: + result = await self.db.execute( + select(SystemConfig).where(SystemConfig.config_key == "data_sources") + ) + config = result.scalar_one_or_none() + + if not config: + raise ConfigurationError("数据源配置未找到", "data_sources") + + data_sources = config.config_value + + # 根据市场选择数据源 + market_lower = market.lower() + if market_lower == "china": + if "tushare" in data_sources: + return data_sources["tushare"] + else: + raise ConfigurationError("中国市场数据源(Tushare)未配置", "tushare") + else: + # 其他市场使用Yahoo Finance + if "yahoo" in data_sources: + return data_sources["yahoo"] + else: + raise ConfigurationError("国际市场数据源(Yahoo)未配置", "yahoo") + + except Exception as e: + if isinstance(e, ConfigurationError): + raise + raise ConfigurationError(f"获取数据源配置失败: {str(e)}", "data_sources") + + async def get_gemini_config(self) -> Dict[str, Any]: + """获取Gemini API配置""" + try: + result = await self.db.execute( + select(SystemConfig).where(SystemConfig.config_key == "gemini_api") + ) + config = result.scalar_one_or_none() + + if not config: + raise ConfigurationError("Gemini API配置未找到", "gemini_api") + + gemini_config = config.config_value + + if not gemini_config.get("api_key"): + raise ConfigurationError("Gemini API密钥未配置", "gemini_api") + + return gemini_config + + except Exception as e: + if isinstance(e, ConfigurationError): + raise + raise ConfigurationError(f"获取Gemini配置失败: {str(e)}", "gemini_api") + + async def _test_data_source_config(self, config_data: Dict[str, Any]) -> ConfigTestResponse: + """测试数据源配置""" + try: + name = config_data.get("name") + api_key = config_data.get("api_key") + base_url = config_data.get("base_url") + timeout = config_data.get("timeout", 30) + + if not name: + return ConfigTestResponse( + success=False, + message="数据源名称未配置" + ) + + # 根据数据源类型进行不同的测试 + if name.lower() == "tushare": + if not api_key: + return ConfigTestResponse( + success=False, + message="Tushare API密钥未配置" + ) + # 测试Tushare API + # 暂时模拟成功 + await asyncio.sleep(0.1) + elif name.lower() == "yahoo": + # 测试Yahoo Finance API + if base_url: + async with httpx.AsyncClient(timeout=timeout) as client: + response = await client.get(f"{base_url}/health", timeout=timeout) + if response.status_code != 200: + return ConfigTestResponse( + success=False, + message=f"数据源API返回错误状态码: {response.status_code}" + ) + + return ConfigTestResponse( + success=True, + message=f"数据源 {name} 连接测试成功" + ) + except Exception as e: + return ConfigTestResponse( + success=False, + message=f"数据源连接测试失败: {str(e)}" + ) \ No newline at end of file diff --git a/backend/app/services/data_fetcher.py b/backend/app/services/data_fetcher.py new file mode 100644 index 0000000..3133fa9 --- /dev/null +++ b/backend/app/services/data_fetcher.py @@ -0,0 +1,673 @@ +""" +数据获取服务基础架构 +处理外部数据源的数据获取 +""" + +from typing import Dict, Any, Optional +from abc import ABC, abstractmethod +import httpx +import asyncio +from datetime import datetime + +from ..schemas.data import ( + FinancialDataResponse, + MarketDataResponse, + SymbolValidationResponse, + DataSourceStatus +) +from ..core.exceptions import ( + DataSourceError, + APIError, + SymbolNotFoundError, + RateLimitError, + AuthenticationError +) + + +class DataFetcher(ABC): + """数据获取服务基类""" + + def __init__(self, config: Dict[str, Any]): + self.config = config + self.name = config.get("name", "unknown") + self.timeout = config.get("timeout", 30) + self.max_retries = config.get("max_retries", 3) + self.retry_delay = config.get("retry_delay", 1) + + @abstractmethod + async def fetch_financial_data(self, symbol: str, market: str) -> FinancialDataResponse: + """获取财务数据""" + pass + + @abstractmethod + async def fetch_market_data(self, symbol: str, market: str) -> MarketDataResponse: + """获取市场数据""" + pass + + @abstractmethod + async def validate_symbol(self, symbol: str, market: str) -> SymbolValidationResponse: + """验证证券代码""" + pass + + async def check_status(self) -> DataSourceStatus: + """检查数据源状态""" + start_time = datetime.now() + try: + # 尝试进行简单的健康检查 + await self._health_check() + end_time = datetime.now() + response_time = int((end_time - start_time).total_seconds() * 1000) + + return DataSourceStatus( + name=self.name, + is_available=True, + last_check=end_time, + response_time_ms=response_time + ) + except Exception as e: + end_time = datetime.now() + return DataSourceStatus( + name=self.name, + is_available=False, + last_check=end_time, + error_message=str(e) + ) + + @abstractmethod + async def _health_check(self): + """健康检查实现""" + pass + + async def _retry_request(self, func, *args, **kwargs): + """重试机制""" + last_exception = None + + for attempt in range(self.max_retries): + try: + return await func(*args, **kwargs) + except (httpx.TimeoutException, httpx.ConnectError) as e: + last_exception = e + if attempt < self.max_retries - 1: + await asyncio.sleep(self.retry_delay * (2 ** attempt)) # 指数退避 + continue + except Exception as e: + # 对于其他类型的异常,不重试 + raise e + + # 所有重试都失败了 + raise DataSourceError( + f"数据源 {self.name} 请求失败,已重试 {self.max_retries} 次", + self.name, + {"last_error": str(last_exception)} + ) + + +class TushareDataFetcher(DataFetcher): + """Tushare数据获取器""" + + def __init__(self, config: Dict[str, Any]): + super().__init__(config) + self.token = config.get("api_key") or config.get("token") + self.base_url = config.get("base_url", "http://api.tushare.pro") + + if not self.token: + raise AuthenticationError("tushare", {"message": "Tushare API token未配置"}) + + async def fetch_financial_data(self, symbol: str, market: str) -> FinancialDataResponse: + """获取财务数据""" + try: + # 转换证券代码格式 + ts_code = self._convert_symbol_format(symbol, market) + + # TODO: 实现实际的Tushare API调用 + # 这里暂时返回模拟数据 + financial_data = await self._retry_request(self._fetch_tushare_financial, ts_code) + + return FinancialDataResponse( + symbol=symbol, + market=market, + data_source="tushare", + last_updated=datetime.now(), + balance_sheet=financial_data.get("balance_sheet"), + income_statement=financial_data.get("income_statement"), + cash_flow=financial_data.get("cash_flow"), + key_metrics=financial_data.get("key_metrics") + ) + except Exception as e: + if isinstance(e, (DataSourceError, APIError)): + raise + raise DataSourceError(f"获取财务数据失败: {str(e)}", "tushare") + + async def fetch_market_data(self, symbol: str, market: str) -> MarketDataResponse: + """获取市场数据""" + try: + ts_code = self._convert_symbol_format(symbol, market) + + # TODO: 实现实际的Tushare API调用 + market_data = await self._retry_request(self._fetch_tushare_market, ts_code) + + return MarketDataResponse( + symbol=symbol, + market=market, + data_source="tushare", + last_updated=datetime.now(), + price_data=market_data.get("price_data"), + volume_data=market_data.get("volume_data"), + technical_indicators=market_data.get("technical_indicators") + ) + except Exception as e: + if isinstance(e, (DataSourceError, APIError)): + raise + raise DataSourceError(f"获取市场数据失败: {str(e)}", "tushare") + + async def validate_symbol(self, symbol: str, market: str) -> SymbolValidationResponse: + """验证证券代码""" + try: + ts_code = self._convert_symbol_format(symbol, market) + + # TODO: 实现实际的证券代码验证 + # 暂时模拟验证逻辑 + is_valid = await self._retry_request(self._validate_tushare_symbol, ts_code) + + return SymbolValidationResponse( + symbol=symbol, + market=market, + is_valid=is_valid, + company_name="示例公司" if is_valid else None, + message="证券代码有效" if is_valid else "证券代码无效" + ) + except Exception as e: + return SymbolValidationResponse( + symbol=symbol, + market=market, + is_valid=False, + message=f"验证失败: {str(e)}" + ) + + async def _health_check(self): + """健康检查""" + try: + async with httpx.AsyncClient(timeout=5) as client: + # 尝试调用一个简单的API来测试连通性 + await self._call_tushare_api(client, "stock_basic", {"limit": 1}) + except Exception as e: + raise DataSourceError(f"Tushare健康检查失败: {str(e)}", "tushare") + + def _convert_symbol_format(self, symbol: str, market: str) -> str: + """转换证券代码格式为Tushare格式""" + if market.lower() == "china": + # 中国股票代码格式转换 + if symbol.startswith("6"): + return f"{symbol}.SH" # 上海证券交易所 + elif symbol.startswith(("0", "3")): + return f"{symbol}.SZ" # 深圳证券交易所 + return symbol + + async def _fetch_tushare_financial(self, ts_code: str) -> Dict[str, Any]: + """获取Tushare财务数据""" + async with httpx.AsyncClient(timeout=self.timeout) as client: + # 获取资产负债表 + balance_sheet_data = await self._call_tushare_api( + client, "balancesheet", {"ts_code": ts_code, "period": "20231231"} + ) + + # 获取利润表 + income_data = await self._call_tushare_api( + client, "income", {"ts_code": ts_code, "period": "20231231"} + ) + + # 获取现金流量表 + cashflow_data = await self._call_tushare_api( + client, "cashflow", {"ts_code": ts_code, "period": "20231231"} + ) + + # 获取基本财务指标 + fina_indicator_data = await self._call_tushare_api( + client, "fina_indicator", {"ts_code": ts_code, "period": "20231231"} + ) + + return { + "balance_sheet": self._process_balance_sheet(balance_sheet_data), + "income_statement": self._process_income_statement(income_data), + "cash_flow": self._process_cash_flow(cashflow_data), + "key_metrics": self._process_key_metrics(fina_indicator_data) + } + + async def _fetch_tushare_market(self, ts_code: str) -> Dict[str, Any]: + """获取Tushare市场数据""" + async with httpx.AsyncClient(timeout=self.timeout) as client: + # 获取日线数据 + daily_data = await self._call_tushare_api( + client, "daily", {"ts_code": ts_code, "start_date": "20240101", "end_date": "20241231"} + ) + + # 获取基本信息 + stock_basic_data = await self._call_tushare_api( + client, "stock_basic", {"ts_code": ts_code} + ) + + return { + "price_data": self._process_price_data(daily_data), + "volume_data": self._process_volume_data(daily_data), + "technical_indicators": self._calculate_technical_indicators(daily_data), + "stock_info": self._process_stock_basic(stock_basic_data) + } + + async def _validate_tushare_symbol(self, ts_code: str) -> bool: + """验证Tushare证券代码""" + try: + async with httpx.AsyncClient(timeout=self.timeout) as client: + result = await self._call_tushare_api( + client, "stock_basic", {"ts_code": ts_code} + ) + return bool(result and len(result.get("items", [])) > 0) + except Exception: + return False + + async def _call_tushare_api(self, client: httpx.AsyncClient, api_name: str, params: Dict[str, Any]) -> Dict[str, Any]: + """调用Tushare API""" + request_data = { + "api_name": api_name, + "token": self.token, + "params": params, + "fields": "" + } + + try: + response = await client.post(self.base_url, json=request_data) + response.raise_for_status() + + result = response.json() + + if result.get("code") != 0: + error_msg = result.get("msg", "Unknown error") + if "权限" in error_msg or "token" in error_msg.lower(): + raise AuthenticationError("tushare", {"message": error_msg}) + elif "频率" in error_msg or "limit" in error_msg.lower(): + raise RateLimitError("tushare") + else: + raise APIError(f"Tushare API错误: {error_msg}", result.get("code"), "tushare") + + return result.get("data", {}) + + except httpx.HTTPStatusError as e: + if e.response.status_code == 401: + raise AuthenticationError("tushare", {"status_code": e.response.status_code}) + elif e.response.status_code == 429: + raise RateLimitError("tushare") + else: + raise APIError(f"HTTP错误: {e.response.status_code}", e.response.status_code, "tushare") + except httpx.RequestError as e: + raise DataSourceError(f"网络请求失败: {str(e)}", "tushare") + + def _process_balance_sheet(self, data: Dict[str, Any]) -> Dict[str, Any]: + """处理资产负债表数据""" + if not data or not data.get("items"): + return {} + + items = data["items"] + if not items: + return {} + + # 取最新一期数据 + latest = items[0] if isinstance(items[0], list) else items + fields = data.get("fields", []) + + if isinstance(latest, list) and fields: + # 将列表数据转换为字典 + balance_data = dict(zip(fields, latest)) + else: + balance_data = latest + + return { + "total_assets": balance_data.get("total_assets", 0), + "total_liab": balance_data.get("total_liab", 0), + "total_hldr_eqy_exc_min_int": balance_data.get("total_hldr_eqy_exc_min_int", 0), + "monetary_cap": balance_data.get("monetary_cap", 0), + "accounts_receiv": balance_data.get("accounts_receiv", 0), + "inventories": balance_data.get("inventories", 0) + } + + def _process_income_statement(self, data: Dict[str, Any]) -> Dict[str, Any]: + """处理利润表数据""" + if not data or not data.get("items"): + return {} + + items = data["items"] + if not items: + return {} + + latest = items[0] if isinstance(items[0], list) else items + fields = data.get("fields", []) + + if isinstance(latest, list) and fields: + income_data = dict(zip(fields, latest)) + else: + income_data = latest + + return { + "revenue": income_data.get("revenue", 0), + "operate_profit": income_data.get("operate_profit", 0), + "total_profit": income_data.get("total_profit", 0), + "n_income": income_data.get("n_income", 0), + "n_income_attr_p": income_data.get("n_income_attr_p", 0), + "basic_eps": income_data.get("basic_eps", 0) + } + + def _process_cash_flow(self, data: Dict[str, Any]) -> Dict[str, Any]: + """处理现金流量表数据""" + if not data or not data.get("items"): + return {} + + items = data["items"] + if not items: + return {} + + latest = items[0] if isinstance(items[0], list) else items + fields = data.get("fields", []) + + if isinstance(latest, list) and fields: + cashflow_data = dict(zip(fields, latest)) + else: + cashflow_data = latest + + return { + "n_cashflow_act": cashflow_data.get("n_cashflow_act", 0), + "n_cashflow_inv_act": cashflow_data.get("n_cashflow_inv_act", 0), + "n_cashflow_fin_act": cashflow_data.get("n_cashflow_fin_act", 0), + "c_cash_equ_end_period": cashflow_data.get("c_cash_equ_end_period", 0) + } + + def _process_key_metrics(self, data: Dict[str, Any]) -> Dict[str, Any]: + """处理关键财务指标数据""" + if not data or not data.get("items"): + return {} + + items = data["items"] + if not items: + return {} + + latest = items[0] if isinstance(items[0], list) else items + fields = data.get("fields", []) + + if isinstance(latest, list) and fields: + metrics_data = dict(zip(fields, latest)) + else: + metrics_data = latest + + return { + "pe": metrics_data.get("pe", 0), + "pb": metrics_data.get("pb", 0), + "ps": metrics_data.get("ps", 0), + "roe": metrics_data.get("roe", 0), + "roa": metrics_data.get("roa", 0), + "gross_margin": metrics_data.get("gross_margin", 0), + "debt_to_assets": metrics_data.get("debt_to_assets", 0) + } + + def _process_price_data(self, data: Dict[str, Any]) -> Dict[str, Any]: + """处理价格数据""" + if not data or not data.get("items"): + return {} + + items = data["items"] + if not items: + return {} + + # 取最新一天的数据 + latest = items[0] if isinstance(items[0], list) else items + fields = data.get("fields", []) + + if isinstance(latest, list) and fields: + price_data = dict(zip(fields, latest)) + else: + price_data = latest + + return { + "close": price_data.get("close", 0), + "open": price_data.get("open", 0), + "high": price_data.get("high", 0), + "low": price_data.get("low", 0), + "pre_close": price_data.get("pre_close", 0), + "change": price_data.get("change", 0), + "pct_chg": price_data.get("pct_chg", 0) + } + + def _process_volume_data(self, data: Dict[str, Any]) -> Dict[str, Any]: + """处理成交量数据""" + if not data or not data.get("items"): + return {} + + items = data["items"] + if not items: + return {} + + latest = items[0] if isinstance(items[0], list) else items + fields = data.get("fields", []) + + if isinstance(latest, list) and fields: + volume_data = dict(zip(fields, latest)) + else: + volume_data = latest + + return { + "vol": volume_data.get("vol", 0), + "amount": volume_data.get("amount", 0) + } + + def _calculate_technical_indicators(self, data: Dict[str, Any]) -> Dict[str, Any]: + """计算技术指标""" + if not data or not data.get("items"): + return {} + + items = data["items"] + if not items or len(items) < 20: + return {} + + # 简单的移动平均计算 + closes = [] + for item in items[:20]: # 取最近20天 + if isinstance(item, list): + fields = data.get("fields", []) + close_idx = fields.index("close") if "close" in fields else -1 + if close_idx >= 0: + closes.append(item[close_idx]) + else: + closes.append(item.get("close", 0)) + + if len(closes) >= 5: + ma_5 = sum(closes[:5]) / 5 + else: + ma_5 = 0 + + if len(closes) >= 20: + ma_20 = sum(closes) / 20 + else: + ma_20 = 0 + + return { + "ma_5": ma_5, + "ma_20": ma_20, + "ma_60": 0 # 需要更多数据计算 + } + + def _process_stock_basic(self, data: Dict[str, Any]) -> Dict[str, Any]: + """处理股票基本信息""" + if not data or not data.get("items"): + return {} + + items = data["items"] + if not items: + return {} + + latest = items[0] if isinstance(items[0], list) else items + fields = data.get("fields", []) + + if isinstance(latest, list) and fields: + basic_data = dict(zip(fields, latest)) + else: + basic_data = latest + + return { + "name": basic_data.get("name", ""), + "industry": basic_data.get("industry", ""), + "market": basic_data.get("market", ""), + "list_date": basic_data.get("list_date", "") + } + + +class YahooDataFetcher(DataFetcher): + """Yahoo Finance数据获取器""" + + def __init__(self, config: Dict[str, Any]): + super().__init__(config) + self.base_url = config.get("base_url", "https://query1.finance.yahoo.com") + + async def fetch_financial_data(self, symbol: str, market: str) -> FinancialDataResponse: + """获取财务数据""" + try: + yahoo_symbol = self._convert_symbol_format(symbol, market) + + # TODO: 实现实际的Yahoo Finance API调用 + financial_data = await self._retry_request(self._fetch_yahoo_financial, yahoo_symbol) + + return FinancialDataResponse( + symbol=symbol, + market=market, + data_source="yahoo", + last_updated=datetime.now(), + balance_sheet=financial_data.get("balance_sheet"), + income_statement=financial_data.get("income_statement"), + cash_flow=financial_data.get("cash_flow"), + key_metrics=financial_data.get("key_metrics") + ) + except Exception as e: + if isinstance(e, (DataSourceError, APIError)): + raise + raise DataSourceError(f"获取财务数据失败: {str(e)}", "yahoo") + + async def fetch_market_data(self, symbol: str, market: str) -> MarketDataResponse: + """获取市场数据""" + try: + yahoo_symbol = self._convert_symbol_format(symbol, market) + + # TODO: 实现实际的Yahoo Finance API调用 + market_data = await self._retry_request(self._fetch_yahoo_market, yahoo_symbol) + + return MarketDataResponse( + symbol=symbol, + market=market, + data_source="yahoo", + last_updated=datetime.now(), + price_data=market_data.get("price_data"), + volume_data=market_data.get("volume_data"), + technical_indicators=market_data.get("technical_indicators") + ) + except Exception as e: + if isinstance(e, (DataSourceError, APIError)): + raise + raise DataSourceError(f"获取市场数据失败: {str(e)}", "yahoo") + + async def validate_symbol(self, symbol: str, market: str) -> SymbolValidationResponse: + """验证证券代码""" + try: + yahoo_symbol = self._convert_symbol_format(symbol, market) + + # TODO: 实现实际的证券代码验证 + is_valid = await self._retry_request(self._validate_yahoo_symbol, yahoo_symbol) + + return SymbolValidationResponse( + symbol=symbol, + market=market, + is_valid=is_valid, + company_name="Example Company" if is_valid else None, + message="Symbol is valid" if is_valid else "Symbol not found" + ) + except Exception as e: + return SymbolValidationResponse( + symbol=symbol, + market=market, + is_valid=False, + message=f"Validation failed: {str(e)}" + ) + + async def _health_check(self): + """健康检查""" + async with httpx.AsyncClient(timeout=self.timeout) as client: + response = await client.get(f"{self.base_url}/v1/finance/search?q=AAPL") + if response.status_code != 200: + raise APIError(f"Yahoo Finance API返回状态码: {response.status_code}", response.status_code, "yahoo") + + def _convert_symbol_format(self, symbol: str, market: str) -> str: + """转换证券代码格式为Yahoo Finance格式""" + if market.lower() == "hongkong": + return f"{symbol}.HK" + elif market.lower() == "japan": + return f"{symbol}.T" + elif market.lower() == "china": + # 中国股票在Yahoo Finance中的格式 + if symbol.startswith("6"): + return f"{symbol}.SS" # 上海 + elif symbol.startswith(("0", "3")): + return f"{symbol}.SZ" # 深圳 + return symbol + + async def _fetch_yahoo_financial(self, yahoo_symbol: str) -> Dict[str, Any]: + """获取Yahoo Finance财务数据""" + # TODO: 实现实际的API调用 + return { + "balance_sheet": {"totalAssets": 2000000}, + "income_statement": {"totalRevenue": 800000}, + "cash_flow": {"operatingCashflow": 300000}, + "key_metrics": {"trailingPE": 18.5} + } + + async def _fetch_yahoo_market(self, yahoo_symbol: str) -> Dict[str, Any]: + """获取Yahoo Finance市场数据""" + # TODO: 实现实际的API调用 + return { + "price_data": {"regularMarketPrice": 150.0, "dayHigh": 155.0, "dayLow": 145.0}, + "volume_data": {"regularMarketVolume": 2000000}, + "technical_indicators": {"fiftyDayAverage": 148.0, "twoHundredDayAverage": 152.0} + } + + async def _validate_yahoo_symbol(self, yahoo_symbol: str) -> bool: + """验证Yahoo Finance证券代码""" + # TODO: 实现实际的验证逻辑 + return True + + +class DataFetcherFactory: + """数据获取器工厂""" + + _fetchers = { + "tushare": TushareDataFetcher, + "yahoo": YahooDataFetcher, + } + + @classmethod + def create_fetcher(cls, data_source: str, config: Dict[str, Any]) -> DataFetcher: + """创建数据获取器""" + data_source_lower = data_source.lower() + + if data_source_lower not in cls._fetchers: + raise DataSourceError( + f"不支持的数据源: {data_source}", + data_source, + {"supported_sources": list(cls._fetchers.keys())} + ) + + fetcher_class = cls._fetchers[data_source_lower] + return fetcher_class(config) + + @classmethod + def get_supported_sources(cls) -> list: + """获取支持的数据源列表""" + return list(cls._fetchers.keys()) + + @classmethod + def register_fetcher(cls, name: str, fetcher_class: type): + """注册新的数据获取器""" + if not issubclass(fetcher_class, DataFetcher): + raise ValueError("数据获取器必须继承自DataFetcher基类") + cls._fetchers[name.lower()] = fetcher_class \ No newline at end of file diff --git a/backend/app/services/data_source_manager.py b/backend/app/services/data_source_manager.py new file mode 100644 index 0000000..b717bca --- /dev/null +++ b/backend/app/services/data_source_manager.py @@ -0,0 +1,357 @@ +""" +数据源管理服务 +处理数据源配置和切换逻辑 +""" + +from typing import Dict, Any, Optional, List +import asyncio +from datetime import datetime + +from .data_fetcher import DataFetcher, DataFetcherFactory +from .ai_analyzer import GeminiAnalyzer, AIAnalyzerFactory +from ..core.exceptions import ( + DataSourceError, + ConfigurationError, + AIAnalysisError +) +from ..schemas.data import ( + FinancialDataResponse, + MarketDataResponse, + SymbolValidationResponse, + DataSourceStatus, + DataSourcesStatusResponse +) +from ..schemas.report import AIAnalysisResponse + + +class DataSourceManager: + """数据源管理器""" + + def __init__(self, config: Dict[str, Any]): + self.config = config + self._data_fetchers: Dict[str, DataFetcher] = {} + self._ai_analyzer: Optional[GeminiAnalyzer] = None + self._market_source_mapping = { + "china": "tushare", + "中国": "tushare", + "hongkong": "yahoo", + "香港": "yahoo", + "usa": "yahoo", + "美国": "yahoo", + "japan": "yahoo", + "日本": "yahoo" + } + + # 初始化数据获取器 + self._initialize_data_fetchers() + + # 初始化AI分析器 + self._initialize_ai_analyzer() + + def _initialize_data_fetchers(self): + """初始化数据获取器""" + data_sources_config = self.config.get("data_sources", {}) + + for source_name, source_config in data_sources_config.items(): + try: + if source_config.get("enabled", True): + fetcher = DataFetcherFactory.create_fetcher(source_name, source_config) + self._data_fetchers[source_name] = fetcher + except Exception as e: + print(f"警告: 初始化数据源 {source_name} 失败: {str(e)}") + + def _initialize_ai_analyzer(self): + """初始化AI分析器""" + ai_config = self.config.get("ai_services", {}) + gemini_config = ai_config.get("gemini", {}) + + if gemini_config.get("enabled", True) and gemini_config.get("api_key"): + try: + self._ai_analyzer = AIAnalyzerFactory.create_gemini_analyzer( + gemini_config["api_key"], + gemini_config + ) + except Exception as e: + print(f"警告: 初始化Gemini分析器失败: {str(e)}") + + def get_data_source_for_market(self, market: str) -> str: + """根据市场获取数据源""" + market_lower = market.lower() + + # 首先检查配置中的映射 + market_mapping = self.config.get("market_mapping", {}) + if market_lower in market_mapping: + return market_mapping[market_lower] + + # 使用默认映射 + return self._market_source_mapping.get(market_lower, "tushare") + + def get_data_fetcher(self, data_source: str) -> DataFetcher: + """获取数据获取器""" + if data_source not in self._data_fetchers: + raise DataSourceError(f"数据源 {data_source} 未配置或不可用", data_source) + + return self._data_fetchers[data_source] + + def get_ai_analyzer(self) -> GeminiAnalyzer: + """获取AI分析器""" + if not self._ai_analyzer: + raise AIAnalysisError("AI分析器未配置或不可用", "gemini") + + return self._ai_analyzer + + async def fetch_financial_data(self, symbol: str, market: str, preferred_source: Optional[str] = None) -> FinancialDataResponse: + """获取财务数据(支持数据源切换)""" + data_source = preferred_source or self.get_data_source_for_market(market) + + try: + fetcher = self.get_data_fetcher(data_source) + return await fetcher.fetch_financial_data(symbol, market) + except DataSourceError as e: + # 尝试备用数据源 + fallback_sources = self._get_fallback_sources(data_source) + + for fallback_source in fallback_sources: + try: + fallback_fetcher = self.get_data_fetcher(fallback_source) + return await fallback_fetcher.fetch_financial_data(symbol, market) + except Exception: + continue + + # 所有数据源都失败了 + raise e + + async def fetch_market_data(self, symbol: str, market: str, preferred_source: Optional[str] = None) -> MarketDataResponse: + """获取市场数据(支持数据源切换)""" + data_source = preferred_source or self.get_data_source_for_market(market) + + try: + fetcher = self.get_data_fetcher(data_source) + return await fetcher.fetch_market_data(symbol, market) + except DataSourceError as e: + # 尝试备用数据源 + fallback_sources = self._get_fallback_sources(data_source) + + for fallback_source in fallback_sources: + try: + fallback_fetcher = self.get_data_fetcher(fallback_source) + return await fallback_fetcher.fetch_market_data(symbol, market) + except Exception: + continue + + # 所有数据源都失败了 + raise e + + async def validate_symbol(self, symbol: str, market: str, preferred_source: Optional[str] = None) -> SymbolValidationResponse: + """验证证券代码(支持数据源切换)""" + data_source = preferred_source or self.get_data_source_for_market(market) + + try: + fetcher = self.get_data_fetcher(data_source) + return await fetcher.validate_symbol(symbol, market) + except DataSourceError as e: + # 尝试备用数据源 + fallback_sources = self._get_fallback_sources(data_source) + + for fallback_source in fallback_sources: + try: + fallback_fetcher = self.get_data_fetcher(fallback_source) + return await fallback_fetcher.validate_symbol(symbol, market) + except Exception: + continue + + # 所有数据源都失败了 + raise e + + async def analyze_with_ai(self, analysis_type: str, symbol: str, market: str, context_data: Dict[str, Any]) -> AIAnalysisResponse: + """使用AI进行分析""" + analyzer = self.get_ai_analyzer() + + if analysis_type == "business_info": + return await analyzer.analyze_business_info(symbol, market, context_data) + elif analysis_type == "fundamental_analysis": + business_info = context_data.get("business_info", {}) + financial_data = context_data.get("financial_data", {}) + return await analyzer.analyze_fundamental(symbol, market, financial_data, business_info) + elif analysis_type == "bullish_analysis": + return await analyzer.analyze_bullish_case(symbol, market, context_data) + elif analysis_type == "bearish_analysis": + return await analyzer.analyze_bearish_case(symbol, market, context_data) + elif analysis_type == "market_analysis": + return await analyzer.analyze_market_sentiment(symbol, market, context_data) + elif analysis_type == "news_analysis": + return await analyzer.analyze_news_catalysts(symbol, market, context_data) + elif analysis_type == "trading_analysis": + return await analyzer.analyze_trading_dynamics(symbol, market, context_data) + elif analysis_type == "insider_analysis": + return await analyzer.analyze_insider_institutional(symbol, market, context_data) + elif analysis_type == "final_conclusion": + all_analyses = context_data.get("all_analyses", []) + return await analyzer.generate_final_conclusion(symbol, market, all_analyses) + else: + raise AIAnalysisError(f"不支持的分析类型: {analysis_type}", "gemini") + + async def check_all_sources_status(self) -> DataSourcesStatusResponse: + """检查所有数据源状态""" + status_tasks = [] + + # 检查数据获取器状态 + for source_name, fetcher in self._data_fetchers.items(): + status_tasks.append(fetcher.check_status()) + + # 检查AI分析器状态 + if self._ai_analyzer: + status_tasks.append(self._check_ai_analyzer_status()) + + # 并发执行状态检查 + statuses = await asyncio.gather(*status_tasks, return_exceptions=True) + + source_statuses = [] + healthy_count = 0 + + for i, status in enumerate(statuses): + if isinstance(status, Exception): + # 处理异常情况 + if i < len(self._data_fetchers): + source_name = list(self._data_fetchers.keys())[i] + else: + source_name = "gemini" + + source_statuses.append(DataSourceStatus( + name=source_name, + is_available=False, + last_check=datetime.now(), + error_message=str(status) + )) + else: + source_statuses.append(status) + if status.is_available: + healthy_count += 1 + + # 确定整体状态 + total_sources = len(source_statuses) + if healthy_count == total_sources: + overall_status = "healthy" + elif healthy_count > 0: + overall_status = "degraded" + else: + overall_status = "down" + + return DataSourcesStatusResponse( + sources=source_statuses, + overall_status=overall_status + ) + + async def _check_ai_analyzer_status(self) -> DataSourceStatus: + """检查AI分析器状态""" + start_time = datetime.now() + try: + # 简单的健康检查 - 尝试生成一个很短的测试内容 + test_prompt = "请回答:1+1等于几?" + await self._ai_analyzer._call_gemini_api(test_prompt) + + end_time = datetime.now() + response_time = int((end_time - start_time).total_seconds() * 1000) + + return DataSourceStatus( + name="gemini", + is_available=True, + last_check=end_time, + response_time_ms=response_time + ) + except Exception as e: + end_time = datetime.now() + return DataSourceStatus( + name="gemini", + is_available=False, + last_check=end_time, + error_message=str(e) + ) + + def _get_fallback_sources(self, primary_source: str) -> List[str]: + """获取备用数据源列表""" + fallback_config = self.config.get("fallback_sources", {}) + + if primary_source in fallback_config: + return fallback_config[primary_source] + + # 默认备用策略 + all_sources = list(self._data_fetchers.keys()) + return [source for source in all_sources if source != primary_source] + + def update_config(self, new_config: Dict[str, Any]): + """更新配置""" + self.config.update(new_config) + + # 重新初始化数据获取器 + self._data_fetchers.clear() + self._initialize_data_fetchers() + + # 重新初始化AI分析器 + self._ai_analyzer = None + self._initialize_ai_analyzer() + + def get_supported_sources(self) -> List[str]: + """获取支持的数据源列表""" + return DataFetcherFactory.get_supported_sources() + + def get_available_sources(self) -> List[str]: + """获取当前可用的数据源列表""" + return list(self._data_fetchers.keys()) + + def is_ai_analyzer_available(self) -> bool: + """检查AI分析器是否可用""" + return self._ai_analyzer is not None + + +def create_data_source_manager(config: Dict[str, Any]) -> DataSourceManager: + """创建数据源管理器""" + return DataSourceManager(config) + + +# 默认配置示例 +DEFAULT_CONFIG = { + "data_sources": { + "tushare": { + "enabled": True, + "api_key": "", # 需要从环境变量或配置文件获取 + "base_url": "http://api.tushare.pro", + "timeout": 30, + "max_retries": 3, + "retry_delay": 1 + }, + "yahoo": { + "enabled": True, + "base_url": "https://query1.finance.yahoo.com", + "timeout": 30, + "max_retries": 3, + "retry_delay": 1 + } + }, + "ai_services": { + "gemini": { + "enabled": True, + "api_key": "", # 需要从环境变量或配置文件获取 + "model": "gemini-pro", + "timeout": 60, + "max_retries": 3, + "retry_delay": 2, + "temperature": 0.7, + "max_output_tokens": 8192 + } + }, + "market_mapping": { + "china": "tushare", + "中国": "tushare", + "hongkong": "yahoo", + "香港": "yahoo", + "usa": "yahoo", + "美国": "yahoo", + "japan": "yahoo", + "日本": "yahoo" + }, + "fallback_sources": { + "tushare": ["yahoo"], + "yahoo": ["tushare"] + } +} \ No newline at end of file diff --git a/backend/app/services/external_api_service.py b/backend/app/services/external_api_service.py new file mode 100644 index 0000000..973512a --- /dev/null +++ b/backend/app/services/external_api_service.py @@ -0,0 +1,322 @@ +""" +外部API集成服务 +统一管理所有外部API调用和数据源切换 +""" + +from typing import Dict, Any, Optional, List +import asyncio +from datetime import datetime + +from .data_source_manager import DataSourceManager, create_data_source_manager +from ..core.config import api_config +from ..core.exceptions import ( + DataSourceError, + AIAnalysisError, + ConfigurationError +) +from ..schemas.data import ( + FinancialDataResponse, + MarketDataResponse, + SymbolValidationResponse, + DataSourcesStatusResponse +) +from ..schemas.report import AIAnalysisResponse + + +class ExternalAPIService: + """外部API服务""" + + def __init__(self): + self._data_source_manager: Optional[DataSourceManager] = None + self._initialize_manager() + + def _initialize_manager(self): + """初始化数据源管理器""" + try: + config = api_config.get_data_source_manager_config() + self._data_source_manager = create_data_source_manager(config) + except Exception as e: + print(f"警告: 初始化数据源管理器失败: {str(e)}") + + async def get_financial_data(self, symbol: str, market: str, preferred_source: Optional[str] = None) -> FinancialDataResponse: + """获取财务数据""" + if not self._data_source_manager: + raise ConfigurationError("数据源管理器未初始化") + + try: + return await self._data_source_manager.fetch_financial_data(symbol, market, preferred_source) + except Exception as e: + raise DataSourceError(f"获取财务数据失败: {str(e)}", preferred_source or "unknown") + + async def get_market_data(self, symbol: str, market: str, preferred_source: Optional[str] = None) -> MarketDataResponse: + """获取市场数据""" + if not self._data_source_manager: + raise ConfigurationError("数据源管理器未初始化") + + try: + return await self._data_source_manager.fetch_market_data(symbol, market, preferred_source) + except Exception as e: + raise DataSourceError(f"获取市场数据失败: {str(e)}", preferred_source or "unknown") + + async def validate_stock_symbol(self, symbol: str, market: str, preferred_source: Optional[str] = None) -> SymbolValidationResponse: + """验证股票代码""" + if not self._data_source_manager: + raise ConfigurationError("数据源管理器未初始化") + + try: + return await self._data_source_manager.validate_symbol(symbol, market, preferred_source) + except Exception as e: + # 验证失败时返回无效结果而不是抛出异常 + return SymbolValidationResponse( + symbol=symbol, + market=market, + is_valid=False, + message=f"验证失败: {str(e)}" + ) + + async def analyze_business_info(self, symbol: str, market: str, financial_data: Dict[str, Any]) -> AIAnalysisResponse: + """分析公司业务信息""" + if not self._data_source_manager: + raise ConfigurationError("数据源管理器未初始化") + + if not self._data_source_manager.is_ai_analyzer_available(): + raise AIAnalysisError("AI分析器不可用", "gemini") + + try: + return await self._data_source_manager.analyze_with_ai( + "business_info", symbol, market, {"financial_data": financial_data} + ) + except Exception as e: + raise AIAnalysisError(f"业务信息分析失败: {str(e)}", "gemini") + + async def analyze_fundamental(self, symbol: str, market: str, financial_data: Dict[str, Any], business_info: Dict[str, Any]) -> AIAnalysisResponse: + """基本面分析""" + if not self._data_source_manager: + raise ConfigurationError("数据源管理器未初始化") + + if not self._data_source_manager.is_ai_analyzer_available(): + raise AIAnalysisError("AI分析器不可用", "gemini") + + try: + context_data = { + "financial_data": financial_data, + "business_info": business_info + } + return await self._data_source_manager.analyze_with_ai( + "fundamental_analysis", symbol, market, context_data + ) + except Exception as e: + raise AIAnalysisError(f"基本面分析失败: {str(e)}", "gemini") + + async def analyze_bullish_case(self, symbol: str, market: str, context_data: Dict[str, Any]) -> AIAnalysisResponse: + """看涨分析""" + if not self._data_source_manager: + raise ConfigurationError("数据源管理器未初始化") + + if not self._data_source_manager.is_ai_analyzer_available(): + raise AIAnalysisError("AI分析器不可用", "gemini") + + try: + return await self._data_source_manager.analyze_with_ai( + "bullish_analysis", symbol, market, context_data + ) + except Exception as e: + raise AIAnalysisError(f"看涨分析失败: {str(e)}", "gemini") + + async def analyze_bearish_case(self, symbol: str, market: str, context_data: Dict[str, Any]) -> AIAnalysisResponse: + """看跌分析""" + if not self._data_source_manager: + raise ConfigurationError("数据源管理器未初始化") + + if not self._data_source_manager.is_ai_analyzer_available(): + raise AIAnalysisError("AI分析器不可用", "gemini") + + try: + return await self._data_source_manager.analyze_with_ai( + "bearish_analysis", symbol, market, context_data + ) + except Exception as e: + raise AIAnalysisError(f"看跌分析失败: {str(e)}", "gemini") + + async def analyze_market_sentiment(self, symbol: str, market: str, context_data: Dict[str, Any]) -> AIAnalysisResponse: + """市场情绪分析""" + if not self._data_source_manager: + raise ConfigurationError("数据源管理器未初始化") + + if not self._data_source_manager.is_ai_analyzer_available(): + raise AIAnalysisError("AI分析器不可用", "gemini") + + try: + return await self._data_source_manager.analyze_with_ai( + "market_analysis", symbol, market, context_data + ) + except Exception as e: + raise AIAnalysisError(f"市场分析失败: {str(e)}", "gemini") + + async def analyze_news_catalysts(self, symbol: str, market: str, context_data: Dict[str, Any]) -> AIAnalysisResponse: + """新闻催化剂分析""" + if not self._data_source_manager: + raise ConfigurationError("数据源管理器未初始化") + + if not self._data_source_manager.is_ai_analyzer_available(): + raise AIAnalysisError("AI分析器不可用", "gemini") + + try: + return await self._data_source_manager.analyze_with_ai( + "news_analysis", symbol, market, context_data + ) + except Exception as e: + raise AIAnalysisError(f"新闻分析失败: {str(e)}", "gemini") + + async def analyze_trading_dynamics(self, symbol: str, market: str, context_data: Dict[str, Any]) -> AIAnalysisResponse: + """交易动态分析""" + if not self._data_source_manager: + raise ConfigurationError("数据源管理器未初始化") + + if not self._data_source_manager.is_ai_analyzer_available(): + raise AIAnalysisError("AI分析器不可用", "gemini") + + try: + return await self._data_source_manager.analyze_with_ai( + "trading_analysis", symbol, market, context_data + ) + except Exception as e: + raise AIAnalysisError(f"交易分析失败: {str(e)}", "gemini") + + async def analyze_insider_institutional(self, symbol: str, market: str, context_data: Dict[str, Any]) -> AIAnalysisResponse: + """内部人与机构动向分析""" + if not self._data_source_manager: + raise ConfigurationError("数据源管理器未初始化") + + if not self._data_source_manager.is_ai_analyzer_available(): + raise AIAnalysisError("AI分析器不可用", "gemini") + + try: + return await self._data_source_manager.analyze_with_ai( + "insider_analysis", symbol, market, context_data + ) + except Exception as e: + raise AIAnalysisError(f"内部人分析失败: {str(e)}", "gemini") + + async def generate_final_conclusion(self, symbol: str, market: str, all_analyses: List[Dict[str, Any]]) -> AIAnalysisResponse: + """生成最终结论""" + if not self._data_source_manager: + raise ConfigurationError("数据源管理器未初始化") + + if not self._data_source_manager.is_ai_analyzer_available(): + raise AIAnalysisError("AI分析器不可用", "gemini") + + try: + context_data = {"all_analyses": all_analyses} + return await self._data_source_manager.analyze_with_ai( + "final_conclusion", symbol, market, context_data + ) + except Exception as e: + raise AIAnalysisError(f"最终结论生成失败: {str(e)}", "gemini") + + async def check_all_services_status(self) -> DataSourcesStatusResponse: + """检查所有外部服务状态""" + if not self._data_source_manager: + raise ConfigurationError("数据源管理器未初始化") + + try: + return await self._data_source_manager.check_all_sources_status() + except Exception as e: + raise DataSourceError(f"检查服务状态失败: {str(e)}") + + def get_supported_data_sources(self) -> List[str]: + """获取支持的数据源列表""" + if not self._data_source_manager: + return [] + + return self._data_source_manager.get_supported_sources() + + def get_available_data_sources(self) -> List[str]: + """获取当前可用的数据源列表""" + if not self._data_source_manager: + return [] + + return self._data_source_manager.get_available_sources() + + def is_ai_service_available(self) -> bool: + """检查AI服务是否可用""" + if not self._data_source_manager: + return False + + return self._data_source_manager.is_ai_analyzer_available() + + def get_data_source_for_market(self, market: str) -> str: + """根据市场获取推荐的数据源""" + if not self._data_source_manager: + return "tushare" # 默认值 + + return self._data_source_manager.get_data_source_for_market(market) + + def update_configuration(self, new_config: Dict[str, Any]): + """更新配置""" + if self._data_source_manager: + self._data_source_manager.update_config(new_config) + else: + # 如果管理器未初始化,尝试重新初始化 + self._initialize_manager() + + async def test_data_source_connection(self, data_source: str, config: Dict[str, Any]) -> Dict[str, Any]: + """测试数据源连接""" + try: + # 创建临时的数据获取器进行测试 + from .data_fetcher import DataFetcherFactory + + test_fetcher = DataFetcherFactory.create_fetcher(data_source, config) + status = await test_fetcher.check_status() + + return { + "success": status.is_available, + "response_time_ms": status.response_time_ms, + "error_message": status.error_message + } + except Exception as e: + return { + "success": False, + "error_message": str(e) + } + + async def test_ai_service_connection(self, service_type: str, config: Dict[str, Any]) -> Dict[str, Any]: + """测试AI服务连接""" + try: + if service_type.lower() == "gemini": + from .ai_analyzer import AIAnalyzerFactory + + test_analyzer = AIAnalyzerFactory.create_gemini_analyzer( + config.get("api_key"), config + ) + + # 简单的测试调用 + start_time = datetime.now() + await test_analyzer._call_gemini_api("测试连接,请回答:OK") + end_time = datetime.now() + + response_time = int((end_time - start_time).total_seconds() * 1000) + + return { + "success": True, + "response_time_ms": response_time + } + else: + return { + "success": False, + "error_message": f"不支持的AI服务类型: {service_type}" + } + except Exception as e: + return { + "success": False, + "error_message": str(e) + } + + +# 创建全局服务实例 +external_api_service = ExternalAPIService() + + +def get_external_api_service() -> ExternalAPIService: + """获取外部API服务实例""" + return external_api_service \ No newline at end of file diff --git a/backend/app/services/progress_tracker.py b/backend/app/services/progress_tracker.py new file mode 100644 index 0000000..96c8582 --- /dev/null +++ b/backend/app/services/progress_tracker.py @@ -0,0 +1,163 @@ +""" +进度追踪服务 +处理报告生成进度的追踪和管理 +""" + +from typing import List, Optional +from uuid import UUID +from datetime import datetime +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select + +from ..models.progress_tracking import ProgressTracking +from ..schemas.progress import ProgressResponse, StepTiming + + +class ProgressTracker: + """进度追踪器""" + + def __init__(self, db_session: AsyncSession): + self.db = db_session + + async def initialize_progress(self, report_id: UUID): + """初始化进度追踪""" + + # 定义报告生成步骤 + steps = [ + "初始化报告", + "获取财务数据", + "生成业务信息", + "执行基本面分析", + "执行看涨分析", + "执行看跌分析", + "执行市场分析", + "执行新闻分析", + "执行交易分析", + "执行内部人分析", + "生成最终结论", + "保存报告" + ] + + # 创建进度记录 + for i, step_name in enumerate(steps, 1): + progress = ProgressTracking( + report_id=report_id, + step_name=step_name, + step_order=i, + status="pending" + ) + self.db.add(progress) + + await self.db.flush() + + async def start_step(self, report_id: UUID, step_name: str): + """开始执行步骤""" + result = await self.db.execute( + select(ProgressTracking).where( + ProgressTracking.report_id == report_id, + ProgressTracking.step_name == step_name + ) + ) + progress = result.scalar_one_or_none() + + if progress: + progress.status = "running" + progress.started_at = datetime.utcnow() + await self.db.flush() + + async def complete_step(self, report_id: UUID, step_name: str, success: bool = True, error_message: Optional[str] = None): + """完成步骤""" + result = await self.db.execute( + select(ProgressTracking).where( + ProgressTracking.report_id == report_id, + ProgressTracking.step_name == step_name + ) + ) + progress = result.scalar_one_or_none() + + if progress: + progress.status = "completed" if success else "failed" + progress.completed_at = datetime.utcnow() + progress.error_message = error_message + + # 计算耗时 + if progress.started_at: + duration = progress.completed_at - progress.started_at + progress.duration_ms = int(duration.total_seconds() * 1000) + + await self.db.flush() + + async def get_progress(self, report_id: UUID) -> ProgressResponse: + """获取进度信息""" + result = await self.db.execute( + select(ProgressTracking) + .where(ProgressTracking.report_id == report_id) + .order_by(ProgressTracking.step_order) + ) + progress_records = result.scalars().all() + + if not progress_records: + raise ValueError(f"未找到报告 {report_id} 的进度信息") + + # 计算当前步骤 + current_step = 1 + current_step_name = "初始化报告" + overall_status = "running" + + completed_count = 0 + failed_count = 0 + + for record in progress_records: + if record.status == "completed": + completed_count += 1 + elif record.status == "failed": + failed_count += 1 + elif record.status == "running": + current_step = record.step_order + current_step_name = record.step_name + + # 确定整体状态 + if failed_count > 0: + overall_status = "failed" + elif completed_count == len(progress_records): + overall_status = "completed" + + # 转换为StepTiming对象 + step_timings = [ + StepTiming( + step_name=record.step_name, + step_order=record.step_order, + status=record.status, + started_at=record.started_at, + completed_at=record.completed_at, + duration_ms=record.duration_ms, + error_message=record.error_message + ) + for record in progress_records + ] + + return ProgressResponse( + report_id=report_id, + current_step=current_step, + total_steps=len(progress_records), + current_step_name=current_step_name, + status=overall_status, + step_timings=step_timings, + estimated_remaining=self._estimate_remaining_time(step_timings) + ) + + def _estimate_remaining_time(self, step_timings: List[StepTiming]) -> Optional[int]: + """估算剩余时间""" + # 计算已完成步骤的平均耗时 + completed_durations = [ + timing.duration_ms for timing in step_timings + if timing.status == "completed" and timing.duration_ms + ] + + if not completed_durations: + return None + + avg_duration_ms = sum(completed_durations) / len(completed_durations) + remaining_steps = len([t for t in step_timings if t.status == "pending"]) + + return int((avg_duration_ms * remaining_steps) / 1000) # 转换为秒 \ No newline at end of file diff --git a/backend/app/services/report_generator.py b/backend/app/services/report_generator.py new file mode 100644 index 0000000..9c81211 --- /dev/null +++ b/backend/app/services/report_generator.py @@ -0,0 +1,643 @@ +""" +报告生成服务 +处理股票基本面分析报告的生成和管理 +""" + +from typing import Dict, Any, Optional, List +from uuid import UUID +from datetime import datetime +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select +import asyncio +import logging + +from ..models.report import Report +from ..models.analysis_module import AnalysisModule +from ..schemas.report import ReportResponse, AnalysisModuleSchema +from ..core.exceptions import ( + ReportGenerationError, + DataSourceError, + AIAnalysisError, + DatabaseError +) +from .progress_tracker import ProgressTracker +from .data_fetcher import DataFetcherFactory +from .ai_analyzer import AIAnalyzerFactory +from .config_manager import ConfigManager + +logger = logging.getLogger(__name__) + + +class AnalysisModuleType: + """分析模块类型常量""" + TRADING_VIEW_CHART = "trading_view_chart" + FINANCIAL_DATA = "financial_data" + BUSINESS_INFO = "business_info" + FUNDAMENTAL_ANALYSIS = "fundamental_analysis" + BULLISH_ANALYSIS = "bullish_analysis" + BEARISH_ANALYSIS = "bearish_analysis" + MARKET_ANALYSIS = "market_analysis" + NEWS_ANALYSIS = "news_analysis" + TRADING_ANALYSIS = "trading_analysis" + INSIDER_ANALYSIS = "insider_analysis" + FINAL_CONCLUSION = "final_conclusion" + + +class ReportGenerator: + """报告生成器""" + + def __init__(self, db_session: AsyncSession, config_manager: ConfigManager): + self.db = db_session + self.config_manager = config_manager + self.progress_tracker = ProgressTracker(db_session) + + # 定义分析模块配置 + self.analysis_modules = [ + { + "type": AnalysisModuleType.TRADING_VIEW_CHART, + "title": "TradingView图表", + "order": 1, + "step_name": "获取财务数据" + }, + { + "type": AnalysisModuleType.FINANCIAL_DATA, + "title": "财务数据分析", + "order": 2, + "step_name": "获取财务数据" + }, + { + "type": AnalysisModuleType.BUSINESS_INFO, + "title": "业务信息分析", + "order": 3, + "step_name": "生成业务信息" + }, + { + "type": AnalysisModuleType.FUNDAMENTAL_ANALYSIS, + "title": "基本面分析(景林模型)", + "order": 4, + "step_name": "执行基本面分析" + }, + { + "type": AnalysisModuleType.BULLISH_ANALYSIS, + "title": "看涨分析师观点", + "order": 5, + "step_name": "执行看涨分析" + }, + { + "type": AnalysisModuleType.BEARISH_ANALYSIS, + "title": "看跌分析师观点", + "order": 6, + "step_name": "执行看跌分析" + }, + { + "type": AnalysisModuleType.MARKET_ANALYSIS, + "title": "市场分析师观点", + "order": 7, + "step_name": "执行市场分析" + }, + { + "type": AnalysisModuleType.NEWS_ANALYSIS, + "title": "新闻分析师观点", + "order": 8, + "step_name": "执行新闻分析" + }, + { + "type": AnalysisModuleType.TRADING_ANALYSIS, + "title": "交易分析师观点", + "order": 9, + "step_name": "执行交易分析" + }, + { + "type": AnalysisModuleType.INSIDER_ANALYSIS, + "title": "内部人与机构动向分析", + "order": 10, + "step_name": "执行内部人分析" + }, + { + "type": AnalysisModuleType.FINAL_CONCLUSION, + "title": "最终结论", + "order": 11, + "step_name": "生成最终结论" + } + ] + + async def generate_report(self, symbol: str, market: str, force_regenerate: bool = False) -> ReportResponse: + """生成股票分析报告""" + try: + # 检查是否存在现有报告 + existing_report = await self._get_existing_report(symbol, market) + + if existing_report and not force_regenerate: + if existing_report.status == "completed": + logger.info(f"返回现有报告: {symbol} ({market})") + return await self._build_report_response(existing_report) + elif existing_report.status == "generating": + logger.info(f"报告正在生成中: {symbol} ({market})") + return await self._build_report_response(existing_report) + + # 创建新报告或重新生成 + if existing_report and force_regenerate: + report = existing_report + report.status = "generating" + report.updated_at = datetime.utcnow() + # 清理现有的分析模块 + await self._cleanup_existing_modules(report.id) + else: + report = await self._create_new_report(symbol, market) + + # 初始化进度追踪 + await self.progress_tracker.initialize_progress(report.id) + + # 异步生成报告内容 + asyncio.create_task(self._generate_report_content(report)) + + return await self._build_report_response(report) + + except Exception as e: + logger.error(f"报告生成失败: {symbol} ({market}) - {str(e)}") + if isinstance(e, (ReportGenerationError, DataSourceError, AIAnalysisError)): + raise + raise ReportGenerationError(f"报告生成失败: {str(e)}") + + async def get_report(self, symbol: str, market: str) -> Optional[ReportResponse]: + """获取现有报告""" + try: + report = await self._get_existing_report(symbol, market) + if report: + return await self._build_report_response(report) + return None + except Exception as e: + logger.error(f"获取报告失败: {symbol} ({market}) - {str(e)}") + raise ReportGenerationError(f"获取报告失败: {str(e)}") + + async def get_report_by_id(self, report_id: UUID) -> Optional[ReportResponse]: + """根据ID获取报告""" + try: + result = await self.db.execute( + select(Report).where(Report.id == report_id) + ) + report = result.scalar_one_or_none() + + if report: + return await self._build_report_response(report) + return None + except Exception as e: + logger.error(f"获取报告失败: {report_id} - {str(e)}") + raise ReportGenerationError(f"获取报告失败: {str(e)}") + + async def _get_existing_report(self, symbol: str, market: str) -> Optional[Report]: + """获取现有报告""" + result = await self.db.execute( + select(Report).where( + Report.symbol == symbol, + Report.market == market + ) + ) + return result.scalar_one_or_none() + + async def _create_new_report(self, symbol: str, market: str) -> Report: + """创建新报告""" + report = Report( + symbol=symbol, + market=market, + status="generating" + ) + self.db.add(report) + await self.db.flush() + return report + + async def _cleanup_existing_modules(self, report_id: UUID): + """清理现有的分析模块""" + result = await self.db.execute( + select(AnalysisModule).where(AnalysisModule.report_id == report_id) + ) + modules = result.scalars().all() + + for module in modules: + await self.db.delete(module) + + await self.db.flush() + + async def _generate_report_content(self, report: Report): + """生成报告内容(异步执行)""" + try: + logger.info(f"开始生成报告内容: {report.symbol} ({report.market})") + + # 开始初始化步骤 + await self.progress_tracker.start_step(report.id, "初始化报告") + + # 创建分析模块记录 + await self._create_analysis_modules(report) + + await self.progress_tracker.complete_step(report.id, "初始化报告", True) + + # 获取配置 + data_source_config = await self.config_manager.get_data_source_config(report.market) + gemini_config = await self.config_manager.get_gemini_config() + + # 创建数据获取器和AI分析器 + data_fetcher = DataFetcherFactory.create_fetcher( + data_source_config["type"], + data_source_config + ) + ai_analyzer = AIAnalyzerFactory.create_gemini_analyzer( + gemini_config["api_key"], + gemini_config + ) + + # 存储分析结果的上下文 + analysis_context = {} + + # 按顺序执行各个分析模块 + for module_config in self.analysis_modules: + try: + await self._execute_analysis_module( + report, module_config, data_fetcher, ai_analyzer, analysis_context + ) + except Exception as e: + logger.error(f"分析模块执行失败: {module_config['type']} - {str(e)}") + # 标记模块为失败,但继续执行其他模块 + await self._mark_module_failed(report.id, module_config["type"], str(e)) + + # 完成报告生成 + await self.progress_tracker.start_step(report.id, "保存报告") + + report.status = "completed" + report.updated_at = datetime.utcnow() + await self.db.commit() + + await self.progress_tracker.complete_step(report.id, "保存报告", True) + + logger.info(f"报告生成完成: {report.symbol} ({report.market})") + + except Exception as e: + logger.error(f"报告生成过程失败: {report.symbol} ({report.market}) - {str(e)}") + + # 标记报告为失败状态 + report.status = "failed" + report.updated_at = datetime.utcnow() + await self.db.commit() + + # 标记当前步骤为失败 + try: + progress = await self.progress_tracker.get_progress(report.id) + current_step = progress.current_step_name + await self.progress_tracker.complete_step(report.id, current_step, False, str(e)) + except Exception: + pass # 忽略进度更新失败 + + async def _create_analysis_modules(self, report: Report): + """创建分析模块记录""" + for module_config in self.analysis_modules: + module = AnalysisModule( + report_id=report.id, + module_type=module_config["type"], + module_order=module_config["order"], + title=module_config["title"], + status="pending" + ) + self.db.add(module) + + await self.db.flush() + + async def _execute_analysis_module( + self, + report: Report, + module_config: Dict[str, Any], + data_fetcher, + ai_analyzer, + analysis_context: Dict[str, Any] + ): + """执行单个分析模块""" + module_type = module_config["type"] + step_name = module_config["step_name"] + + logger.info(f"执行分析模块: {module_type}") + + # 开始步骤 + await self.progress_tracker.start_step(report.id, step_name) + + # 标记模块开始 + await self._mark_module_started(report.id, module_type) + + try: + # 根据模块类型执行相应的分析 + if module_type == AnalysisModuleType.FINANCIAL_DATA: + content = await self._execute_financial_data_module( + report.symbol, report.market, data_fetcher + ) + analysis_context["financial_data"] = content + + elif module_type == AnalysisModuleType.TRADING_VIEW_CHART: + content = await self._execute_trading_view_module( + report.symbol, report.market + ) + + elif module_type == AnalysisModuleType.BUSINESS_INFO: + content = await self._execute_business_info_module( + report.symbol, report.market, ai_analyzer, analysis_context + ) + analysis_context["business_info"] = content + + elif module_type == AnalysisModuleType.FUNDAMENTAL_ANALYSIS: + content = await self._execute_fundamental_analysis_module( + report.symbol, report.market, ai_analyzer, analysis_context + ) + analysis_context["fundamental_analysis"] = content + + elif module_type == AnalysisModuleType.BULLISH_ANALYSIS: + content = await self._execute_bullish_analysis_module( + report.symbol, report.market, ai_analyzer, analysis_context + ) + analysis_context["bullish_analysis"] = content + + elif module_type == AnalysisModuleType.BEARISH_ANALYSIS: + content = await self._execute_bearish_analysis_module( + report.symbol, report.market, ai_analyzer, analysis_context + ) + analysis_context["bearish_analysis"] = content + + elif module_type == AnalysisModuleType.MARKET_ANALYSIS: + content = await self._execute_market_analysis_module( + report.symbol, report.market, ai_analyzer, analysis_context + ) + analysis_context["market_analysis"] = content + + elif module_type == AnalysisModuleType.NEWS_ANALYSIS: + content = await self._execute_news_analysis_module( + report.symbol, report.market, ai_analyzer, analysis_context + ) + analysis_context["news_analysis"] = content + + elif module_type == AnalysisModuleType.TRADING_ANALYSIS: + content = await self._execute_trading_analysis_module( + report.symbol, report.market, ai_analyzer, analysis_context + ) + analysis_context["trading_analysis"] = content + + elif module_type == AnalysisModuleType.INSIDER_ANALYSIS: + content = await self._execute_insider_analysis_module( + report.symbol, report.market, ai_analyzer, analysis_context + ) + analysis_context["insider_analysis"] = content + + elif module_type == AnalysisModuleType.FINAL_CONCLUSION: + content = await self._execute_final_conclusion_module( + report.symbol, report.market, ai_analyzer, analysis_context + ) + analysis_context["final_conclusion"] = content + + else: + raise ReportGenerationError(f"未知的分析模块类型: {module_type}") + + # 保存模块内容 + await self._save_module_content(report.id, module_type, content) + + # 标记模块完成 + await self._mark_module_completed(report.id, module_type) + + # 完成步骤 + await self.progress_tracker.complete_step(report.id, step_name, True) + + except Exception as e: + logger.error(f"分析模块执行失败: {module_type} - {str(e)}") + + # 标记模块失败 + await self._mark_module_failed(report.id, module_type, str(e)) + + # 完成步骤(失败) + await self.progress_tracker.complete_step(report.id, step_name, False, str(e)) + + raise e + + async def _execute_financial_data_module(self, symbol: str, market: str, data_fetcher) -> Dict[str, Any]: + """执行财务数据分析模块""" + try: + # 获取财务数据 + financial_data = await data_fetcher.fetch_financial_data(symbol, market) + + # 获取市场数据 + market_data = await data_fetcher.fetch_market_data(symbol, market) + + return { + "financial_data": financial_data.dict(), + "market_data": market_data.dict(), + "summary": { + "data_source": financial_data.data_source, + "last_updated": financial_data.last_updated.isoformat(), + "data_quality": "good" # 可以添加数据质量评估逻辑 + } + } + except Exception as e: + raise DataSourceError(f"财务数据获取失败: {str(e)}") + + async def _execute_trading_view_module(self, symbol: str, market: str) -> Dict[str, Any]: + """执行TradingView图表模块""" + # 生成TradingView图表配置 + return { + "chart_config": { + "symbol": symbol, + "market": market, + "interval": "1D", + "theme": "light", + "style": "1", # 蜡烛图 + "toolbar_bg": "#f1f3f6", + "enable_publishing": False, + "withdateranges": True, + "hide_side_toolbar": False, + "allow_symbol_change": False, + "studies": [ + "MASimple@tv-basicstudies", # 移动平均线 + "Volume@tv-basicstudies" # 成交量 + ] + }, + "display_settings": { + "width": "100%", + "height": 500, + "autosize": True + } + } + + async def _execute_business_info_module(self, symbol: str, market: str, ai_analyzer, analysis_context: Dict[str, Any]) -> Dict[str, Any]: + """执行业务信息分析模块""" + try: + financial_data = analysis_context.get("financial_data", {}) + + result = await ai_analyzer.analyze_business_info(symbol, market, financial_data) + + return result.content + except Exception as e: + raise AIAnalysisError(f"业务信息分析失败: {str(e)}") + + async def _execute_fundamental_analysis_module(self, symbol: str, market: str, ai_analyzer, analysis_context: Dict[str, Any]) -> Dict[str, Any]: + """执行基本面分析模块""" + try: + financial_data = analysis_context.get("financial_data", {}) + business_info = analysis_context.get("business_info", {}) + + result = await ai_analyzer.analyze_fundamental(symbol, market, financial_data, business_info) + + return result.content + except Exception as e: + raise AIAnalysisError(f"基本面分析失败: {str(e)}") + + async def _execute_bullish_analysis_module(self, symbol: str, market: str, ai_analyzer, analysis_context: Dict[str, Any]) -> Dict[str, Any]: + """执行看涨分析模块""" + try: + result = await ai_analyzer.analyze_bullish_case(symbol, market, analysis_context) + return result.content + except Exception as e: + raise AIAnalysisError(f"看涨分析失败: {str(e)}") + + async def _execute_bearish_analysis_module(self, symbol: str, market: str, ai_analyzer, analysis_context: Dict[str, Any]) -> Dict[str, Any]: + """执行看跌分析模块""" + try: + result = await ai_analyzer.analyze_bearish_case(symbol, market, analysis_context) + return result.content + except Exception as e: + raise AIAnalysisError(f"看跌分析失败: {str(e)}") + + async def _execute_market_analysis_module(self, symbol: str, market: str, ai_analyzer, analysis_context: Dict[str, Any]) -> Dict[str, Any]: + """执行市场分析模块""" + try: + result = await ai_analyzer.analyze_market_sentiment(symbol, market, analysis_context) + return result.content + except Exception as e: + raise AIAnalysisError(f"市场分析失败: {str(e)}") + + async def _execute_news_analysis_module(self, symbol: str, market: str, ai_analyzer, analysis_context: Dict[str, Any]) -> Dict[str, Any]: + """执行新闻分析模块""" + try: + result = await ai_analyzer.analyze_news_catalysts(symbol, market, analysis_context) + return result.content + except Exception as e: + raise AIAnalysisError(f"新闻分析失败: {str(e)}") + + async def _execute_trading_analysis_module(self, symbol: str, market: str, ai_analyzer, analysis_context: Dict[str, Any]) -> Dict[str, Any]: + """执行交易分析模块""" + try: + result = await ai_analyzer.analyze_trading_dynamics(symbol, market, analysis_context) + return result.content + except Exception as e: + raise AIAnalysisError(f"交易分析失败: {str(e)}") + + async def _execute_insider_analysis_module(self, symbol: str, market: str, ai_analyzer, analysis_context: Dict[str, Any]) -> Dict[str, Any]: + """执行内部人分析模块""" + try: + result = await ai_analyzer.analyze_insider_institutional(symbol, market, analysis_context) + return result.content + except Exception as e: + raise AIAnalysisError(f"内部人分析失败: {str(e)}") + + async def _execute_final_conclusion_module(self, symbol: str, market: str, ai_analyzer, analysis_context: Dict[str, Any]) -> Dict[str, Any]: + """执行最终结论模块""" + try: + # 收集所有分析结果 + all_analyses = [] + for key, value in analysis_context.items(): + if key != "financial_data": # 排除原始财务数据 + all_analyses.append({ + "analysis_type": key, + "content": value + }) + + result = await ai_analyzer.generate_final_conclusion(symbol, market, all_analyses) + return result.content + except Exception as e: + raise AIAnalysisError(f"最终结论生成失败: {str(e)}") + + async def _mark_module_started(self, report_id: UUID, module_type: str): + """标记模块开始""" + result = await self.db.execute( + select(AnalysisModule).where( + AnalysisModule.report_id == report_id, + AnalysisModule.module_type == module_type + ) + ) + module = result.scalar_one_or_none() + + if module: + module.status = "running" + module.started_at = datetime.utcnow() + await self.db.flush() + + async def _mark_module_completed(self, report_id: UUID, module_type: str): + """标记模块完成""" + result = await self.db.execute( + select(AnalysisModule).where( + AnalysisModule.report_id == report_id, + AnalysisModule.module_type == module_type + ) + ) + module = result.scalar_one_or_none() + + if module: + module.status = "completed" + module.completed_at = datetime.utcnow() + await self.db.flush() + + async def _mark_module_failed(self, report_id: UUID, module_type: str, error_message: str): + """标记模块失败""" + result = await self.db.execute( + select(AnalysisModule).where( + AnalysisModule.report_id == report_id, + AnalysisModule.module_type == module_type + ) + ) + module = result.scalar_one_or_none() + + if module: + module.status = "failed" + module.completed_at = datetime.utcnow() + module.error_message = error_message + await self.db.flush() + + async def _save_module_content(self, report_id: UUID, module_type: str, content: Dict[str, Any]): + """保存模块内容""" + result = await self.db.execute( + select(AnalysisModule).where( + AnalysisModule.report_id == report_id, + AnalysisModule.module_type == module_type + ) + ) + module = result.scalar_one_or_none() + + if module: + module.content = content + await self.db.flush() + + async def _build_report_response(self, report: Report) -> ReportResponse: + """构建报告响应""" + # 获取分析模块 + result = await self.db.execute( + select(AnalysisModule) + .where(AnalysisModule.report_id == report.id) + .order_by(AnalysisModule.module_order) + ) + modules = result.scalars().all() + + # 转换为响应模式 + module_schemas = [ + AnalysisModuleSchema( + id=module.id, + module_type=module.module_type, + module_order=module.module_order, + title=module.title, + content=module.content, + status=module.status, + started_at=module.started_at, + completed_at=module.completed_at, + error_message=module.error_message + ) + for module in modules + ] + + return ReportResponse( + id=report.id, + symbol=report.symbol, + market=report.market, + status=report.status, + created_at=report.created_at, + updated_at=report.updated_at, + analysis_modules=module_schemas + ) \ No newline at end of file diff --git a/backend/check_db.py b/backend/check_db.py new file mode 100755 index 0000000..fc4435e --- /dev/null +++ b/backend/check_db.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +""" +数据库连接检查脚本 +用于验证数据库配置和连接状态 +""" + +import asyncio +import sys +import os + +# 添加项目根目录到Python路径 +sys.path.append(os.path.dirname(__file__)) + +from app.core.database import check_db_connection, close_db +from app.core.config import settings + + +async def main(): + """主函数:检查数据库连接""" + try: + print(f"正在检查数据库连接...") + print(f"数据库URL: {settings.DATABASE_URL}") + + # 检查数据库连接 + is_connected = await check_db_connection() + + if is_connected: + print("✅ 数据库连接正常!") + else: + print("❌ 数据库连接失败!") + print("请检查:") + print("1. PostgreSQL服务是否运行") + print("2. 数据库配置是否正确") + print("3. 网络连接是否正常") + sys.exit(1) + + except Exception as e: + print(f"❌ 数据库连接检查失败: {e}") + sys.exit(1) + finally: + await close_db() + + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/backend/init_db.py b/backend/init_db.py new file mode 100755 index 0000000..8e4e6af --- /dev/null +++ b/backend/init_db.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +""" +数据库初始化脚本 +用于创建数据库表和运行初始迁移 +""" + +import asyncio +import sys +import os + +# 添加项目根目录到Python路径 +sys.path.append(os.path.dirname(__file__)) + +from app.core.database import init_db, close_db +from app.core.config import settings + + +async def main(): + """主函数:初始化数据库""" + try: + print(f"正在连接数据库: {settings.DATABASE_URL}") + print("正在创建数据库表...") + + # 初始化数据库表 + await init_db() + + print("✅ 数据库表创建成功!") + print("💡 提示: 如果需要使用Alembic管理迁移,请运行:") + print(" alembic stamp head") + print(" alembic revision --autogenerate -m '描述'") + print(" alembic upgrade head") + + except Exception as e: + print(f"❌ 数据库初始化失败: {e}") + sys.exit(1) + finally: + await close_db() + + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000..3a7062d --- /dev/null +++ b/backend/main.py @@ -0,0 +1,101 @@ +""" +FastAPI应用入口点 +基本面选股系统后端服务 +""" + +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from contextlib import asynccontextmanager + +from app.core.config import settings +from app.core.database import engine, Base +from app.routers import reports, config, progress + + +@asynccontextmanager +async def lifespan(app: FastAPI): + """应用生命周期管理""" + # 启动时创建数据库表 + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + yield + # 关闭时清理资源 + await engine.dispose() + + +# 创建FastAPI应用实例 +app = FastAPI( + title="基本面选股系统", + description=""" + 提供股票基本面分析和报告生成的API服务 + + ## 功能特性 + + * **报告管理**: 创建、查询、更新和删除股票分析报告 + * **进度追踪**: 实时追踪报告生成进度 + * **配置管理**: 管理系统配置,包括数据库、API密钥等 + * **多市场支持**: 支持中国、香港、美国、日本股票市场 + + ## 支持的市场 + + * `china` - 中国A股市场 + * `hongkong` - 香港股票市场 + * `usa` - 美国股票市场 + * `japan` - 日本股票市场 + """, + version="1.0.0", + lifespan=lifespan, + docs_url="/docs", + redoc_url="/redoc", + openapi_url="/openapi.json" +) + +# 配置CORS中间件 +app.add_middleware( + CORSMiddleware, + allow_origins=settings.ALLOWED_ORIGINS, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# 注册路由 +app.include_router( + reports.router, + prefix="/api/reports", + tags=["reports"], + responses={ + 404: {"description": "报告不存在"}, + 500: {"description": "服务器内部错误"} + } +) +app.include_router( + config.router, + prefix="/api/config", + tags=["config"], + responses={ + 400: {"description": "配置参数错误"}, + 500: {"description": "服务器内部错误"} + } +) +app.include_router( + progress.router, + prefix="/api/progress", + tags=["progress"], + responses={ + 404: {"description": "进度记录不存在"}, + 500: {"description": "服务器内部错误"} + } +) + + +@app.get("/") +async def root(): + """根路径健康检查""" + return {"message": "基本面选股系统API服务正在运行", "version": "1.0.0"} + + +@app.get("/health") +async def health_check(): + """健康检查端点""" + return {"status": "healthy", "service": "fundamental-stock-analysis"} \ No newline at end of file diff --git a/backend/manage_db.py b/backend/manage_db.py new file mode 100755 index 0000000..7de0bf0 --- /dev/null +++ b/backend/manage_db.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +""" +数据库管理脚本 +提供数据库初始化、检查、迁移等功能 +""" + +import asyncio +import sys +import os +import argparse + +# 添加项目根目录到Python路径 +sys.path.append(os.path.dirname(__file__)) + +from app.core.database import init_db, close_db, check_db_connection +from app.core.config import settings + + +async def check_connection(): + """检查数据库连接""" + print(f"正在检查数据库连接...") + print(f"数据库URL: {settings.DATABASE_URL}") + + is_connected = await check_db_connection() + + if is_connected: + print("✅ 数据库连接正常!") + return True + else: + print("❌ 数据库连接失败!") + print("请检查:") + print("1. PostgreSQL服务是否运行") + print("2. 数据库配置是否正确") + print("3. 网络连接是否正常") + return False + + +async def initialize_database(): + """初始化数据库""" + print(f"正在连接数据库: {settings.DATABASE_URL}") + print("正在创建数据库表...") + + try: + # 初始化数据库表 + await init_db() + + print("✅ 数据库表创建成功!") + print("💡 提示: 如果需要使用Alembic管理迁移,请运行:") + print(" alembic stamp head") + print(" alembic revision --autogenerate -m '描述'") + print(" alembic upgrade head") + return True + + except Exception as e: + print(f"❌ 数据库初始化失败: {e}") + return False + + +async def show_status(): + """显示数据库状态""" + print("=== 数据库状态 ===") + print(f"数据库URL: {settings.DATABASE_URL}") + print(f"数据库Echo: {settings.DATABASE_ECHO}") + + # 检查连接 + is_connected = await check_db_connection() + print(f"连接状态: {'✅ 正常' if is_connected else '❌ 失败'}") + + if is_connected: + try: + from app.core.database import AsyncSessionLocal + async with AsyncSessionLocal() as session: + # 检查表是否存在 + result = await session.execute(""" + SELECT table_name + FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name IN ('reports', 'analysis_modules', 'progress_tracking', 'system_config') + ORDER BY table_name + """) + tables = [row[0] for row in result.fetchall()] + + print(f"已创建的表: {', '.join(tables) if tables else '无'}") + + if 'reports' in tables: + result = await session.execute("SELECT COUNT(*) FROM reports") + count = result.scalar() + print(f"报告数量: {count}") + + except Exception as e: + print(f"获取详细状态失败: {e}") + + +async def main(): + """主函数""" + parser = argparse.ArgumentParser(description='数据库管理工具') + parser.add_argument('command', choices=['check', 'init', 'status'], + help='要执行的命令') + + args = parser.parse_args() + + try: + if args.command == 'check': + success = await check_connection() + elif args.command == 'init': + success = await initialize_database() + elif args.command == 'status': + await show_status() + success = True + else: + print(f"未知命令: {args.command}") + success = False + + if not success: + sys.exit(1) + + except Exception as e: + print(f"❌ 执行失败: {e}") + sys.exit(1) + finally: + await close_db() + + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..698e029 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,40 @@ +# FastAPI和相关依赖 +fastapi==0.104.1 +uvicorn[standard]==0.24.0 +python-multipart==0.0.6 + +# 数据库相关 +sqlalchemy[asyncio]==2.0.23 +asyncpg==0.29.0 +alembic==1.12.1 + +# 数据验证和序列化 +pydantic==2.5.0 +pydantic-settings==2.1.0 + +# HTTP客户端 +httpx==0.25.2 +aiohttp==3.9.1 + +# AI服务 +google-generativeai==0.3.2 + +# 环境变量管理 +python-dotenv==1.0.0 + +# 日志和监控 +structlog==23.2.0 + +# 开发和测试依赖 +pytest==7.4.3 +pytest-asyncio==0.21.1 +pytest-cov==4.1.0 +black==23.11.0 +isort==5.12.0 +flake8==6.1.0 + +# 类型检查 +mypy==1.7.1 + +# 安全相关 +cryptography==41.0.7 \ No newline at end of file diff --git a/backend/test_external_apis.py b/backend/test_external_apis.py new file mode 100644 index 0000000..691a279 --- /dev/null +++ b/backend/test_external_apis.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +""" +外部API集成测试脚本 +用于验证Tushare和Gemini API集成是否正常工作 +""" + +import asyncio +import os +import sys +from pathlib import Path + +# 添加项目根目录到Python路径 +project_root = Path(__file__).parent +sys.path.insert(0, str(project_root)) + +from app.services.external_api_service import get_external_api_service +from app.core.config import settings + + +async def test_data_sources(): + """测试数据源""" + print("=== 测试数据源 ===") + + service = get_external_api_service() + + # 检查支持的数据源 + supported_sources = service.get_supported_data_sources() + print(f"支持的数据源: {supported_sources}") + + available_sources = service.get_available_data_sources() + print(f"可用的数据源: {available_sources}") + + # 测试数据源状态 + try: + status_response = await service.check_all_services_status() + print(f"整体状态: {status_response.overall_status}") + + for source_status in status_response.sources: + status_text = "✅ 可用" if source_status.is_available else "❌ 不可用" + print(f" {source_status.name}: {status_text}") + if source_status.response_time_ms: + print(f" 响应时间: {source_status.response_time_ms}ms") + if source_status.error_message: + print(f" 错误信息: {source_status.error_message}") + except Exception as e: + print(f"检查状态失败: {e}") + + +async def test_symbol_validation(): + """测试证券代码验证""" + print("\n=== 测试证券代码验证 ===") + + service = get_external_api_service() + + test_cases = [ + ("000001", "中国"), # 平安银行 + ("600036", "中国"), # 招商银行 + ("AAPL", "美国"), # 苹果 + ("INVALID", "中国") # 无效代码 + ] + + for symbol, market in test_cases: + try: + result = await service.validate_stock_symbol(symbol, market) + status_text = "✅ 有效" if result.is_valid else "❌ 无效" + print(f" {symbol} ({market}): {status_text}") + if result.company_name: + print(f" 公司名称: {result.company_name}") + if result.message: + print(f" 消息: {result.message}") + except Exception as e: + print(f" {symbol} ({market}): ❌ 验证失败 - {e}") + + +async def test_financial_data(): + """测试财务数据获取""" + print("\n=== 测试财务数据获取 ===") + + service = get_external_api_service() + + test_cases = [ + ("000001", "中国"), # 平安银行 + ("AAPL", "美国") # 苹果 + ] + + for symbol, market in test_cases: + try: + result = await service.get_financial_data(symbol, market) + print(f" {symbol} ({market}): ✅ 获取成功") + print(f" 数据源: {result.data_source}") + print(f" 更新时间: {result.last_updated}") + + # 显示部分财务数据 + if result.balance_sheet: + print(f" 资产负债表: {len(result.balance_sheet)} 项数据") + if result.income_statement: + print(f" 利润表: {len(result.income_statement)} 项数据") + + except Exception as e: + print(f" {symbol} ({market}): ❌ 获取失败 - {e}") + + +async def test_ai_analysis(): + """测试AI分析""" + print("\n=== 测试AI分析 ===") + + service = get_external_api_service() + + if not service.is_ai_service_available(): + print("❌ AI服务不可用,请检查Gemini API配置") + return + + # 模拟财务数据 + mock_financial_data = { + "balance_sheet": {"total_assets": 1000000, "total_liab": 600000}, + "income_statement": {"revenue": 500000, "n_income": 50000}, + "cash_flow": {"n_cashflow_act": 80000}, + "key_metrics": {"pe": 15.5, "pb": 1.2, "roe": 12.5} + } + + try: + result = await service.analyze_business_info("000001", "中国", mock_financial_data) + print(" 业务信息分析: ✅ 成功") + print(f" 使用模型: {result.model_used}") + print(f" 生成时间: {result.generated_at}") + + # 显示部分分析内容 + if result.content.get("company_overview"): + overview = result.content["company_overview"][:100] + "..." if len(result.content["company_overview"]) > 100 else result.content["company_overview"] + print(f" 公司概览: {overview}") + + except Exception as e: + print(f" 业务信息分析: ❌ 失败 - {e}") + + +async def main(): + """主测试函数""" + print("开始测试外部API集成...") + print(f"Tushare Token: {'已配置' if settings.TUSHARE_TOKEN else '未配置'}") + print(f"Gemini API Key: {'已配置' if settings.GEMINI_API_KEY else '未配置'}") + print() + + await test_data_sources() + await test_symbol_validation() + await test_financial_data() + await test_ai_analysis() + + print("\n测试完成!") + + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..3e980bc --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,86 @@ +# 基本面选股系统 - 前端 + +这是基本面选股系统的前端应用,使用 Next.js 14 和 TypeScript 构建。 + +## 技术栈 + +- **框架**: Next.js 14 (App Router) +- **语言**: TypeScript +- **样式**: Tailwind CSS +- **UI组件**: shadcn/ui +- **字体**: Noto Sans SC (中文支持) + +## 项目结构 + +``` +src/ +├── app/ # Next.js App Router 页面 +│ ├── layout.tsx # 根布局 +│ ├── page.tsx # 首页 +│ └── globals.css # 全局样式 +├── components/ # React 组件 +│ └── ui/ # shadcn/ui 基础组件 +├── lib/ # 工具库 +│ ├── api.ts # API 客户端 +│ ├── types.ts # TypeScript 类型定义 +│ └── utils.ts # 工具函数 +└── hooks/ # 自定义 React Hooks + ├── useReport.ts # 报告数据钩子 + └── useProgress.ts # 进度追踪钩子 +``` + +## 开发命令 + +```bash +# 安装依赖 +npm install + +# 启动开发服务器 +npm run dev + +# 构建生产版本 +npm run build + +# 启动生产服务器 +npm start + +# 代码检查 +npm run lint +``` + +## 功能特性 + +- ✅ Next.js 14 项目初始化 +- ✅ TypeScript 配置 +- ✅ Tailwind CSS 样式系统 +- ✅ shadcn/ui 组件库集成 +- ✅ 中文字体支持 (Noto Sans SC) +- ✅ 基础项目结构 +- ✅ API 客户端封装 +- ✅ 自定义 Hooks +- ✅ 响应式布局 + +## 环境变量 + +创建 `.env.local` 文件并配置以下变量: + +```env +NEXT_PUBLIC_API_URL=http://localhost:8000 +``` + +## 开发说明 + +1. 项目使用 App Router 架构 +2. 所有页面组件位于 `src/app/` 目录 +3. 可复用组件位于 `src/components/` 目录 +4. API 调用通过 `src/lib/api.ts` 统一管理 +5. 类型定义集中在 `src/lib/types.ts` +6. 自定义 Hooks 用于状态管理和数据获取 + +## 下一步开发 + +- 实现股票搜索表单组件 +- 创建报告页面和路由 +- 集成 TradingView 图表 +- 实现进度显示组件 +- 添加配置管理页面 \ No newline at end of file diff --git a/frontend/components.json b/frontend/components.json new file mode 100644 index 0000000..8c574b7 --- /dev/null +++ b/frontend/components.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "src/app/globals.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils" + } +} \ No newline at end of file diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs new file mode 100644 index 0000000..719cea2 --- /dev/null +++ b/frontend/eslint.config.mjs @@ -0,0 +1,25 @@ +import { dirname } from "path"; +import { fileURLToPath } from "url"; +import { FlatCompat } from "@eslint/eslintrc"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const compat = new FlatCompat({ + baseDirectory: __dirname, +}); + +const eslintConfig = [ + ...compat.extends("next/core-web-vitals", "next/typescript"), + { + ignores: [ + "node_modules/**", + ".next/**", + "out/**", + "build/**", + "next-env.d.ts", + ], + }, +]; + +export default eslintConfig; diff --git a/frontend/next.config.ts b/frontend/next.config.ts new file mode 100644 index 0000000..e9ffa30 --- /dev/null +++ b/frontend/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..feb2309 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,6190 @@ +{ + "name": "frontend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.1.0", + "dependencies": { + "@radix-ui/react-slot": "^1.2.3", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.546.0", + "next": "15.5.6", + "react": "19.1.0", + "react-dom": "19.1.0", + "tailwind-merge": "^3.3.1", + "tailwindcss-animate": "^1.0.7" + }, + "devDependencies": { + "@eslint/eslintrc": "^3", + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "^9", + "eslint-config-next": "15.5.6", + "tailwindcss": "^4", + "typescript": "^5" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@emnapi/core": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz", + "integrity": "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", + "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "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.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.1.tgz", + "integrity": "sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.16.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", + "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", + "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.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "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.0", + "minimatch": "^3.1.2", + "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/js": { + "version": "9.38.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz", + "integrity": "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==", + "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.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", + "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.16.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/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.4.tgz", + "integrity": "sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.3" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.4.tgz", + "integrity": "sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.3" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.3.tgz", + "integrity": "sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.3.tgz", + "integrity": "sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.3.tgz", + "integrity": "sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.3.tgz", + "integrity": "sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.3.tgz", + "integrity": "sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.3.tgz", + "integrity": "sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.3.tgz", + "integrity": "sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.3.tgz", + "integrity": "sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.3.tgz", + "integrity": "sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.4.tgz", + "integrity": "sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.3" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.4.tgz", + "integrity": "sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.3" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.4.tgz", + "integrity": "sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.3" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.4.tgz", + "integrity": "sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.3" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.4.tgz", + "integrity": "sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.3" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.4.tgz", + "integrity": "sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.3" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.4.tgz", + "integrity": "sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.3" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.4.tgz", + "integrity": "sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.5.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.4.tgz", + "integrity": "sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.4.tgz", + "integrity": "sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.4.tgz", + "integrity": "sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "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/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@next/env": { + "version": "15.5.6", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.6.tgz", + "integrity": "sha512-3qBGRW+sCGzgbpc5TS1a0p7eNxnOarGVQhZxfvTdnV0gFI61lX7QNtQ4V1TSREctXzYn5NetbUsLvyqwLFJM6Q==", + "license": "MIT" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "15.5.6", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.6.tgz", + "integrity": "sha512-YxDvsT2fwy1j5gMqk3ppXlsgDopHnkM4BoxSVASbvvgh5zgsK8lvWerDzPip8k3WVzsTZ1O7A7si1KNfN4OZfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "3.3.1" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.6.tgz", + "integrity": "sha512-ES3nRz7N+L5Umz4KoGfZ4XX6gwHplwPhioVRc25+QNsDa7RtUF/z8wJcbuQ2Tffm5RZwuN2A063eapoJ1u4nPg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.6.tgz", + "integrity": "sha512-JIGcytAyk9LQp2/nuVZPAtj8uaJ/zZhsKOASTjxDug0SPU9LAM3wy6nPU735M1OqacR4U20LHVF5v5Wnl9ptTA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.6.tgz", + "integrity": "sha512-qvz4SVKQ0P3/Im9zcS2RmfFL/UCQnsJKJwQSkissbngnB/12c6bZTCB0gHTexz1s6d/mD0+egPKXAIRFVS7hQg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.6.tgz", + "integrity": "sha512-FsbGVw3SJz1hZlvnWD+T6GFgV9/NYDeLTNQB2MXoPN5u9VA9OEDy6fJEfePfsUKAhJufFbZLgp0cPxMuV6SV0w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.6.tgz", + "integrity": "sha512-3QnHGFWlnvAgyxFxt2Ny8PTpXtQD7kVEeaFat5oPAHHI192WKYB+VIKZijtHLGdBBvc16tiAkPTDmQNOQ0dyrA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.6.tgz", + "integrity": "sha512-OsGX148sL+TqMK9YFaPFPoIaJKbFJJxFzkXZljIgA9hjMjdruKht6xDCEv1HLtlLNfkx3c5w2GLKhj7veBQizQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.6.tgz", + "integrity": "sha512-ONOMrqWxdzXDJNh2n60H6gGyKed42Ieu6UTVPZteXpuKbLZTH4G4eBMsr5qWgOBA+s7F+uB4OJbZnrkEDnZ5Fg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.6.tgz", + "integrity": "sha512-pxK4VIjFRx1MY92UycLOOw7dTdvccWsNETQ0kDHkBlcFH1GrTLUjSiHU1ohrznnux6TqRHgv5oflhfIWZwVROQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.14.0.tgz", + "integrity": "sha512-WJFej426qe4RWOm9MMtP4V3CV4AucXolQty+GRgAWLgQXmpCuwzs7hEpxxhSc/znXUSxum9d/P/32MW0FlAAlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.14.tgz", + "integrity": "sha512-hpz+8vFk3Ic2xssIA3e01R6jkmsAhvkQdXlEbRTk6S10xDAtiQiM3FyvZVGsucefq764euO/b8WUW9ysLdThHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.0", + "lightningcss": "1.30.1", + "magic-string": "^0.30.19", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.14" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.14.tgz", + "integrity": "sha512-23yx+VUbBwCg2x5XWdB8+1lkPajzLmALEfMb51zZUBYaYVPDQvBSD/WYDqiVyBIo2BZFa3yw1Rpy3G2Jp+K0dw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.5.1" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.14", + "@tailwindcss/oxide-darwin-arm64": "4.1.14", + "@tailwindcss/oxide-darwin-x64": "4.1.14", + "@tailwindcss/oxide-freebsd-x64": "4.1.14", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.14", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.14", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.14", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.14", + "@tailwindcss/oxide-linux-x64-musl": "4.1.14", + "@tailwindcss/oxide-wasm32-wasi": "4.1.14", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.14", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.14" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.14.tgz", + "integrity": "sha512-a94ifZrGwMvbdeAxWoSuGcIl6/DOP5cdxagid7xJv6bwFp3oebp7y2ImYsnZBMTwjn5Ev5xESvS3FFYUGgPODQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.14.tgz", + "integrity": "sha512-HkFP/CqfSh09xCnrPJA7jud7hij5ahKyWomrC3oiO2U9i0UjP17o9pJbxUN0IJ471GTQQmzwhp0DEcpbp4MZTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.14.tgz", + "integrity": "sha512-eVNaWmCgdLf5iv6Qd3s7JI5SEFBFRtfm6W0mphJYXgvnDEAZ5sZzqmI06bK6xo0IErDHdTA5/t7d4eTfWbWOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.14.tgz", + "integrity": "sha512-QWLoRXNikEuqtNb0dhQN6wsSVVjX6dmUFzuuiL09ZeXju25dsei2uIPl71y2Ic6QbNBsB4scwBoFnlBfabHkEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.14.tgz", + "integrity": "sha512-VB4gjQni9+F0VCASU+L8zSIyjrLLsy03sjcR3bM0V2g4SNamo0FakZFKyUQ96ZVwGK4CaJsc9zd/obQy74o0Fw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.14.tgz", + "integrity": "sha512-qaEy0dIZ6d9vyLnmeg24yzA8XuEAD9WjpM5nIM1sUgQ/Zv7cVkharPDQcmm/t/TvXoKo/0knI3me3AGfdx6w1w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.14.tgz", + "integrity": "sha512-ISZjT44s59O8xKsPEIesiIydMG/sCXoMBCqsphDm/WcbnuWLxxb+GcvSIIA5NjUw6F8Tex7s5/LM2yDy8RqYBQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.14.tgz", + "integrity": "sha512-02c6JhLPJj10L2caH4U0zF8Hji4dOeahmuMl23stk0MU1wfd1OraE7rOloidSF8W5JTHkFdVo/O7uRUJJnUAJg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.14.tgz", + "integrity": "sha512-TNGeLiN1XS66kQhxHG/7wMeQDOoL0S33x9BgmydbrWAb9Qw0KYdd8o1ifx4HOGDWhVmJ+Ul+JQ7lyknQFilO3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.14.tgz", + "integrity": "sha512-uZYAsaW/jS/IYkd6EWPJKW/NlPNSkWkBlaeVBi/WsFQNP05/bzkebUL8FH1pdsqx4f2fH/bWFcUABOM9nfiJkQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.5.0", + "@emnapi/runtime": "^1.5.0", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.0.5", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.14.tgz", + "integrity": "sha512-Az0RnnkcvRqsuoLH2Z4n3JfAef0wElgzHD5Aky/e+0tBUxUhIeIqFBTMNQvmMRSP15fWwmvjBxZ3Q8RhsDnxAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.14.tgz", + "integrity": "sha512-ttblVGHgf68kEE4om1n/n44I0yGPkCPbLsqzjvybhpwa6mKKtgFfAzy6btc3HRmuW7nHe0OOrSeNP9sQmmH9XA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.14.tgz", + "integrity": "sha512-BdMjIxy7HUNThK87C7BC8I1rE8BVUsfNQSI5siQ4JK3iIa3w0XyVvVL9SXLWO//CtYTcp1v7zci0fYwJOjB+Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.1.14", + "@tailwindcss/oxide": "4.1.14", + "postcss": "^8.4.41", + "tailwindcss": "4.1.14" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "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/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.22", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.22.tgz", + "integrity": "sha512-hRnu+5qggKDSyWHlnmThnUqg62l29Aj/6vcYgUaSFL9oc7DVjeWEQN3PRgdSc6F8d9QRMWkf36CLMch1Do/+RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.2", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", + "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.2", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz", + "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.1.tgz", + "integrity": "sha512-rUsLh8PXmBjdiPY+Emjz9NX2yHvhS11v0SR6xNJkm5GM1MO9ea/1GoDKlHHZGrOJclL/cZ2i/vRUYVtjRhrHVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.46.1", + "@typescript-eslint/type-utils": "8.46.1", + "@typescript-eslint/utils": "8.46.1", + "@typescript-eslint/visitor-keys": "8.46.1", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.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.46.1", + "eslint": "^8.57.0 || ^9.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.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.1.tgz", + "integrity": "sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.46.1", + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/typescript-estree": "8.46.1", + "@typescript-eslint/visitor-keys": "8.46.1", + "debug": "^4.3.4" + }, + "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", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.1.tgz", + "integrity": "sha512-FOIaFVMHzRskXr5J4Jp8lFVV0gz5ngv3RHmn+E4HYxSJ3DgDzU7fVI1/M7Ijh1zf6S7HIoaIOtln1H5y8V+9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.46.1", + "@typescript-eslint/types": "^8.46.1", + "debug": "^4.3.4" + }, + "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.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.1.tgz", + "integrity": "sha512-weL9Gg3/5F0pVQKiF8eOXFZp8emqWzZsOJuWRUNtHT+UNV2xSJegmpCNQHy37aEQIbToTq7RHKhWvOsmbM680A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/visitor-keys": "8.46.1" + }, + "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.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.1.tgz", + "integrity": "sha512-X88+J/CwFvlJB+mK09VFqx5FE4H5cXD+H/Bdza2aEWkSb8hnWIQorNcscRl4IEo1Cz9VI/+/r/jnGWkbWPx54g==", + "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.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.1.tgz", + "integrity": "sha512-+BlmiHIiqufBxkVnOtFwjah/vrkF4MtKKvpXrKSPLCkCtAp8H01/VV43sfqA98Od7nJpDcFnkwgyfQbOG0AMvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/typescript-estree": "8.46.1", + "@typescript-eslint/utils": "8.46.1", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.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", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.1.tgz", + "integrity": "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ==", + "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.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.1.tgz", + "integrity": "sha512-uIifjT4s8cQKFQ8ZBXXyoUODtRoAd7F7+G8MKmtzj17+1UbdzFl52AzRyZRyKqPHhgzvXunnSckVu36flGy8cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.46.1", + "@typescript-eslint/tsconfig-utils": "8.46.1", + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/visitor-keys": "8.46.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.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/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.1.tgz", + "integrity": "sha512-vkYUy6LdZS7q1v/Gxb2Zs7zziuXN0wxqsetJdeZdRe/f5dwJFglmuvZBfTUivCtjH725C1jWCDfpadadD95EDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.46.1", + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/typescript-estree": "8.46.1" + }, + "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", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.1.tgz", + "integrity": "sha512-ptkmIf2iDkNUjdeu2bQqhFPV1m6qTnFFjg7PPDjxKWaMaP0Z6I9l30Jr3g5QqbZGdw8YdYvLp+XnqnWWZOg/NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.1", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "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/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "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/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/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.0.tgz", + "integrity": "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "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/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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.30001751", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz", + "integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==", + "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/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "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/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-abstract": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.6", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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.38.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz", + "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.1", + "@eslint/core": "^0.16.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.38.0", + "@eslint/plugin-kit": "^0.4.0", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "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.2", + "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-config-next": { + "version": "15.5.6", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.5.6.tgz", + "integrity": "sha512-cGr3VQlPsZBEv8rtYp4BpG1KNXDqGvPo9VC1iaCgIA11OfziC/vczng+TnAS3WpRIR3Q5ye/6yl+CRUuZ1fPGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@next/eslint-plugin-next": "15.5.6", + "@rushstack/eslint-patch": "^1.10.3", + "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-react": "^7.37.0", + "eslint-plugin-react-hooks": "^5.0.0" + }, + "peerDependencies": { + "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", + "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.4.0", + "get-tsconfig": "^4.10.0", + "is-bun-module": "^2.0.0", + "stable-hash": "^0.0.5", + "tinyglobby": "^0.2.13", + "unrs-resolver": "^1.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/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/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "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/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-react/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/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.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "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/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "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/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.12.0.tgz", + "integrity": "sha512-LScr2aNr2FbjAjZh2C6X6BxRx1/x+aTDExct/xyq2XKbYOiG5c0aK7pMsSuyc0brz3ibr/lbQiHD9jzt4lccJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "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/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "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/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "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/lightningcss": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", + "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "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.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==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lucide-react": { + "version": "0.546.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.546.0.tgz", + "integrity": "sha512-Z94u6fKT43lKeYHiVyvyR8fT7pwCzDu7RyMPpTvh054+xahSgj4HFQ+NmflvzdXsoAjYGdCguGaFKYuvq0ThCQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.19", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", + "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "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==", + "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/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "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/next": { + "version": "15.5.6", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.6.tgz", + "integrity": "sha512-zTxsnI3LQo3c9HSdSf91O1jMNsEzIXDShXd4wVdg9y5shwLqBXi4ZtUUJyB86KGVSJLZx0PFONvO54aheGX8QQ==", + "license": "MIT", + "dependencies": { + "@next/env": "15.5.6", + "@swc/helpers": "0.5.15", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.5.6", + "@next/swc-darwin-x64": "15.5.6", + "@next/swc-linux-arm64-gnu": "15.5.6", + "@next/swc-linux-arm64-musl": "15.5.6", + "@next/swc-linux-x64-gnu": "15.5.6", + "@next/swc-linux-x64-musl": "15.5.6", + "@next/swc-win32-arm64-msvc": "15.5.6", + "@next/swc-win32-x64-msvc": "15.5.6", + "sharp": "^0.34.3" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "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.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "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/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.0" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "devOptional": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/sharp": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.4.tgz", + "integrity": "sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.0", + "semver": "^7.7.2" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.4", + "@img/sharp-darwin-x64": "0.34.4", + "@img/sharp-libvips-darwin-arm64": "1.2.3", + "@img/sharp-libvips-darwin-x64": "1.2.3", + "@img/sharp-libvips-linux-arm": "1.2.3", + "@img/sharp-libvips-linux-arm64": "1.2.3", + "@img/sharp-libvips-linux-ppc64": "1.2.3", + "@img/sharp-libvips-linux-s390x": "1.2.3", + "@img/sharp-libvips-linux-x64": "1.2.3", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.3", + "@img/sharp-libvips-linuxmusl-x64": "1.2.3", + "@img/sharp-linux-arm": "0.34.4", + "@img/sharp-linux-arm64": "0.34.4", + "@img/sharp-linux-ppc64": "0.34.4", + "@img/sharp-linux-s390x": "0.34.4", + "@img/sharp-linux-x64": "0.34.4", + "@img/sharp-linuxmusl-arm64": "0.34.4", + "@img/sharp-linuxmusl-x64": "0.34.4", + "@img/sharp-wasm32": "0.34.4", + "@img/sharp-win32-arm64": "0.34.4", + "@img/sharp-win32-ia32": "0.34.4", + "@img/sharp-win32-x64": "0.34.4" + } + }, + "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/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stable-hash": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", + "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "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/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwind-merge": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", + "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.14.tgz", + "integrity": "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==", + "license": "MIT" + }, + "node_modules/tailwindcss-animate": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.1.tgz", + "integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "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/tinyglobby/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/tinyglobby/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/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "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/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "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/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/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..a327700 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,33 @@ +{ + "name": "frontend", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev --turbopack", + "build": "next build --turbopack", + "start": "next start", + "lint": "eslint" + }, + "dependencies": { + "@radix-ui/react-slot": "^1.2.3", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.546.0", + "next": "15.5.6", + "react": "19.1.0", + "react-dom": "19.1.0", + "tailwind-merge": "^3.3.1", + "tailwindcss-animate": "^1.0.7" + }, + "devDependencies": { + "@eslint/eslintrc": "^3", + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "^9", + "eslint-config-next": "15.5.6", + "tailwindcss": "^4", + "typescript": "^5" + } +} diff --git a/frontend/postcss.config.mjs b/frontend/postcss.config.mjs new file mode 100644 index 0000000..c7bcb4b --- /dev/null +++ b/frontend/postcss.config.mjs @@ -0,0 +1,5 @@ +const config = { + plugins: ["@tailwindcss/postcss"], +}; + +export default config; diff --git a/frontend/src/app/favicon.ico b/frontend/src/app/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/frontend/src/app/favicon.ico differ diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css new file mode 100644 index 0000000..c74ca25 --- /dev/null +++ b/frontend/src/app/globals.css @@ -0,0 +1,54 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; + --secondary: 210 40% 96%; + --secondary-foreground: 222.2 47.4% 11.2%; + --muted: 210 40% 96%; + --muted-foreground: 215.4 16.3% 46.9%; + --accent: 210 40% 96%; + --accent-foreground: 222.2 47.4% 11.2%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 222.2 84% 4.9%; + --radius: 0.5rem; +} + +.dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 11.2%; + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 212.7 26.8% 83.9%; +} + +body { + font-family: "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif; + background-color: hsl(var(--background)); + color: hsl(var(--foreground)); +} diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx new file mode 100644 index 0000000..403e1a8 --- /dev/null +++ b/frontend/src/app/layout.tsx @@ -0,0 +1,32 @@ +import type { Metadata } from "next"; +import { Noto_Sans_SC } from "next/font/google"; +import "./globals.css"; + +const notoSansSC = Noto_Sans_SC({ + subsets: ["latin"], + weight: ["300", "400", "500", "700"], + variable: "--font-noto-sans-sc", +}); + +export const metadata: Metadata = { + title: "基本面选股系统", + description: "专业的股票基本面分析平台,提供全面的投资决策支持", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + +
+
+ {children} +
+
+ + + ); +} diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx new file mode 100644 index 0000000..d8521f1 --- /dev/null +++ b/frontend/src/app/page.tsx @@ -0,0 +1,17 @@ +export default function Home() { + return ( +
+
+

+ 基本面选股系统 +

+

+ 专业的股票基本面分析平台,通过多维度分析为您提供全面的投资决策支持 +

+
+ 前端项目已成功初始化,准备开始开发核心功能 +
+
+
+ ); +} diff --git a/frontend/src/components/.gitkeep b/frontend/src/components/.gitkeep new file mode 100644 index 0000000..e2fc588 --- /dev/null +++ b/frontend/src/components/.gitkeep @@ -0,0 +1 @@ +# This file ensures the components directory is tracked by git \ No newline at end of file diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx new file mode 100644 index 0000000..c9c71de --- /dev/null +++ b/frontend/src/components/ui/button.tsx @@ -0,0 +1,56 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: + "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } \ No newline at end of file diff --git a/frontend/src/components/ui/card.tsx b/frontend/src/components/ui/card.tsx new file mode 100644 index 0000000..938aa22 --- /dev/null +++ b/frontend/src/components/ui/card.tsx @@ -0,0 +1,79 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } \ No newline at end of file diff --git a/frontend/src/hooks/.gitkeep b/frontend/src/hooks/.gitkeep new file mode 100644 index 0000000..372784e --- /dev/null +++ b/frontend/src/hooks/.gitkeep @@ -0,0 +1 @@ +# This file ensures the hooks directory is tracked by git \ No newline at end of file diff --git a/frontend/src/hooks/useProgress.ts b/frontend/src/hooks/useProgress.ts new file mode 100644 index 0000000..9bc9ca2 --- /dev/null +++ b/frontend/src/hooks/useProgress.ts @@ -0,0 +1,68 @@ +import { useState, useEffect, useRef, useCallback } from "react"; +import { ProgressResponse } from "@/lib/types"; +import { apiClient } from "@/lib/api"; + +export function useProgress(reportId?: string, pollingInterval: number = 2000) { + const [progress, setProgress] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const intervalRef = useRef(null); + + const stopPolling = useCallback(() => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + setLoading(false); + }, []); + + const fetchProgress = useCallback(async (id: string) => { + try { + const progressData = await apiClient.getReportProgress(id); + setProgress(progressData); + + // 如果报告已完成或失败,停止轮询 + if (progressData.status === "completed" || progressData.status === "failed") { + stopPolling(); + } + } catch (err) { + setError(err instanceof Error ? err.message : "获取进度失败"); + stopPolling(); + } + }, [stopPolling]); + + const startPolling = useCallback((id: string) => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + + setLoading(true); + setError(null); + + // 立即获取一次进度 + fetchProgress(id); + + // 开始轮询 + intervalRef.current = setInterval(() => { + fetchProgress(id); + }, pollingInterval); + }, [fetchProgress, pollingInterval]); + + useEffect(() => { + if (reportId) { + startPolling(reportId); + } + + return () => { + stopPolling(); + }; + }, [reportId, startPolling, stopPolling]); + + return { + progress, + loading, + error, + startPolling, + stopPolling, + }; +} \ No newline at end of file diff --git a/frontend/src/hooks/useReport.ts b/frontend/src/hooks/useReport.ts new file mode 100644 index 0000000..d73c9da --- /dev/null +++ b/frontend/src/hooks/useReport.ts @@ -0,0 +1,49 @@ +import { useState, useEffect } from "react"; +import { Report, TradingMarket } from "@/lib/types"; +import { apiClient } from "@/lib/api"; + +export function useReport(symbol?: string, market?: TradingMarket) { + const [report, setReport] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const fetchReport = async (sym: string, mkt: TradingMarket) => { + setLoading(true); + setError(null); + try { + const reportData = await apiClient.getReport(sym, mkt); + setReport(reportData); + } catch (err) { + setError(err instanceof Error ? err.message : "获取报告失败"); + } finally { + setLoading(false); + } + }; + + const regenerateReport = async (sym: string, mkt: TradingMarket, force: boolean = false) => { + setLoading(true); + setError(null); + try { + const reportData = await apiClient.regenerateReport(sym, mkt, force); + setReport(reportData); + } catch (err) { + setError(err instanceof Error ? err.message : "重新生成报告失败"); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + if (symbol && market) { + fetchReport(symbol, market); + } + }, [symbol, market]); + + return { + report, + loading, + error, + fetchReport, + regenerateReport, + }; +} \ No newline at end of file diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts new file mode 100644 index 0000000..02a95db --- /dev/null +++ b/frontend/tailwind.config.ts @@ -0,0 +1,79 @@ +import type { Config } from "tailwindcss" + +const config: Config = { + darkMode: "class", + content: [ + "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", + "./src/components/**/*.{js,ts,jsx,tsx,mdx}", + "./src/app/**/*.{js,ts,jsx,tsx,mdx}", + ], + prefix: "", + theme: { + container: { + center: true, + padding: "2rem", + screens: { + "2xl": "1400px", + }, + }, + extend: { + colors: { + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + destructive: { + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", + }, + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + keyframes: { + "accordion-down": { + from: { height: "0" }, + to: { height: "var(--radix-accordion-content-height)" }, + }, + "accordion-up": { + from: { height: "var(--radix-accordion-content-height)" }, + to: { height: "0" }, + }, + }, + animation: { + "accordion-down": "accordion-down 0.2s ease-out", + "accordion-up": "accordion-up 0.2s ease-out", + }, + }, + }, + plugins: [require("tailwindcss-animate")], +} + +export default config \ No newline at end of file diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..c133409 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +}