From a8927a18e6e6379ab86c84f2b3ba3f2242f5a287 Mon Sep 17 00:00:00 2001 From: chiguyong Date: Thu, 23 Apr 2026 14:46:43 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20GEO=E5=B9=B3=E5=8F=B0MVP=E5=AE=8C?= =?UTF-8?q?=E6=95=B4=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端: FastAPI + SQLAlchemy 2.0 + PostgreSQL + Redis - 用户认证(注册/登录/JWT) - 查询词CRUD管理 - 引用数据统计与查询 - CSV报告导出 - Citation Engine引用检测引擎(Playwright) - APScheduler定时调度器 - 前端: Next.js 14 + TailwindCSS + shadcn/ui + Recharts - 登录/注册页面(NextAuth.js) - Dashboard数据总览(统计卡片+趋势图+平台对比图) - 查询词管理(完整CRUD+立即查询) - 引用记录查看与筛选 - 报告导出(CSV) - 设置页面(用户信息+套餐对比) - 基础设施: Docker Compose(PostgreSQL/Redis/FastAPI/Next.js) - 测试: 39个pytest单元/集成测试全部通过 --- .env.example | 19 + .gitignore | 43 + .../zh/content/AI平台集成/AI平台集成.md | 447 + .../zh/content/AI平台集成/Kimi平台集成.md | 438 + .../content/AI平台集成/平台适配器扩展指南.md | 408 + .../zh/content/AI平台集成/引用检测算法.md | 477 ++ .../zh/content/AI平台集成/文心平台集成.md | 387 + .../zh/content/AI平台集成/适配器架构设计.md | 510 ++ .../zh/content/API接口文档/API接口文档.md | 407 + .../zh/content/API接口文档/引用数据接口.md | 590 ++ .../zh/content/API接口文档/报告导出接口.md | 366 + .../zh/content/API接口文档/查询管理接口.md | 546 ++ .../zh/content/API接口文档/认证接口.md | 413 + .../zh/content/任务调度系统/任务调度系统.md | 417 + .../zh/content/任务调度系统/性能优化.md | 378 + .../zh/content/任务调度系统/查询执行流程.md | 496 ++ .../zh/content/任务调度系统/调度器设计.md | 343 + .../zh/content/前端系统架构/API客户端.md | 369 + .../content/前端系统架构/Next.js应用配置.md | 416 + .../zh/content/前端系统架构/UI组件库.md | 547 ++ .../zh/content/前端系统架构/前端系统架构.md | 349 + .../zh/content/前端系统架构/数据可视化.md | 323 + .../content/前端系统架构/认证系统前端实现.md | 409 + .../zh/content/前端系统架构/页面组件设计.md | 411 + .../zh/content/后端系统架构/API接口设计.md | 455 + .../zh/content/后端系统架构/后端系统架构.md | 431 + .../zh/content/后端系统架构/工作器系统.md | 460 + .../zh/content/后端系统架构/引用检测引擎.md | 568 ++ .../zh/content/后端系统架构/数据模型设计.md | 462 + .../zh/content/后端系统架构/核心框架配置.md | 388 + .../zh/content/后端系统架构/认证系统.md | 505 ++ .../repowiki/zh/content/开发指南/代码规范.md | 329 + .../repowiki/zh/content/开发指南/开发工具.md | 360 + .../repowiki/zh/content/开发指南/开发指南.md | 413 + .../repowiki/zh/content/开发指南/开发流程.md | 283 + .../zh/content/开发指南/新功能开发.md | 359 + .qoder/repowiki/zh/content/快速开始.md | 454 + .../zh/content/扩展与定制/功能扩展.md | 308 + .../zh/content/扩展与定制/扩展与定制.md | 481 ++ .../zh/content/扩展与定制/第三方集成.md | 510 ++ .../zh/content/扩展与定制/配置定制.md | 329 + .qoder/repowiki/zh/content/故障排除与FAQ.md | 480 ++ .../zh/content/数据库设计/数据库架构.md | 436 + .../zh/content/数据库设计/数据库设计.md | 521 ++ .../zh/content/数据库设计/数据库迁移.md | 319 + .../zh/content/数据库设计/数据模型.md | 419 + .../zh/content/数据库设计/表结构设计.md | 501 ++ .../repowiki/zh/content/测试策略/单元测试.md | 497 ++ .../zh/content/测试策略/测试最佳实践.md | 384 + .../repowiki/zh/content/测试策略/测试策略.md | 425 + .../repowiki/zh/content/测试策略/集成测试.md | 365 + .../zh/content/部署与运维/Docker容器化部署.md | 685 ++ .../zh/content/部署与运维/生产环境部署.md | 347 + .../zh/content/部署与运维/监控与日志管理.md | 276 + .../zh/content/部署与运维/运维最佳实践.md | 375 + .../zh/content/部署与运维/部署与运维.md | 418 + .../repowiki/zh/content/项目概述/快速开始.md | 390 + .qoder/repowiki/zh/content/项目概述/技术栈.md | 348 + .../repowiki/zh/content/项目概述/核心功能.md | 455 + .../repowiki/zh/content/项目概述/系统架构.md | 366 + .../repowiki/zh/content/项目概述/项目介绍.md | 348 + .../repowiki/zh/content/项目概述/项目概述.md | 408 + .../repowiki/zh/meta/repowiki-metadata.json | 1 + backend/Dockerfile | 40 + backend/alembic.ini | 149 + backend/alembic/README | 1 + backend/alembic/env.py | 88 + backend/alembic/script.py.mako | 28 + .../488d0bd5ab01_initial_migration.py | 127 + backend/app/__init__.py | 0 backend/app/api/__init__.py | 3 + backend/app/api/auth.py | 42 + backend/app/api/citations.py | 77 + backend/app/api/deps.py | 42 + backend/app/api/queries.py | 85 + backend/app/api/reports.py | 46 + backend/app/config.py | 22 + backend/app/database.py | 28 + backend/app/main.py | 56 + backend/app/models/__init__.py | 13 + backend/app/models/citation_record.py | 41 + backend/app/models/query.py | 54 + backend/app/models/query_task.py | 38 + backend/app/models/subscription.py | 36 + backend/app/models/user.py | 40 + backend/app/schemas/__init__.py | 0 backend/app/schemas/auth.py | 33 + backend/app/schemas/citation.py | 49 + backend/app/schemas/query.py | 93 + backend/app/services/__init__.py | 0 backend/app/services/auth.py | 68 + backend/app/services/citation.py | 277 + backend/app/services/query.py | 122 + backend/app/utils/__init__.py | 0 backend/app/workers/__init__.py | 12 + backend/app/workers/citation_engine.py | 308 + backend/app/workers/platforms/__init__.py | 0 backend/app/workers/platforms/base.py | 17 + backend/app/workers/platforms/kimi.py | 205 + backend/app/workers/platforms/wenxin.py | 204 + backend/app/workers/scheduler.py | 94 + backend/requirements.txt | 35 + docker-compose.yml | 70 + ...台 MVP 开发指导文档(Trae_Qoder 专用).pdf | Bin 0 -> 561745 bytes frontend/.eslintrc.json | 3 + frontend/.gitignore | 36 + frontend/Dockerfile | 14 + frontend/README.md | 36 + frontend/app/(auth)/layout.tsx | 11 + frontend/app/(auth)/login/page.tsx | 92 + frontend/app/(auth)/register/page.tsx | 127 + .../(dashboard)/dashboard/citations/page.tsx | 293 + frontend/app/(dashboard)/dashboard/page.tsx | 226 + .../(dashboard)/dashboard/queries/page.tsx | 525 ++ .../(dashboard)/dashboard/reports/page.tsx | 199 + .../(dashboard)/dashboard/settings/page.tsx | 171 + frontend/app/(dashboard)/layout.tsx | 26 + frontend/app/api/auth/[...nextauth]/route.ts | 6 + frontend/app/favicon.ico | Bin 0 -> 25931 bytes frontend/app/fonts/GeistMonoVF.woff | Bin 0 -> 67864 bytes frontend/app/fonts/GeistVF.woff | Bin 0 -> 66268 bytes frontend/app/globals.css | 63 + frontend/app/layout.tsx | 36 + frontend/app/page.tsx | 6 + frontend/components/charts/platform-chart.tsx | 67 + frontend/components/charts/trend-chart.tsx | 59 + frontend/components/layout/header.tsx | 29 + frontend/components/layout/sidebar.tsx | 55 + frontend/components/providers.tsx | 8 + frontend/components/ui/badge.tsx | 36 + frontend/components/ui/button.tsx | 56 + frontend/components/ui/card.tsx | 79 + frontend/components/ui/dialog.tsx | 122 + frontend/components/ui/dropdown-menu.tsx | 200 + frontend/components/ui/input.tsx | 22 + frontend/components/ui/label.tsx | 26 + frontend/components/ui/select.tsx | 160 + frontend/components/ui/skeleton.tsx | 15 + frontend/components/ui/table.tsx | 117 + frontend/components/ui/tabs.tsx | 55 + frontend/lib/api.ts | 78 + frontend/lib/auth.ts | 55 + frontend/lib/platforms.ts | 17 + frontend/lib/utils.ts | 6 + frontend/next.config.mjs | 4 + frontend/package-lock.json | 7500 +++++++++++++++++ frontend/package.json | 39 + frontend/postcss.config.mjs | 8 + frontend/tailwind.config.ts | 56 + frontend/tsconfig.json | 26 + frontend/types/next-auth.d.ts | 25 + tests/__init__.py | 0 tests/conftest.py | 122 + tests/test_auth.py | 103 + tests/test_business_flow.py | 440 + tests/test_citation_engine.py | 126 + tests/test_citations.py | 92 + tests/test_queries.py | 153 + tests/test_scheduler.py | 122 + 159 files changed, 39959 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 .qoder/repowiki/zh/content/AI平台集成/AI平台集成.md create mode 100644 .qoder/repowiki/zh/content/AI平台集成/Kimi平台集成.md create mode 100644 .qoder/repowiki/zh/content/AI平台集成/平台适配器扩展指南.md create mode 100644 .qoder/repowiki/zh/content/AI平台集成/引用检测算法.md create mode 100644 .qoder/repowiki/zh/content/AI平台集成/文心平台集成.md create mode 100644 .qoder/repowiki/zh/content/AI平台集成/适配器架构设计.md create mode 100644 .qoder/repowiki/zh/content/API接口文档/API接口文档.md create mode 100644 .qoder/repowiki/zh/content/API接口文档/引用数据接口.md create mode 100644 .qoder/repowiki/zh/content/API接口文档/报告导出接口.md create mode 100644 .qoder/repowiki/zh/content/API接口文档/查询管理接口.md create mode 100644 .qoder/repowiki/zh/content/API接口文档/认证接口.md create mode 100644 .qoder/repowiki/zh/content/任务调度系统/任务调度系统.md create mode 100644 .qoder/repowiki/zh/content/任务调度系统/性能优化.md create mode 100644 .qoder/repowiki/zh/content/任务调度系统/查询执行流程.md create mode 100644 .qoder/repowiki/zh/content/任务调度系统/调度器设计.md create mode 100644 .qoder/repowiki/zh/content/前端系统架构/API客户端.md create mode 100644 .qoder/repowiki/zh/content/前端系统架构/Next.js应用配置.md create mode 100644 .qoder/repowiki/zh/content/前端系统架构/UI组件库.md create mode 100644 .qoder/repowiki/zh/content/前端系统架构/前端系统架构.md create mode 100644 .qoder/repowiki/zh/content/前端系统架构/数据可视化.md create mode 100644 .qoder/repowiki/zh/content/前端系统架构/认证系统前端实现.md create mode 100644 .qoder/repowiki/zh/content/前端系统架构/页面组件设计.md create mode 100644 .qoder/repowiki/zh/content/后端系统架构/API接口设计.md create mode 100644 .qoder/repowiki/zh/content/后端系统架构/后端系统架构.md create mode 100644 .qoder/repowiki/zh/content/后端系统架构/工作器系统.md create mode 100644 .qoder/repowiki/zh/content/后端系统架构/引用检测引擎.md create mode 100644 .qoder/repowiki/zh/content/后端系统架构/数据模型设计.md create mode 100644 .qoder/repowiki/zh/content/后端系统架构/核心框架配置.md create mode 100644 .qoder/repowiki/zh/content/后端系统架构/认证系统.md create mode 100644 .qoder/repowiki/zh/content/开发指南/代码规范.md create mode 100644 .qoder/repowiki/zh/content/开发指南/开发工具.md create mode 100644 .qoder/repowiki/zh/content/开发指南/开发指南.md create mode 100644 .qoder/repowiki/zh/content/开发指南/开发流程.md create mode 100644 .qoder/repowiki/zh/content/开发指南/新功能开发.md create mode 100644 .qoder/repowiki/zh/content/快速开始.md create mode 100644 .qoder/repowiki/zh/content/扩展与定制/功能扩展.md create mode 100644 .qoder/repowiki/zh/content/扩展与定制/扩展与定制.md create mode 100644 .qoder/repowiki/zh/content/扩展与定制/第三方集成.md create mode 100644 .qoder/repowiki/zh/content/扩展与定制/配置定制.md create mode 100644 .qoder/repowiki/zh/content/故障排除与FAQ.md create mode 100644 .qoder/repowiki/zh/content/数据库设计/数据库架构.md create mode 100644 .qoder/repowiki/zh/content/数据库设计/数据库设计.md create mode 100644 .qoder/repowiki/zh/content/数据库设计/数据库迁移.md create mode 100644 .qoder/repowiki/zh/content/数据库设计/数据模型.md create mode 100644 .qoder/repowiki/zh/content/数据库设计/表结构设计.md create mode 100644 .qoder/repowiki/zh/content/测试策略/单元测试.md create mode 100644 .qoder/repowiki/zh/content/测试策略/测试最佳实践.md create mode 100644 .qoder/repowiki/zh/content/测试策略/测试策略.md create mode 100644 .qoder/repowiki/zh/content/测试策略/集成测试.md create mode 100644 .qoder/repowiki/zh/content/部署与运维/Docker容器化部署.md create mode 100644 .qoder/repowiki/zh/content/部署与运维/生产环境部署.md create mode 100644 .qoder/repowiki/zh/content/部署与运维/监控与日志管理.md create mode 100644 .qoder/repowiki/zh/content/部署与运维/运维最佳实践.md create mode 100644 .qoder/repowiki/zh/content/部署与运维/部署与运维.md create mode 100644 .qoder/repowiki/zh/content/项目概述/快速开始.md create mode 100644 .qoder/repowiki/zh/content/项目概述/技术栈.md create mode 100644 .qoder/repowiki/zh/content/项目概述/核心功能.md create mode 100644 .qoder/repowiki/zh/content/项目概述/系统架构.md create mode 100644 .qoder/repowiki/zh/content/项目概述/项目介绍.md create mode 100644 .qoder/repowiki/zh/content/项目概述/项目概述.md create mode 100644 .qoder/repowiki/zh/meta/repowiki-metadata.json create mode 100644 backend/Dockerfile create mode 100644 backend/alembic.ini create mode 100644 backend/alembic/README create mode 100644 backend/alembic/env.py create mode 100644 backend/alembic/script.py.mako create mode 100644 backend/alembic/versions/488d0bd5ab01_initial_migration.py create mode 100644 backend/app/__init__.py create mode 100644 backend/app/api/__init__.py create mode 100644 backend/app/api/auth.py create mode 100644 backend/app/api/citations.py create mode 100644 backend/app/api/deps.py create mode 100644 backend/app/api/queries.py create mode 100644 backend/app/api/reports.py create mode 100644 backend/app/config.py create mode 100644 backend/app/database.py create mode 100644 backend/app/main.py create mode 100644 backend/app/models/__init__.py create mode 100644 backend/app/models/citation_record.py create mode 100644 backend/app/models/query.py create mode 100644 backend/app/models/query_task.py create mode 100644 backend/app/models/subscription.py create mode 100644 backend/app/models/user.py create mode 100644 backend/app/schemas/__init__.py create mode 100644 backend/app/schemas/auth.py create mode 100644 backend/app/schemas/citation.py create mode 100644 backend/app/schemas/query.py create mode 100644 backend/app/services/__init__.py create mode 100644 backend/app/services/auth.py create mode 100644 backend/app/services/citation.py create mode 100644 backend/app/services/query.py create mode 100644 backend/app/utils/__init__.py create mode 100644 backend/app/workers/__init__.py create mode 100644 backend/app/workers/citation_engine.py create mode 100644 backend/app/workers/platforms/__init__.py create mode 100644 backend/app/workers/platforms/base.py create mode 100644 backend/app/workers/platforms/kimi.py create mode 100644 backend/app/workers/platforms/wenxin.py create mode 100644 backend/app/workers/scheduler.py create mode 100644 backend/requirements.txt create mode 100644 docker-compose.yml create mode 100644 docs/GEO 平台 MVP 开发指导文档(Trae_Qoder 专用).pdf create mode 100644 frontend/.eslintrc.json create mode 100644 frontend/.gitignore create mode 100644 frontend/Dockerfile create mode 100644 frontend/README.md create mode 100644 frontend/app/(auth)/layout.tsx create mode 100644 frontend/app/(auth)/login/page.tsx create mode 100644 frontend/app/(auth)/register/page.tsx create mode 100644 frontend/app/(dashboard)/dashboard/citations/page.tsx create mode 100644 frontend/app/(dashboard)/dashboard/page.tsx create mode 100644 frontend/app/(dashboard)/dashboard/queries/page.tsx create mode 100644 frontend/app/(dashboard)/dashboard/reports/page.tsx create mode 100644 frontend/app/(dashboard)/dashboard/settings/page.tsx create mode 100644 frontend/app/(dashboard)/layout.tsx create mode 100644 frontend/app/api/auth/[...nextauth]/route.ts create mode 100644 frontend/app/favicon.ico create mode 100644 frontend/app/fonts/GeistMonoVF.woff create mode 100644 frontend/app/fonts/GeistVF.woff create mode 100644 frontend/app/globals.css create mode 100644 frontend/app/layout.tsx create mode 100644 frontend/app/page.tsx create mode 100644 frontend/components/charts/platform-chart.tsx create mode 100644 frontend/components/charts/trend-chart.tsx create mode 100644 frontend/components/layout/header.tsx create mode 100644 frontend/components/layout/sidebar.tsx create mode 100644 frontend/components/providers.tsx create mode 100644 frontend/components/ui/badge.tsx create mode 100644 frontend/components/ui/button.tsx create mode 100644 frontend/components/ui/card.tsx create mode 100644 frontend/components/ui/dialog.tsx create mode 100644 frontend/components/ui/dropdown-menu.tsx create mode 100644 frontend/components/ui/input.tsx create mode 100644 frontend/components/ui/label.tsx create mode 100644 frontend/components/ui/select.tsx create mode 100644 frontend/components/ui/skeleton.tsx create mode 100644 frontend/components/ui/table.tsx create mode 100644 frontend/components/ui/tabs.tsx create mode 100644 frontend/lib/api.ts create mode 100644 frontend/lib/auth.ts create mode 100644 frontend/lib/platforms.ts create mode 100644 frontend/lib/utils.ts create mode 100644 frontend/next.config.mjs create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/postcss.config.mjs create mode 100644 frontend/tailwind.config.ts create mode 100644 frontend/tsconfig.json create mode 100644 frontend/types/next-auth.d.ts create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/test_auth.py create mode 100644 tests/test_business_flow.py create mode 100644 tests/test_citation_engine.py create mode 100644 tests/test_citations.py create mode 100644 tests/test_queries.py create mode 100644 tests/test_scheduler.py diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..93a26ed --- /dev/null +++ b/.env.example @@ -0,0 +1,19 @@ +# 数据库 +DATABASE_URL=postgresql+asyncpg://postgres:postgres123@db:5432/geo_platform + +# Redis +REDIS_URL=redis://redis:6379/0 + +# JWT +JWT_SECRET=your-secret-key-change-in-production +JWT_EXPIRE_HOURS=24 + +# 前端 +NEXT_PUBLIC_API_URL=http://localhost:8000 + +# Playwright +PLAYWRIGHT_BROWSERS_PATH=/ms-playwright + +# 国内大模型API(可选) +ZHIPU_API_KEY= +TONGYI_API_KEY= diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..326ac6f --- /dev/null +++ b/.gitignore @@ -0,0 +1,43 @@ +# Node +node_modules/ +frontend/node_modules/ +frontend/.next/ +frontend/out/ + +# Python +__pycache__/ +*.pyc +*.pyo +backend/venv/ +backend/.venv/ +*.egg-info/ + +# Environment +.env +frontend/.env.local + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Database +*.db +*.sqlite3 + +# Logs +*.log + +# Docker +postgres_data/ +redis_data/ + +# Cache +.npm-cache/ +.pytest_cache/ +tsconfig.tsbuildinfo diff --git a/.qoder/repowiki/zh/content/AI平台集成/AI平台集成.md b/.qoder/repowiki/zh/content/AI平台集成/AI平台集成.md new file mode 100644 index 0000000..afd216e --- /dev/null +++ b/.qoder/repowiki/zh/content/AI平台集成/AI平台集成.md @@ -0,0 +1,447 @@ +# AI平台集成 + + +**本文档引用的文件** +- [backend/app/workers/platforms/base.py](file://backend/app/workers/platforms/base.py) +- [backend/app/workers/platforms/kimi.py](file://backend/app/workers/platforms/kimi.py) +- [backend/app/workers/platforms/wenxin.py](file://backend/app/workers/platforms/wenxin.py) +- [backend/app/workers/citation_engine.py](file://backend/app/workers/citation_engine.py) +- [backend/app/config.py](file://backend/app/config.py) +- [backend/app/models/query.py](file://backend/app/models/query.py) +- [backend/app/models/citation_record.py](file://backend/app/models/citation_record.py) +- [backend/app/models/query_task.py](file://backend/app/models/query_task.py) +- [backend/app/workers/scheduler.py](file://backend/app/workers/scheduler.py) +- [backend/app/api/citations.py](file://backend/app/api/citations.py) +- [backend/app/services/citation.py](file://backend/app/services/citation.py) +- [frontend/lib/platforms.ts](file://frontend/lib/platforms.ts) +- [tests/test_citation_engine.py](file://tests/test_citation_engine.py) +- [backend/requirements.txt](file://backend/requirements.txt) + + +## 目录 +1. [简介](#简介) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖分析](#依赖分析) +7. [性能考虑](#性能考虑) +8. [故障排查指南](#故障排查指南) +9. [结论](#结论) +10. [附录](#附录) + +## 简介 +本项目是一个AI平台集成系统,支持通过统一适配器接口对接多个大模型平台(如Kimi、文心一言)。系统包含以下能力: +- 适配器架构:以抽象基类为统一接口,扩展新的AI平台只需实现query方法。 +- 浏览器自动化:基于Playwright对网页版AI平台进行自动交互,包含页面定位、输入提交、响应稳定检测与指数退避重试。 +- 引用检测引擎:对平台返回内容进行品牌匹配、竞争品牌识别与置信度评分,生成引用记录。 +- 定时调度:基于APScheduler的异步调度器,周期性检查并执行到期查询任务。 +- API与服务层:提供查询与统计接口,支持立即执行、导出CSV等功能。 + +## 项目结构 +后端采用分层架构: +- workers:工作流与平台适配器、引用检测引擎、调度器 +- models:数据库ORM模型(查询、引用记录、任务) +- services:业务服务(查询统计、触发执行、导出) +- api:FastAPI路由与对外接口 +- config:应用配置(环境变量读取) + +```mermaid +graph TB +subgraph "前端" +FE["前端应用
platforms.ts"] +end +subgraph "后端" +API["API 层
citations.py"] +SVC["服务层
services/citation.py"] +SCH["调度器
workers/scheduler.py"] +CE["引用检测引擎
workers/citation_engine.py"] +AD_K["适配器: Kimi
workers/platforms/kimi.py"] +AD_W["适配器: 文心一言
workers/platforms/wenxin.py"] +CFG["配置
app/config.py"] +DB_Q["模型: Query
models/query.py"] +DB_CR["模型: CitationRecord
models/citation_record.py"] +DB_QT["模型: QueryTask
models/query_task.py"] +end +FE --> API +API --> SVC +SVC --> SCH +SCH --> CE +CE --> AD_K +CE --> AD_W +CE --> DB_CR +CE --> DB_QT +CE --> DB_Q +AD_K --> CFG +AD_W --> CFG +``` + +**图表来源** +- [backend/app/api/citations.py:1-78](file://backend/app/api/citations.py#L1-L78) +- [backend/app/services/citation.py:1-269](file://backend/app/services/citation.py#L1-L269) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) +- [backend/app/workers/platforms/kimi.py:1-206](file://backend/app/workers/platforms/kimi.py#L1-L206) +- [backend/app/workers/platforms/wenxin.py:1-205](file://backend/app/workers/platforms/wenxin.py#L1-L205) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [backend/app/models/citation_record.py:1-42](file://backend/app/models/citation_record.py#L1-L42) +- [backend/app/models/query_task.py:1-39](file://backend/app/models/query_task.py#L1-L39) + +**章节来源** +- [backend/app/api/citations.py:1-78](file://backend/app/api/citations.py#L1-L78) +- [backend/app/services/citation.py:1-269](file://backend/app/services/citation.py#L1-L269) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) +- [backend/app/workers/platforms/base.py:1-18](file://backend/app/workers/platforms/base.py#L1-L18) +- [backend/app/workers/platforms/kimi.py:1-206](file://backend/app/workers/platforms/kimi.py#L1-L206) +- [backend/app/workers/platforms/wenxin.py:1-205](file://backend/app/workers/platforms/wenxin.py#L1-L205) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [backend/app/models/citation_record.py:1-42](file://backend/app/models/citation_record.py#L1-L42) +- [backend/app/models/query_task.py:1-39](file://backend/app/models/query_task.py#L1-L39) +- [frontend/lib/platforms.ts:1-18](file://frontend/lib/platforms.ts#L1-L18) + +## 核心组件 +- 抽象适配器基类:定义统一的平台名称、URL以及查询接口,便于扩展新平台。 +- 平台适配器(Kimi/文心一言):基于Playwright实现页面交互、输入提交、响应稳定检测与重试。 +- 引用检测引擎:品牌匹配(精确/别名/模糊)、竞争品牌识别、置信度评分与记录生成。 +- 调度器:定时扫描到期查询,调用引擎执行并更新任务状态。 +- API与服务:提供查询列表、统计、立即执行、导出CSV等接口。 +- 数据模型:Query、CitationRecord、QueryTask支撑查询生命周期与结果存储。 + +**章节来源** +- [backend/app/workers/platforms/base.py:1-18](file://backend/app/workers/platforms/base.py#L1-L18) +- [backend/app/workers/platforms/kimi.py:1-206](file://backend/app/workers/platforms/kimi.py#L1-L206) +- [backend/app/workers/platforms/wenxin.py:1-205](file://backend/app/workers/platforms/wenxin.py#L1-L205) +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [backend/app/api/citations.py:1-78](file://backend/app/api/citations.py#L1-L78) +- [backend/app/services/citation.py:1-269](file://backend/app/services/citation.py#L1-L269) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [backend/app/models/citation_record.py:1-42](file://backend/app/models/citation_record.py#L1-L42) +- [backend/app/models/query_task.py:1-39](file://backend/app/models/query_task.py#L1-L39) + +## 架构总览 +系统通过“适配器 + 引擎 + 调度器”的解耦方式实现多平台接入与统一处理流程。前端通过API触发查询与查看统计;调度器按频率驱动查询;引擎负责跨平台数据采集与分析;服务层提供数据访问与导出能力。 + +```mermaid +sequenceDiagram +participant FE as "前端" +participant API as "API 层" +participant SVC as "服务层" +participant SCH as "调度器" +participant CE as "引用检测引擎" +participant AD as "平台适配器" +participant DB as "数据库" +FE->>API : 触发立即查询 +API->>SVC : trigger_query_now() +SVC->>DB : 写入 QueryTask(状态 pending) +Note over SCH,DB : 定时任务扫描 next_query_at<=now 的查询 +SCH->>CE : execute_query(query) +CE->>AD : query(keyword) +AD-->>CE : 原始响应文本 +CE->>CE : 品牌匹配/竞争品牌识别 +CE->>DB : 写入 CitationRecord +CE->>DB : 更新 Query.next_query_at +API-->>FE : 返回任务状态/查询结果 +``` + +**图表来源** +- [backend/app/api/citations.py:59-78](file://backend/app/api/citations.py#L59-L78) +- [backend/app/services/citation.py:204-234](file://backend/app/services/citation.py#L204-L234) +- [backend/app/workers/scheduler.py:51-84](file://backend/app/workers/scheduler.py#L51-L84) +- [backend/app/workers/citation_engine.py:159-234](file://backend/app/workers/citation_engine.py#L159-L234) +- [backend/app/workers/platforms/kimi.py:33-48](file://backend/app/workers/platforms/kimi.py#L33-L48) +- [backend/app/workers/platforms/wenxin.py:33-48](file://backend/app/workers/platforms/wenxin.py#L33-L48) +- [backend/app/models/query.py:29-31](file://backend/app/models/query.py#L29-L31) +- [backend/app/models/citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) +- [backend/app/models/query_task.py:11-39](file://backend/app/models/query_task.py#L11-L39) + +## 详细组件分析 + +### 适配器架构与扩展机制 +- 抽象基类定义 + - 平台标识:platform_name、platform_url + - 统一接口:query(keyword)返回原始响应文本;可选close()释放资源 +- 扩展步骤 + - 继承BasePlatformAdapter + - 实现query与可选close + - 在CitationEngine的platforms映射中注册实例 +- 设计优势 + - 解耦平台差异,统一调用入口 + - 易于新增平台与替换实现 + +```mermaid +classDiagram +class BasePlatformAdapter { ++string platform_name ++string platform_url ++query(keyword) str ++close() void +} +class KimiAdapter { ++platform_name = "kimi" ++platform_url = "https : //kimi.moonshot.cn" ++query(keyword) str +-_ensure_browser() +-_do_query(keyword) str +-_wait_for_response_stable(page) str ++close() void +} +class WenxinAdapter { ++platform_name = "wenxin" ++platform_url = "https : //yiyan.baidu.com" ++query(keyword) str +-_ensure_browser() +-_do_query(keyword) str +-_wait_for_response_stable(page) str ++close() void +} +BasePlatformAdapter <|-- KimiAdapter +BasePlatformAdapter <|-- WenxinAdapter +``` + +**图表来源** +- [backend/app/workers/platforms/base.py:4-17](file://backend/app/workers/platforms/base.py#L4-L17) +- [backend/app/workers/platforms/kimi.py:11-206](file://backend/app/workers/platforms/kimi.py#L11-L206) +- [backend/app/workers/platforms/wenxin.py:11-205](file://backend/app/workers/platforms/wenxin.py#L11-L205) + +**章节来源** +- [backend/app/workers/platforms/base.py:1-18](file://backend/app/workers/platforms/base.py#L1-L18) +- [backend/app/workers/platforms/kimi.py:1-206](file://backend/app/workers/platforms/kimi.py#L1-L206) +- [backend/app/workers/platforms/wenxin.py:1-205](file://backend/app/workers/platforms/wenxin.py#L1-L205) + +### Kimi平台浏览器自动化集成 +- Playwright配置 + - 启动headless Chromium,设置视口与UA + - 异常处理:若未安装浏览器,抛出明确提示 +- 页面交互逻辑 + - 导航至平台URL,等待页面加载 + - 多选择器尝试定位输入框,支持textarea/div/input等 + - 自动提交:优先点击发送按钮,否则按回车 +- 响应稳定检测 + - 轮询消息容器,连续多次检测文本未变化视为稳定 + - 超时则返回当前文本 +- 错误重试机制 + - 最多重试3次,指数退避(2^attempt秒延迟) + - 记录每次尝试日志,最终失败抛出异常 + +```mermaid +flowchart TD +Start(["开始查询"]) --> Ensure["启动浏览器"] +Ensure --> NewContext["新建上下文与页面"] +NewContext --> Goto["导航到平台URL"] +Goto --> WaitInput["等待输入框出现"] +WaitInput --> Fill["填充关键词"] +Fill --> Submit{"找到发送按钮?"} +Submit --> |是| Click["点击发送按钮"] +Submit --> |否| Enter["按回车键提交"] +Click --> WaitResp["等待回复稳定"] +Enter --> WaitResp +WaitResp --> Stable{"文本稳定?"} +Stable --> |是| Return["返回响应文本"] +Stable --> |否| Sleep["等待2秒"] --> WaitResp +Return --> Close["关闭页面与上下文"] +Close --> End(["结束"]) +``` + +**图表来源** +- [backend/app/workers/platforms/kimi.py:21-125](file://backend/app/workers/platforms/kimi.py#L21-L125) +- [backend/app/workers/platforms/kimi.py:126-197](file://backend/app/workers/platforms/kimi.py#L126-L197) + +**章节来源** +- [backend/app/workers/platforms/kimi.py:1-206](file://backend/app/workers/platforms/kimi.py#L1-L206) + +### 文心一言平台API集成实现 +- 平台适配器实现 + - 与Kimi适配器一致的Playwright交互流程 + - 输入框与发送按钮选择策略、响应稳定检测与重试机制 +- 配置管理 + - 通过配置类读取环境变量,支持自定义浏览器路径等参数 +- 注意事项 + - 若Playwright浏览器未安装,会抛出明确错误提示,需先安装对应浏览器 + +**章节来源** +- [backend/app/workers/platforms/wenxin.py:1-205](file://backend/app/workers/platforms/wenxin.py#L1-L205) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) + +### 引用检测引擎工作原理 +- 品牌匹配策略 + - 精确匹配:命中目标品牌,置信度1.0 + - 别名匹配:命中别名,置信度0.9 + - 模糊匹配:基于序列相似度阈值,返回最高相似度及置信度 + - 结果包含是否引用、置信度、匹配类型、段落位置、上下文片段 +- 竞争品牌识别 + - 预定义行业品牌集合,从文本中识别除目标品牌外的竞争品牌 +- 置信度评分机制 + - 精确命中1.0,别名0.9,模糊按相似度取值并四舍五入 +- 引擎执行流程 + - 构建BrandMatcher,遍历查询配置的平台 + - 调用适配器获取原始响应,执行匹配与竞争品牌检测 + - 生成CitationRecord并写入数据库,更新Query.next_query_at + +```mermaid +flowchart TD +Q["输入: 关键词/目标品牌/别名"] --> BuildMatcher["构建BrandMatcher"] +BuildMatcher --> ForEachPlat{"遍历平台"} +ForEachPlat --> QueryRaw["调用适配器.query()"] +QueryRaw --> Match["品牌匹配(match)"] +Match --> Competitor["竞争品牌检测(detect)"] +Competitor --> Record["生成CitationRecord"] +Record --> Persist["写入数据库"] +Persist --> NextTime["更新Query.next_query_at"] +NextTime --> ForEachPlat +ForEachPlat --> Done["完成"] +``` + +**图表来源** +- [backend/app/workers/citation_engine.py:148-234](file://backend/app/workers/citation_engine.py#L148-L234) +- [backend/app/workers/citation_engine.py:236-266](file://backend/app/workers/citation_engine.py#L236-L266) +- [backend/app/models/citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) +- [backend/app/models/query.py:29-31](file://backend/app/models/query.py#L29-L31) + +**章节来源** +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) +- [tests/test_citation_engine.py:1-54](file://tests/test_citation_engine.py#L1-L54) + +### 定时调度与任务管理 +- 调度器 + - 使用APScheduler的AsyncIOScheduler,每小时检查一次 + - 查找status='active'且next_query_at<=now的查询 + - 逐个调用CitationEngine.execute_query并更新QueryTask状态 +- 任务模型 + - QueryTask记录平台、状态、错误信息与时间戳 + - 支持pending/running/success/failed状态流转 + +```mermaid +sequenceDiagram +participant S as "调度器" +participant DB as "数据库" +participant CE as "引用检测引擎" +participant QT as "QueryTask" +S->>DB : 查询 active 且到期的 Query +DB-->>S : 返回待执行查询列表 +loop 遍历查询 +S->>QT : 创建/更新 QueryTask +S->>CE : execute_query(query) +CE-->>S : 返回 CitationRecord 列表 +S->>DB : 更新 QueryTask 状态/时间 +end +``` + +**图表来源** +- [backend/app/workers/scheduler.py:51-84](file://backend/app/workers/scheduler.py#L51-L84) +- [backend/app/models/query_task.py:11-39](file://backend/app/models/query_task.py#L11-L39) + +**章节来源** +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [backend/app/models/query_task.py:1-39](file://backend/app/models/query_task.py#L1-L39) + +### API与服务层 +- API路由 + - 列表查询、统计、立即执行查询 + - 立即执行返回任务状态与消息 +- 服务层 + - 权限校验:仅允许用户访问自己的查询 + - 统计聚合:总查询数、引用数、引用率、按平台分布、趋势 + - 导出CSV:将引用记录导出为CSV字符串 + +**章节来源** +- [backend/app/api/citations.py:1-78](file://backend/app/api/citations.py#L1-L78) +- [backend/app/services/citation.py:1-269](file://backend/app/services/citation.py#L1-L269) + +## 依赖分析 +- 外部依赖 + - FastAPI、SQLAlchemy、APScheduler、Playwright、Pydantic Settings等 +- 内部模块耦合 + - CitationEngine依赖适配器与数据库模型 + - Scheduler依赖CitationEngine与Query模型 + - API与Service层依赖数据库与权限控制 + +```mermaid +graph LR +REQ["requirements.txt"] --> FA["FastAPI"] +REQ --> SA["SQLAlchemy"] +REQ --> AP["APScheduler"] +REQ --> PW["Playwright"] +REQ --> PYD["Pydantic Settings"] +CE["CitationEngine"] --> AD1["KimiAdapter"] +CE --> AD2["WenxinAdapter"] +CE --> DB1["CitationRecord"] +CE --> DB2["QueryTask"] +CE --> DB3["Query"] +SCH["Scheduler"] --> CE +API["API"] --> SVC["Service"] +SVC --> DB1 +SVC --> DB3 +``` + +**图表来源** +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [backend/app/api/citations.py:1-78](file://backend/app/api/citations.py#L1-L78) +- [backend/app/services/citation.py:1-269](file://backend/app/services/citation.py#L1-L269) + +**章节来源** +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) + +## 性能考虑 +- 浏览器资源管理 + - 适配器在close中释放浏览器与上下文,避免资源泄漏 +- 重试与退避 + - 指数退避降低平台压力,提升成功率 +- 响应稳定检测 + - 通过连续检测减少过早截断,提高稳定性 +- 数据库索引 + - Query与CitationRecord的关键字段建立索引,优化查询性能 +- 异步调度 + - 使用AsyncIOScheduler与异步数据库连接,提升并发效率 + +[本节为通用建议,无需特定文件引用] + +## 故障排查指南 +- Playwright浏览器未安装 + - 现象:启动浏览器时报错,提示需要安装浏览器 + - 处理:按照提示运行安装命令,确保浏览器可用 +- 页面元素定位失败 + - 现象:找不到输入框或发送按钮 + - 处理:检查选择器策略或平台UI变更,必要时调整选择器集合 +- 响应长时间不稳定 + - 现象:等待回复超时返回当前文本 + - 处理:适当延长等待时间或检查网络与平台状态 +- 查询任务失败 + - 现象:QueryTask状态为failed并记录错误信息 + - 处理:查看错误日志,确认平台可用性与网络状况 + +**章节来源** +- [backend/app/workers/platforms/kimi.py:27-31](file://backend/app/workers/platforms/kimi.py#L27-L31) +- [backend/app/workers/platforms/kimi.py:87-88](file://backend/app/workers/platforms/kimi.py#L87-L88) +- [backend/app/workers/platforms/kimi.py:145-148](file://backend/app/workers/platforms/kimi.py#L145-L148) +- [backend/app/workers/citation_engine.py:211-227](file://backend/app/workers/citation_engine.py#L211-L227) + +## 结论 +该系统通过适配器模式实现了对多平台的统一接入,结合浏览器自动化与引用检测引擎,提供了从查询、匹配到统计与导出的完整能力。调度器保障了周期性任务的可靠执行,API与服务层为前端与运维提供了清晰的接口。未来可在适配器扩展、稳定性与性能优化方面持续演进。 + +[本节为总结性内容,无需特定文件引用] + +## 附录 + +### 新AI平台接入扩展指南与最佳实践 +- 扩展步骤 + - 新建适配器类继承BasePlatformAdapter,实现query与可选close + - 在CitationEngine的platforms映射中注册新适配器实例 + - 在前端platforms.ts中添加平台映射与展示项 +- 最佳实践 + - 明确定义platform_name与platform_url + - 统一异常处理与日志记录 + - 使用指数退避与响应稳定检测提升鲁棒性 + - 合理设置超时与重试次数 + - 在close中确保资源释放 + - 为新平台编写单元测试覆盖关键场景 + +**章节来源** +- [backend/app/workers/platforms/base.py:4-17](file://backend/app/workers/platforms/base.py#L4-L17) +- [backend/app/workers/citation_engine.py:152-155](file://backend/app/workers/citation_engine.py#L152-L155) +- [frontend/lib/platforms.ts:1-18](file://frontend/lib/platforms.ts#L1-L18) \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/AI平台集成/Kimi平台集成.md b/.qoder/repowiki/zh/content/AI平台集成/Kimi平台集成.md new file mode 100644 index 0000000..bbb4ea8 --- /dev/null +++ b/.qoder/repowiki/zh/content/AI平台集成/Kimi平台集成.md @@ -0,0 +1,438 @@ +# Kimi平台集成 + + +**本文档引用的文件** +- [backend/app/workers/platforms/kimi.py](file://backend/app/workers/platforms/kimi.py) +- [backend/app/workers/platforms/base.py](file://backend/app/workers/platforms/base.py) +- [backend/app/workers/citation_engine.py](file://backend/app/workers/citation_engine.py) +- [backend/app/workers/scheduler.py](file://backend/app/workers/scheduler.py) +- [backend/app/models/query.py](file://backend/app/models/query.py) +- [backend/app/models/citation_record.py](file://backend/app/models/citation_record.py) +- [backend/app/models/query_task.py](file://backend/app/models/query_task.py) +- [backend/app/services/query.py](file://backend/app/services/query.py) +- [backend/app/services/citation.py](file://backend/app/services/citation.py) +- [backend/app/api/queries.py](file://backend/app/api/queries.py) +- [backend/app/api/citations.py](file://backend/app/api/citations.py) +- [backend/app/main.py](file://backend/app/main.py) +- [frontend/lib/platforms.ts](file://frontend/lib/platforms.ts) + + +## 目录 +1. [简介](#简介) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖关系分析](#依赖关系分析) +7. [性能考虑](#性能考虑) +8. [故障排查指南](#故障排查指南) +9. [结论](#结论) +10. [附录](#附录) + +## 简介 +本文件面向需要集成Kimi平台的开发者与运维人员,系统性阐述Kimi适配器的实现方式与运行机制,涵盖Playwright浏览器自动化配置与初始化、页面交互逻辑(搜索框定位、输入处理、搜索按钮点击、结果解析)、错误重试与超时处理、浏览器会话管理与资源清理、API调用示例与常见问题解决,以及性能优化与调试技巧。 + +## 项目结构 +后端采用FastAPI + SQLAlchemy + APScheduler的异步架构,前端通过Next.js提供可视化界面。Kimi适配器位于工作线程模块,被引用检测引擎统一编排,定时调度器周期性触发查询任务。 + +```mermaid +graph TB +subgraph "前端" +FE_Platforms["前端平台映射
frontend/lib/platforms.ts"] +end +subgraph "后端" +API_Queries["查询API
backend/app/api/queries.py"] +API_Citations["引用API
backend/app/api/citations.py"] +Service_Query["查询服务
backend/app/services/query.py"] +Service_Citation["引用服务
backend/app/services/citation.py"] +Scheduler["定时调度器
backend/app/workers/scheduler.py"] +Engine["引用检测引擎
backend/app/workers/citation_engine.py"] +Adapter_Kimi["Kimi适配器
backend/app/workers/platforms/kimi.py"] +Model_Query["查询模型
backend/app/models/query.py"] +Model_Record["引用记录模型
backend/app/models/citation_record.py"] +Model_Task["查询任务模型
backend/app/models/query_task.py"] +end +FE_Platforms --> API_Queries +FE_Platforms --> API_Citations +API_Queries --> Service_Query +API_Citations --> Service_Citation +Service_Query --> Scheduler +Service_Citation --> Scheduler +Scheduler --> Engine +Engine --> Adapter_Kimi +Engine --> Model_Query +Engine --> Model_Record +Engine --> Model_Task +``` + +图表来源 +- [backend/app/main.py:13-22](file://backend/app/main.py#L13-L22) +- [backend/app/workers/scheduler.py:25-40](file://backend/app/workers/scheduler.py#L25-L40) +- [backend/app/workers/citation_engine.py:148-157](file://backend/app/workers/citation_engine.py#L148-L157) +- [backend/app/workers/platforms/kimi.py:11-20](file://backend/app/workers/platforms/kimi.py#L11-L20) +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [backend/app/models/citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) +- [backend/app/models/query_task.py:11-39](file://backend/app/models/query_task.py#L11-L39) + +章节来源 +- [backend/app/main.py:13-22](file://backend/app/main.py#L13-L22) +- [backend/app/workers/scheduler.py:25-40](file://backend/app/workers/scheduler.py#L25-L40) +- [frontend/lib/platforms.ts:1-18](file://frontend/lib/platforms.ts#L1-L18) + +## 核心组件 +- Kimi适配器:基于Playwright的异步浏览器自动化,负责导航、输入、提交、等待响应稳定与结果提取。 +- 引用检测引擎:编排多平台查询,执行品牌匹配与竞争品牌检测,并持久化结果。 +- 定时调度器:周期性扫描到期查询,触发执行并更新任务状态。 +- 数据模型:查询、引用记录、查询任务三者构成完整的查询生命周期与结果存储。 +- API与服务:提供REST接口与业务服务,支撑前端展示与手动触发“立即执行”。 + +章节来源 +- [backend/app/workers/platforms/kimi.py:11-20](file://backend/app/workers/platforms/kimi.py#L11-L20) +- [backend/app/workers/citation_engine.py:148-157](file://backend/app/workers/citation_engine.py#L148-L157) +- [backend/app/workers/scheduler.py:25-40](file://backend/app/workers/scheduler.py#L25-L40) +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [backend/app/models/citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) +- [backend/app/models/query_task.py:11-39](file://backend/app/models/query_task.py#L11-L39) + +## 架构总览 +下图展示了从用户发起查询到结果入库的关键路径,以及Kimi适配器在其中的角色。 + +```mermaid +sequenceDiagram +participant FE as "前端" +participant API as "后端API" +participant SVC as "服务层" +participant SCH as "调度器" +participant ENG as "引用检测引擎" +participant ADP as "Kimi适配器" +participant PW as "Playwright浏览器" +FE->>API : "POST /api/v1/queries 或 /api/v1/citations/run-now" +API->>SVC : "创建查询/立即执行" +SVC->>SCH : "登记查询任务" +SCH->>ENG : "周期性执行查询" +ENG->>ADP : "query(keyword)" +ADP->>PW : "启动浏览器/新建上下文/打开页面" +ADP->>PW : "定位输入框/输入关键词/提交" +ADP->>PW : "等待回复稳定/提取文本" +ADP-->>ENG : "返回原始响应文本" +ENG-->>SVC : "品牌匹配/竞争品牌检测" +SVC-->>API : "写入引用记录" +API-->>FE : "返回查询结果/统计" +``` + +图表来源 +- [backend/app/api/queries.py:26-39](file://backend/app/api/queries.py#L26-L39) +- [backend/app/api/citations.py:59-77](file://backend/app/api/citations.py#L59-L77) +- [backend/app/services/query.py:45-81](file://backend/app/services/query.py#L45-L81) +- [backend/app/services/citation.py:204-234](file://backend/app/services/citation.py#L204-L234) +- [backend/app/workers/scheduler.py:51-84](file://backend/app/workers/scheduler.py#L51-L84) +- [backend/app/workers/citation_engine.py:159-234](file://backend/app/workers/citation_engine.py#L159-L234) +- [backend/app/workers/platforms/kimi.py:33-125](file://backend/app/workers/platforms/kimi.py#L33-L125) + +## 详细组件分析 + +### Kimi适配器(Browser Automation) +- 启动与初始化 + - 通过异步Playwright启动Chromium浏览器,headless模式运行。 + - 若浏览器未安装,捕获异常并提示安装命令。 +- 页面交互 + - 导航至Kimi首页,设置页面加载超时。 + - 多选择器策略定位输入框(textarea、contenteditable等),逐个尝试直至命中。 + - 输入关键词后,优先点击发送按钮;若未发现发送按钮则模拟回车提交。 +- 结果解析与稳定性 + - 使用多级消息容器选择器提取最新回复内容。 + - 通过“连续N次文本不变”判定回复稳定,避免过早截断。 + - 超时情况下返回当前已收集文本。 +- 错误处理与重试 + - 单次查询失败时进行最多两次重试(共三次尝试),指数退避延迟。 + - 页面超时与通用异常均转换为可读的运行时错误。 +- 资源管理 + - 每次查询独立创建上下文与页面,完成后及时关闭,避免资源泄漏。 + - 提供显式关闭方法用于进程退出时清理Playwright与浏览器实例。 + +```mermaid +flowchart TD +Start(["进入 query(keyword)"]) --> Ensure["确保浏览器已启动"] +Ensure --> RetryLoop{"重试次数 < 3"} +RetryLoop --> |是| DoQuery["_do_query 执行单次查询"] +DoQuery --> Stable["等待回复稳定"] +Stable --> Success["返回响应文本"] +DoQuery --> |异常| Delay["指数退避等待"] +Delay --> RetryLoop +RetryLoop --> |否| FinalFail["记录最终失败并抛出异常"] +Success --> Close["关闭页面/上下文"] +FinalFail --> Close +Close --> End(["结束"]) +``` + +图表来源 +- [backend/app/workers/platforms/kimi.py:33-48](file://backend/app/workers/platforms/kimi.py#L33-L48) +- [backend/app/workers/platforms/kimi.py:50-125](file://backend/app/workers/platforms/kimi.py#L50-L125) +- [backend/app/workers/platforms/kimi.py:126-197](file://backend/app/workers/platforms/kimi.py#L126-L197) +- [backend/app/workers/platforms/kimi.py:198-206](file://backend/app/workers/platforms/kimi.py#L198-L206) + +章节来源 +- [backend/app/workers/platforms/kimi.py:17-32](file://backend/app/workers/platforms/kimi.py#L17-L32) +- [backend/app/workers/platforms/kimi.py:33-48](file://backend/app/workers/platforms/kimi.py#L33-L48) +- [backend/app/workers/platforms/kimi.py:50-125](file://backend/app/workers/platforms/kimi.py#L50-L125) +- [backend/app/workers/platforms/kimi.py:126-197](file://backend/app/workers/platforms/kimi.py#L126-L197) +- [backend/app/workers/platforms/kimi.py:198-206](file://backend/app/workers/platforms/kimi.py#L198-L206) + +### 引用检测引擎(Citation Engine) +- 平台编排 + - 维护平台适配器映射,支持Kimi与文心等平台。 + - 针对每个查询遍历指定平台,执行查询与检测。 +- 品牌匹配 + - 支持精确匹配、别名匹配、模糊匹配三种策略,输出置信度与首次出现位置。 +- 竞争品牌检测 + - 基于预设行业品牌库,识别文本中除目标品牌外的其他品牌。 +- 任务状态管理 + - 为每次平台查询创建或获取对应任务记录,维护状态与错误信息。 +- 结果持久化 + - 将引用检测结果写入引用记录表,包含是否引用、位置、文本片段、竞争品牌及原始响应。 + +```mermaid +classDiagram +class CitationEngine { ++execute_query(query, db) list ++execute_single_platform(keyword, platform, target_brand, aliases) dict ++close() void +} +class KimiAdapter { ++query(keyword) str ++close() void +} +class BrandMatcher { ++match(text) dict +} +class CompetitorDetector { ++detect(text, target_brand) list +} +CitationEngine --> KimiAdapter : "依赖" +CitationEngine --> BrandMatcher : "使用" +CitationEngine --> CompetitorDetector : "使用" +``` + +图表来源 +- [backend/app/workers/citation_engine.py:148-157](file://backend/app/workers/citation_engine.py#L148-L157) +- [backend/app/workers/citation_engine.py:236-266](file://backend/app/workers/citation_engine.py#L236-L266) +- [backend/app/workers/platforms/kimi.py:11-20](file://backend/app/workers/platforms/kimi.py#L11-L20) + +章节来源 +- [backend/app/workers/citation_engine.py:148-157](file://backend/app/workers/citation_engine.py#L148-L157) +- [backend/app/workers/citation_engine.py:159-234](file://backend/app/workers/citation_engine.py#L159-L234) +- [backend/app/workers/citation_engine.py:236-266](file://backend/app/workers/citation_engine.py#L236-L266) + +### 定时调度器(Scheduler) +- 触发机制 + - 使用APScheduler的AsyncIOScheduler,每小时检查一次到期查询。 + - 条件:查询状态为激活且next_query_at小于等于当前时间。 +- 执行流程 + - 逐条执行查询,调用引用检测引擎,更新任务状态与查询时间字段。 +- 关闭流程 + - 应用关闭时停止调度器并关闭各平台适配器。 + +```mermaid +sequenceDiagram +participant S as "调度器" +participant DB as "数据库" +participant ENG as "引用检测引擎" +participant ADP as "Kimi适配器" +S->>DB : "查询到期的查询记录" +loop 遍历查询 +S->>ENG : "execute_query(query, db)" +ENG->>ADP : "query(keyword)" +ADP-->>ENG : "返回原始响应" +ENG-->>S : "写入引用记录并更新时间" +end +S->>S : "关闭时调用 engine.close()" +``` + +图表来源 +- [backend/app/workers/scheduler.py:51-84](file://backend/app/workers/scheduler.py#L51-L84) +- [backend/app/workers/citation_engine.py:302-309](file://backend/app/workers/citation_engine.py#L302-L309) + +章节来源 +- [backend/app/workers/scheduler.py:25-40](file://backend/app/workers/scheduler.py#L25-L40) +- [backend/app/workers/scheduler.py:51-84](file://backend/app/workers/scheduler.py#L51-L84) +- [backend/app/workers/scheduler.py:86-90](file://backend/app/workers/scheduler.py#L86-L90) + +### 数据模型(Models) +- 查询(Query) + - 关键字段:关键词、目标品牌、品牌别名、平台列表、频率、状态、下次查询时间等。 +- 引用记录(CitationRecord) + - 关键字段:是否引用、引用位置、引用文本、竞争品牌列表、原始响应、查询时间等。 +- 查询任务(QueryTask) + - 关键字段:状态、错误信息、计划/开始/完成时间等。 + +```mermaid +erDiagram +QUERY { +uuid id PK +uuid user_id FK +string keyword +string target_brand +jsonb brand_aliases +jsonb platforms +string frequency +string status +timestamp last_queried_at +timestamp next_query_at +timestamp created_at +timestamp updated_at +} +CITATION_RECORD { +uuid id PK +uuid query_id FK +string platform +boolean cited +int citation_position +text citation_text +jsonb competitor_brands +text raw_response +timestamp queried_at +} +QUERY_TASK { +uuid id PK +uuid query_id FK +string platform +string status +text error_message +timestamp scheduled_at +timestamp started_at +timestamp completed_at +} +QUERY ||--o{ CITATION_RECORD : "拥有" +QUERY ||--o{ QUERY_TASK : "拥有" +``` + +图表来源 +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [backend/app/models/citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) +- [backend/app/models/query_task.py:11-39](file://backend/app/models/query_task.py#L11-L39) + +章节来源 +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [backend/app/models/citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) +- [backend/app/models/query_task.py:11-39](file://backend/app/models/query_task.py#L11-L39) + +### API与服务(API & Services) +- 查询API + - 支持分页列出、创建、查询详情、更新、删除查询。 +- 引用API + - 支持分页列出引用、统计查询、立即执行查询(触发任务)。 +- 服务层 + - 查询服务:校验用户配额、计算下次查询时间、增删改查。 + - 引用服务:权限校验、统计聚合、CSV导出、立即执行任务登记。 + +章节来源 +- [backend/app/api/queries.py:15-86](file://backend/app/api/queries.py#L15-L86) +- [backend/app/api/citations.py:25-77](file://backend/app/api/citations.py#L25-L77) +- [backend/app/services/query.py:12-130](file://backend/app/services/query.py#L12-L130) +- [backend/app/services/citation.py:24-269](file://backend/app/services/citation.py#L24-L269) + +## 依赖关系分析 +- 组件耦合 + - 引用检测引擎对Kimi适配器存在直接依赖;对品牌匹配器与竞争品牌检测器为组合关系。 + - 定时调度器仅通过引擎接口触发执行,解耦具体平台实现。 +- 外部依赖 + - Playwright浏览器驱动(需提前安装)。 + - PostgreSQL数据库(SQLAlchemy ORM)。 + - APScheduler(异步调度)。 +- 循环依赖 + - 未发现循环导入或调用链路。 + +```mermaid +graph LR +SCH["调度器"] --> ENG["引用检测引擎"] +ENG --> ADP["Kimi适配器"] +ENG --> BM["品牌匹配器"] +ENG --> CD["竞争品牌检测器"] +APIQ["查询API"] --> SVQ["查询服务"] +APIC["引用API"] --> SVC["引用服务"] +SVQ --> DB["数据库"] +SVC --> DB +``` + +图表来源 +- [backend/app/workers/scheduler.py:25-40](file://backend/app/workers/scheduler.py#L25-L40) +- [backend/app/workers/citation_engine.py:148-157](file://backend/app/workers/citation_engine.py#L148-L157) +- [backend/app/api/queries.py:15-86](file://backend/app/api/queries.py#L15-L86) +- [backend/app/api/citations.py:25-77](file://backend/app/api/citations.py#L25-L77) + +章节来源 +- [backend/app/workers/scheduler.py:25-40](file://backend/app/workers/scheduler.py#L25-L40) +- [backend/app/workers/citation_engine.py:148-157](file://backend/app/workers/citation_engine.py#L148-L157) +- [backend/app/api/queries.py:15-86](file://backend/app/api/queries.py#L15-L86) +- [backend/app/api/citations.py:25-77](file://backend/app/api/citations.py#L25-L77) + +## 性能考虑 +- 浏览器资源复用 + - 当前实现为每次查询新建上下文与页面,简单可靠但开销较大。可在高并发场景下评估复用策略(注意状态隔离)。 +- 等待策略优化 + - 输入框与发送按钮的等待时间可按环境调整;对稳定检测增加最大轮询次数上限,避免长时间阻塞。 +- 重试与退避 + - 已采用指数退避,建议结合平台可用性监控动态调整重试上限与初始延迟。 +- 数据库索引 + - 查询模型的索引设计有助于高频筛选;建议在引用记录表上针对查询时间、平台、是否引用等字段建立复合索引以提升统计与导出性能。 +- 前端平台映射 + - 前端平台键值映射清晰,便于UI展示与用户选择。 + +章节来源 +- [backend/app/workers/platforms/kimi.py:33-48](file://backend/app/workers/platforms/kimi.py#L33-L48) +- [backend/app/models/query.py:50-54](file://backend/app/models/query.py#L50-L54) +- [backend/app/models/citation_record.py:37-41](file://backend/app/models/citation_record.py#L37-L41) +- [frontend/lib/platforms.ts:1-18](file://frontend/lib/platforms.ts#L1-L18) + +## 故障排查指南 +- 浏览器未安装 + - 现象:启动浏览器时报错,提示未安装Playwright浏览器。 + - 处理:按照日志提示运行安装命令,确保Chromium可用。 +- 页面元素定位失败 + - 现象:无法找到输入框或发送按钮,抛出“无法找到输入框”错误。 + - 处理:检查页面结构变化,适当扩展选择器集合;确认页面加载完成后再查找元素。 +- 页面超时 + - 现象:导航或元素等待超时,返回当前文本或抛出超时异常。 + - 处理:适当提高超时阈值;检查网络状况与代理设置;必要时启用本地镜像站点。 +- 重试仍失败 + - 现象:多次重试后仍失败。 + - 处理:查看日志中的最后一次错误;检查平台访问限制与验证码弹窗;考虑降低并发或增加延时。 +- 资源未释放 + - 现象:长时间运行后内存占用上升。 + - 处理:确保每次查询后页面与上下文均已关闭;在应用退出时调用适配器关闭方法。 +- API调用失败 + - 现象:接口返回4xx/5xx错误。 + - 处理:核对鉴权头与请求体;检查用户权限与查询状态;查看服务端日志定位异常。 + +章节来源 +- [backend/app/workers/platforms/kimi.py:27-31](file://backend/app/workers/platforms/kimi.py#L27-L31) +- [backend/app/workers/platforms/kimi.py:87-88](file://backend/app/workers/platforms/kimi.py#L87-L88) +- [backend/app/workers/platforms/kimi.py:116-118](file://backend/app/workers/platforms/kimi.py#L116-L118) +- [backend/app/workers/platforms/kimi.py:198-206](file://backend/app/workers/platforms/kimi.py#L198-L206) +- [backend/app/api/queries.py:34-38](file://backend/app/api/queries.py#L34-L38) +- [backend/app/api/citations.py:67-71](file://backend/app/api/citations.py#L67-L71) + +## 结论 +Kimi适配器通过Playwright实现了对Kimi网页版的自动化交互,具备完善的错误重试、超时处理与资源管理机制。配合引用检测引擎与定时调度器,可稳定地完成跨平台的引用检测任务。建议在生产环境中关注浏览器资源复用、等待策略与数据库索引优化,并持续监控平台页面结构变化以保持适配器的健壮性。 + +## 附录 + +### API调用示例(路径参考) +- 创建查询 + - POST /api/v1/queries + - 请求体字段:keyword、target_brand、brand_aliases、platforms、frequency + - 成功响应:QueryResponse +- 立即执行查询 + - POST /api/v1/queries/{query_id}/run-now + - 成功响应:RunNowResponse(包含任务ID与状态) +- 查询引用列表 + - GET /api/v1/citations/?query_id={query_id}&platform={platform} + - 成功响应:CitationListResponse +- 引用统计 + - GET /api/v1/citations/stats?query_id={query_id} + - 成功响应:CitationStatsResponse + +章节来源 +- [backend/app/api/queries.py:26-39](file://backend/app/api/queries.py#L26-L39) +- [backend/app/api/citations.py:59-77](file://backend/app/api/citations.py#L59-L77) +- [backend/app/api/citations.py:25-56](file://backend/app/api/citations.py#L25-L56) \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/AI平台集成/平台适配器扩展指南.md b/.qoder/repowiki/zh/content/AI平台集成/平台适配器扩展指南.md new file mode 100644 index 0000000..8ba82c9 --- /dev/null +++ b/.qoder/repowiki/zh/content/AI平台集成/平台适配器扩展指南.md @@ -0,0 +1,408 @@ +# 平台适配器扩展指南 + + +**本文档引用的文件** +- [backend/app/workers/platforms/base.py](file://backend/app/workers/platforms/base.py) +- [backend/app/workers/platforms/kimi.py](file://backend/app/workers/platforms/kimi.py) +- [backend/app/workers/platforms/wenxin.py](file://backend/app/workers/platforms/wenxin.py) +- [backend/app/workers/citation_engine.py](file://backend/app/workers/citation_engine.py) +- [backend/app/workers/scheduler.py](file://backend/app/workers/scheduler.py) +- [backend/app/models/query.py](file://backend/app/models/query.py) +- [backend/app/config.py](file://backend/app/config.py) +- [frontend/lib/platforms.ts](file://frontend/lib/platforms.ts) +- [frontend/components/charts/platform-chart.tsx](file://frontend/components/charts/platform-chart.tsx) +- [frontend/lib/api.ts](file://frontend/lib/api.ts) +- [tests/test_citation_engine.py](file://tests/test_citation_engine.py) + + +## 目录 +1. [简介](#简介) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖分析](#依赖分析) +7. [性能考虑](#性能考虑) +8. [故障排查指南](#故障排查指南) +9. [结论](#结论) +10. [附录](#附录) + +## 简介 +本指南面向需要为系统新增AI平台适配器的开发者,提供从基类继承、实现必需方法、配置平台参数,到资源管理与性能优化的完整流程。文档同时覆盖两类平台适配策略:基于浏览器自动化(Playwright)的平台与基于API的平台,并给出测试方法、验证标准与常见问题排查建议。 + +## 项目结构 +后端采用分层架构,平台适配器位于 workers/platforms 子目录;引用检测引擎位于 workers 子目录;查询模型与调度器分别位于 models 与 workers 子目录;前端通过 lib 与 components 提供平台映射、图表展示与API封装。 + +```mermaid +graph TB +subgraph "后端" +CFG["配置
app/config.py"] +SCH["调度器
workers/scheduler.py"] +CE["引用引擎
workers/citation_engine.py"] +BP["适配器基类
workers/platforms/base.py"] +KIMI["Kimi适配器
workers/platforms/kimi.py"] +WENXIN["文心一言适配器
workers/platforms/wenxin.py"] +QRY["查询模型
models/query.py"] +end +subgraph "前端" +PFM["平台映射
lib/platforms.ts"] +API["API封装
lib/api.ts"] +CHART["平台图表
components/charts/platform-chart.tsx"] +end +SCH --> CE +CE --> KIMI +CE --> WENXIN +CE --> QRY +BP -. 继承 .- KIMI +BP -. 继承 .- WENXIN +PFM --> CHART +API --> SCH +``` + +**图示来源** +- [backend/app/workers/platforms/base.py:1-18](file://backend/app/workers/platforms/base.py#L1-L18) +- [backend/app/workers/platforms/kimi.py:1-206](file://backend/app/workers/platforms/kimi.py#L1-L206) +- [backend/app/workers/platforms/wenxin.py:1-205](file://backend/app/workers/platforms/wenxin.py#L1-L205) +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [frontend/lib/platforms.ts:1-18](file://frontend/lib/platforms.ts#L1-L18) +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) +- [frontend/components/charts/platform-chart.tsx:1-68](file://frontend/components/charts/platform-chart.tsx#L1-L68) + +**章节来源** +- [backend/app/workers/platforms/base.py:1-18](file://backend/app/workers/platforms/base.py#L1-L18) +- [backend/app/workers/platforms/kimi.py:1-206](file://backend/app/workers/platforms/kimi.py#L1-L206) +- [backend/app/workers/platforms/wenxin.py:1-205](file://backend/app/workers/platforms/wenxin.py#L1-L205) +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [frontend/lib/platforms.ts:1-18](file://frontend/lib/platforms.ts#L1-L18) +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) +- [frontend/components/charts/platform-chart.tsx:1-68](file://frontend/components/charts/platform-chart.tsx#L1-L68) + +## 核心组件 +- 适配器基类 BasePlatformAdapter:定义平台名称、平台URL以及抽象方法 query 与可选方法 close。 +- 具体适配器 KimiAdapter 与 WenxinAdapter:均继承自基类,实现 query 方法并通过 Playwright 自动化浏览器进行交互。 +- 引擎 CitationEngine:负责编排查询、调用适配器、执行品牌匹配与竞争品牌检测、持久化结果。 +- 调度器 QueryScheduler:周期性触发查询任务执行。 +- 查询模型 Query:承载关键词、目标品牌、别名、平台列表、频率与时间戳等元数据。 +- 配置 Settings:集中管理数据库、Redis、JWT、Playwright浏览器路径及第三方API密钥等。 + +**章节来源** +- [backend/app/workers/platforms/base.py:1-18](file://backend/app/workers/platforms/base.py#L1-L18) +- [backend/app/workers/platforms/kimi.py:1-206](file://backend/app/workers/platforms/kimi.py#L1-L206) +- [backend/app/workers/platforms/wenxin.py:1-205](file://backend/app/workers/platforms/wenxin.py#L1-L205) +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) + +## 架构总览 +下图展示了从调度器到引擎再到具体适配器的调用链路,以及前端如何消费平台映射与统计数据。 + +```mermaid +sequenceDiagram +participant S as "调度器
workers/scheduler.py" +participant E as "引擎
workers/citation_engine.py" +participant A as "适配器
Kimi/Wenxin" +participant DB as "数据库
SQLAlchemy" +S->>E : 触发执行查询 +E->>DB : 查询待执行的 Query +loop 遍历平台 +E->>A : 调用 query(keyword) +A-->>E : 返回原始响应文本 +E->>E : 品牌匹配/竞争品牌检测 +E->>DB : 写入引用记录与任务状态 +end +E-->>S : 返回本次执行结果 +``` + +**图示来源** +- [backend/app/workers/scheduler.py:51-84](file://backend/app/workers/scheduler.py#L51-L84) +- [backend/app/workers/citation_engine.py:159-234](file://backend/app/workers/citation_engine.py#L159-L234) +- [backend/app/workers/platforms/kimi.py:33-48](file://backend/app/workers/platforms/kimi.py#L33-L48) +- [backend/app/workers/platforms/wenxin.py:33-48](file://backend/app/workers/platforms/wenxin.py#L33-L48) + +## 详细组件分析 + +### 基类与继承规范 +- 必须实现的方法 + - query(keyword: str) -> str:在指定平台查询关键词并返回原始响应文本。 +- 可选方法 + - close():释放浏览器或网络连接等资源。 +- 平台元信息 + - platform_name:平台标识字符串。 + - platform_url:平台主页URL。 + +```mermaid +classDiagram +class BasePlatformAdapter { ++string platform_name ++string platform_url ++query(keyword) str ++close() void +} +class KimiAdapter { ++platform_name = "kimi" ++platform_url = "https : //kimi.moonshot.cn" ++query(keyword) str ++close() void +} +class WenxinAdapter { ++platform_name = "wenxin" ++platform_url = "https : //yiyan.baidu.com" ++query(keyword) str ++close() void +} +KimiAdapter --|> BasePlatformAdapter +WenxinAdapter --|> BasePlatformAdapter +``` + +**图示来源** +- [backend/app/workers/platforms/base.py:4-17](file://backend/app/workers/platforms/base.py#L4-L17) +- [backend/app/workers/platforms/kimi.py:11-20](file://backend/app/workers/platforms/kimi.py#L11-L20) +- [backend/app/workers/platforms/wenxin.py:11-20](file://backend/app/workers/platforms/wenxin.py#L11-L20) + +**章节来源** +- [backend/app/workers/platforms/base.py:4-17](file://backend/app/workers/platforms/base.py#L4-L17) + +### 浏览器驱动型平台适配策略(以Kimi/Wenxin为例) +- 启动与重用浏览器:首次调用时初始化 Playwright 并启动 Chromium 浏览器,避免重复创建。 +- 页面交互:导航至平台URL,等待并定位输入框,填充关键词,提交(回车或点击发送按钮)。 +- 稳定性检测:持续轮询消息容器,当文本连续多次保持一致时判定回复稳定,返回内容。 +- 错误处理:捕获超时与异常,记录日志并抛出可诊断的错误;提供指数退避重试机制。 +- 资源管理:在 finally 中确保 page/context 关闭;close()统一释放浏览器与Playwright实例。 + +```mermaid +flowchart TD +Start(["进入 query"]) --> Ensure["确保浏览器已启动"] +Ensure --> TryDo["_do_query 尝试一次查询"] +TryDo --> Stable["等待回复稳定"] +Stable --> Done["返回原始响应文本"] +TryDo --> |异常| Retry{"重试次数 < 3?"} +Retry --> |是| Backoff["指数退避等待"] --> TryDo +Retry --> |否| Raise["抛出最终异常"] +Done --> Close["关闭 page/context"] +Raise --> Close +Close --> End(["结束"]) +``` + +**图示来源** +- [backend/app/workers/platforms/kimi.py:21-48](file://backend/app/workers/platforms/kimi.py#L21-L48) +- [backend/app/workers/platforms/kimi.py:50-124](file://backend/app/workers/platforms/kimi.py#L50-L124) +- [backend/app/workers/platforms/kimi.py:126-196](file://backend/app/workers/platforms/kimi.py#L126-L196) +- [backend/app/workers/platforms/kimi.py:198-205](file://backend/app/workers/platforms/kimi.py#L198-L205) +- [backend/app/workers/platforms/wenxin.py:21-48](file://backend/app/workers/platforms/wenxin.py#L21-L48) +- [backend/app/workers/platforms/wenxin.py:50-123](file://backend/app/workers/platforms/wenxin.py#L50-L123) +- [backend/app/workers/platforms/wenxin.py:124-195](file://backend/app/workers/platforms/wenxin.py#L124-L195) +- [backend/app/workers/platforms/wenxin.py:197-204](file://backend/app/workers/platforms/wenxin.py#L197-L204) + +**章节来源** +- [backend/app/workers/platforms/kimi.py:1-206](file://backend/app/workers/platforms/kimi.py#L1-L206) +- [backend/app/workers/platforms/wenxin.py:1-205](file://backend/app/workers/platforms/wenxin.py#L1-L205) + +### API型平台适配策略(扩展指导) +- 继承基类并实现 query:通过HTTP客户端发起请求,解析JSON响应,提取平台返回的文本内容。 +- 错误处理:区分网络错误、平台限流/鉴权失败、解析异常等,统一转换为可诊断的异常类型。 +- 资源管理:若使用连接池或长连接,实现 close() 以释放连接;否则可为空实现。 +- 配置参数:在 Settings 中添加平台API密钥与基础URL,前端与后端分别读取使用。 + +(本小节为概念性指导,不直接分析具体文件) + +### 引擎与品牌检测 +- CitationEngine 负责: + - 初始化各平台适配器实例并注册到平台字典。 + - 针对每个 Query 的平台列表依次执行查询。 + - 使用 BrandMatcher 执行精确/别名/模糊匹配,返回置信度与位置信息。 + - 使用 CompetitorDetector 检测文本中出现的竞争品牌集合。 + - 记录 CitationRecord 并更新 Query 的时间戳与下次查询时间。 +- 资源关闭:在 close() 中遍历适配器逐一关闭,避免资源泄漏。 + +```mermaid +classDiagram +class CitationEngine { ++dict platforms ++BrandMatcher matcher ++CompetitorDetector competitor_detector ++execute_query(query, db) list ++execute_single_platform(keyword, platform, ...) dict ++close() void +} +class BrandMatcher { ++match(text) dict +} +class CompetitorDetector { ++detect(text, target_brand) list +} +CitationEngine --> BrandMatcher : "使用" +CitationEngine --> CompetitorDetector : "使用" +``` + +**图示来源** +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) +- [backend/app/workers/citation_engine.py:19-120](file://backend/app/workers/citation_engine.py#L19-L120) +- [backend/app/workers/citation_engine.py:122-146](file://backend/app/workers/citation_engine.py#L122-L146) + +**章节来源** +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) + +### 调度与执行 +- QueryScheduler 使用 APScheduler 定时触发检查与执行逻辑,每小时扫描一次到期的查询任务。 +- 对每个查询任务,CitationEngine.execute_query 会: + - 创建或获取 QueryTask 并更新状态为 running。 + - 遍历平台列表执行查询与检测。 + - 记录结果并更新 Query 的 last_queried_at 与 next_query_at。 + - 若失败则记录错误并写入一条 cited=False 的占位记录。 +- 关闭时统一调用 engine.close() 释放适配器资源。 + +```mermaid +sequenceDiagram +participant SCH as "调度器" +participant CE as "引擎" +participant DB as "数据库" +SCH->>CE : check_and_execute_queries() +CE->>DB : 查询 active 且到期的 Query +loop 遍历查询 +CE->>DB : 获取/创建 QueryTask +CE->>CE : 执行平台查询与检测 +CE->>DB : 写入 CitationRecord 与更新 Query +end +SCH->>CE : shutdown() +CE->>CE : close() +``` + +**图示来源** +- [backend/app/workers/scheduler.py:30-90](file://backend/app/workers/scheduler.py#L30-L90) +- [backend/app/workers/citation_engine.py:159-234](file://backend/app/workers/citation_engine.py#L159-L234) + +**章节来源** +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) + +### 前端集成与展示 +- 平台映射:前端 lib/platforms.ts 定义平台键到中文标签的映射,用于UI显示与图表标签。 +- 图表组件:components/charts/platform-chart.tsx 接收平台统计数据,渲染引用率柱状图。 +- API封装:frontend/lib/api.ts 提供认证、查询、引用与报表导出等接口封装,便于前端调用。 + +```mermaid +graph LR +PFM["平台映射
lib/platforms.ts"] --> CHART["平台图表
components/charts/platform-chart.tsx"] +API["API封装
lib/api.ts"] --> SCH["调度器
workers/scheduler.py"] +API --> CE["引擎
workers/citation_engine.py"] +``` + +**图示来源** +- [frontend/lib/platforms.ts:1-18](file://frontend/lib/platforms.ts#L1-L18) +- [frontend/components/charts/platform-chart.tsx:34-39](file://frontend/components/charts/platform-chart.tsx#L34-L39) +- [frontend/lib/api.ts:23-57](file://frontend/lib/api.ts#L23-L57) + +**章节来源** +- [frontend/lib/platforms.ts:1-18](file://frontend/lib/platforms.ts#L1-L18) +- [frontend/components/charts/platform-chart.tsx:1-68](file://frontend/components/charts/platform-chart.tsx#L1-L68) +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) + +## 依赖分析 +- 组件耦合 + - CitationEngine 与具体适配器松耦合:通过平台字典注册与动态查找,便于扩展新平台。 + - 调度器与引擎解耦:调度器仅负责触发,引擎负责业务逻辑与平台交互。 +- 外部依赖 + - Playwright:用于浏览器自动化(Kimi/Wenxin)。 + - SQLAlchemy:用于数据库访问与事务控制。 + - APScheduler:用于定时任务调度。 + - Pydantic Settings:用于配置管理。 + +```mermaid +graph TB +CE["CitationEngine"] --> KIMI["KimiAdapter"] +CE --> WENXIN["WenxinAdapter"] +CE --> DB["SQLAlchemy"] +SCH["QueryScheduler"] --> CE +CE --> CFG["Settings"] +KIMI --> PW["Playwright"] +WENXIN --> PW +``` + +**图示来源** +- [backend/app/workers/citation_engine.py:152-155](file://backend/app/workers/citation_engine.py#L152-L155) +- [backend/app/workers/scheduler.py:26-28](file://backend/app/workers/scheduler.py#L26-L28) +- [backend/app/config.py:11-14](file://backend/app/config.py#L11-L14) +- [backend/app/workers/platforms/kimi.py:4-6](file://backend/app/workers/platforms/kimi.py#L4-L6) +- [backend/app/workers/platforms/wenxin.py:4-6](file://backend/app/workers/platforms/wenxin.py#L4-L6) + +**章节来源** +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [backend/app/workers/platforms/kimi.py:1-206](file://backend/app/workers/platforms/kimi.py#L1-L206) +- [backend/app/workers/platforms/wenxin.py:1-205](file://backend/app/workers/platforms/wenxin.py#L1-L205) + +## 性能考虑 +- 浏览器复用:避免每次查询都启动/关闭浏览器,减少冷启动开销。 +- 稳定性检测阈值:合理设置“连续稳定”次数与轮询间隔,平衡准确性和延迟。 +- 指数退避:在重试时采用指数退避,降低平台压力并提升成功率。 +- 数据库批处理:批量写入引用记录与任务状态,减少IO往返。 +- 超时与并发:为页面导航、元素等待与响应稳定检测设置合理超时,避免长时间阻塞。 +- 缓存与预热:可在进程启动时预热部分资源(如Playwright浏览器),缩短首次查询耗时。 + +(本节为通用建议,不直接分析具体文件) + +## 故障排查指南 +- 浏览器相关 + - 现象:启动浏览器失败或找不到输入框。 + - 排查:确认已安装 Playwright 浏览器;检查平台URL可达性;核对页面选择器是否随平台更新而变更。 + - 参考实现:Kimi/Wenxin 适配器在启动失败时抛出明确提示,需按提示执行安装命令。 +- 超时与不稳定 + - 现象:页面操作超时或回复未稳定。 + - 排查:适当提高等待超时与稳定检测阈值;检查网络环境与平台负载。 + - 参考实现:适配器内对超时与异常进行捕获并记录日志。 +- 品牌匹配 + - 现象:匹配结果不符合预期。 + - 排查:调整别名列表、提高模糊匹配阈值或优化候选词提取逻辑。 + - 参考实现:测试用例覆盖精确、别名、模糊与无匹配场景。 +- 调度与状态 + - 现象:查询未按时执行或状态异常。 + - 排查:检查调度器是否启动、数据库连接与时区设置、Query 的 next_query_at 是否正确更新。 +- 前端展示 + - 现象:平台图表标签显示异常。 + - 排查:确认前端平台映射与后端平台键一致。 + +**章节来源** +- [backend/app/workers/platforms/kimi.py:27-31](file://backend/app/workers/platforms/kimi.py#L27-L31) +- [backend/app/workers/platforms/kimi.py:87-88](file://backend/app/workers/platforms/kimi.py#L87-L88) +- [backend/app/workers/platforms/kimi.py:116-118](file://backend/app/workers/platforms/kimi.py#L116-L118) +- [backend/app/workers/platforms/wenxin.py:27-31](file://backend/app/workers/platforms/wenxin.py#L27-L31) +- [backend/app/workers/platforms/wenxin.py:86-87](file://backend/app/workers/platforms/wenxin.py#L86-L87) +- [backend/app/workers/platforms/wenxin.py:114-116](file://backend/app/workers/platforms/wenxin.py#L114-L116) +- [tests/test_citation_engine.py:6-53](file://tests/test_citation_engine.py#L6-L53) +- [backend/app/workers/scheduler.py:30-40](file://backend/app/workers/scheduler.py#L30-L40) +- [backend/app/workers/citation_engine.py:229-232](file://backend/app/workers/citation_engine.py#L229-L232) +- [frontend/lib/platforms.ts:1-18](file://frontend/lib/platforms.ts#L1-L18) + +## 结论 +通过继承 BasePlatformAdapter 并遵循本文提供的实现规范与最佳实践,可以快速、安全地为系统接入新的AI平台。对于浏览器驱动型平台,重点在于稳定的选择器策略与资源管理;对于API型平台,重点在于健壮的错误处理与配置管理。配合引擎的品牌检测能力与调度器的自动化执行,可构建高可用的跨平台引用检测体系。 + +## 附录 + +### 新平台接入步骤清单 +- 继承基类并实现必需方法 + - 在 workers/platforms 下新建适配器文件,继承 BasePlatformAdapter。 + - 实现 query 与可选的 close。 + - 设置 platform_name 与 platform_url。 +- 注册到引擎 + - 在 CitationEngine 的平台字典中注册新适配器实例。 +- 配置与密钥 + - 在 Settings 中添加必要的配置项(如API密钥、基础URL)。 +- 前端集成 + - 在前端 lib/platforms.ts 中添加平台键与中文标签。 + - 如需展示统计,确保后端返回的数据结构与前端图表组件兼容。 +- 测试与验证 + - 编写单元测试覆盖品牌匹配与竞争品牌检测逻辑。 + - 进行端到端测试,验证调度器触发、引擎执行与数据库写入。 +- 性能与稳定性 + - 评估并优化超时、重试与稳定检测策略。 + - 监控日志与错误指标,持续改进。 + +(本节为流程性说明,不直接分析具体文件) \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/AI平台集成/引用检测算法.md b/.qoder/repowiki/zh/content/AI平台集成/引用检测算法.md new file mode 100644 index 0000000..b51fd09 --- /dev/null +++ b/.qoder/repowiki/zh/content/AI平台集成/引用检测算法.md @@ -0,0 +1,477 @@ +# 引用检测算法 + + +**本文档引用的文件** +- [citation_engine.py](file://backend/app/workers/citation_engine.py) +- [base.py](file://backend/app/workers/platforms/base.py) +- [kimi.py](file://backend/app/workers/platforms/kimi.py) +- [wenxin.py](file://backend/app/workers/platforms/wenxin.py) +- [citation_record.py](file://backend/app/models/citation_record.py) +- [query.py](file://backend/app/models/query.py) +- [query_task.py](file://backend/app/models/query_task.py) +- [citation.py](file://backend/app/services/citation.py) +- [scheduler.py](file://backend/app/workers/scheduler.py) +- [citations.py](file://backend/app/api/citations.py) +- [config.py](file://backend/app/config.py) +- [database.py](file://backend/app/database.py) +- [test_citation_engine.py](file://tests/test_citation_engine.py) + + +## 目录 +1. [简介](#简介) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖关系分析](#依赖关系分析) +7. [性能考虑](#性能考虑) +8. [故障排查指南](#故障排查指南) +9. [结论](#结论) +10. [附录](#附录) + +## 简介 +本文件系统性阐述引用检测算法的设计与实现,重点覆盖以下方面: +- 品牌匹配策略:精确匹配、别名匹配、模糊匹配的判定逻辑与置信度评分 +- 竞争品牌识别机制:基于预定义品牌库的竞争者发现 +- 置信度评分系统:关键词匹配权重、上下文相关性评估与结果排序规则 +- 引用上下文提取技术:文本片段截取、语义分析与相关性判断 +- 算法优化策略:性能提升与准确性改进方法 +- 算法调优指南与实际应用场景示例 + +## 项目结构 +后端采用分层架构,围绕“查询-执行-记录-统计”闭环组织: +- 查询模型与任务模型负责用户查询配置与执行计划 +- 引擎模块负责跨平台调用、品牌匹配与竞争品牌识别 +- 适配器模块封装不同AI平台的网页自动化交互 +- 服务模块提供API接口、统计数据与导出功能 +- 定时调度器按频率自动触发查询任务 + +```mermaid +graph TB +subgraph "查询与任务" +Q["Query
查询配置"] +QT["QueryTask
查询任务"] +end +subgraph "引擎与适配器" +CE["CitationEngine
引用检测引擎"] +BM["BrandMatcher
品牌匹配器"] +CD["CompetitorDetector
竞争品牌检测器"] +KP["KimiAdapter
Kimi适配器"] +WP["WenxinAdapter
文心一言适配器"] +end +subgraph "数据与服务" +CR["CitationRecord
引用记录"] +SVC["Citation Services
统计与导出"] +SCH["QueryScheduler
定时调度器"] +end +Q --> CE +Q --> QT +CE --> BM +CE --> CD +CE --> KP +CE --> WP +CE --> CR +SVC --> CR +SCH --> CE +``` + +图表来源 +- [citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) +- [query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [query_task.py:11-39](file://backend/app/models/query_task.py#L11-L39) +- [citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) +- [kimi.py:11-206](file://backend/app/workers/platforms/kimi.py#L11-L206) +- [wenxin.py:11-205](file://backend/app/workers/platforms/wenxin.py#L11-L205) +- [scheduler.py:25-95](file://backend/app/workers/scheduler.py#L25-L95) +- [citation.py:24-269](file://backend/app/services/citation.py#L24-L269) + +章节来源 +- [citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) +- [query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [query_task.py:11-39](file://backend/app/models/query_task.py#L11-L39) +- [citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) +- [kimi.py:11-206](file://backend/app/workers/platforms/kimi.py#L11-L206) +- [wenxin.py:11-205](file://backend/app/workers/platforms/wenxin.py#L11-L205) +- [scheduler.py:25-95](file://backend/app/workers/scheduler.py#L25-L95) +- [citation.py:24-269](file://backend/app/services/citation.py#L24-L269) + +## 核心组件 +- 引用检测引擎:协调平台查询、品牌匹配与竞争品牌识别,并持久化结果 +- 品牌匹配器:提供精确、别名与模糊三种匹配策略,输出置信度与上下文 +- 竞争品牌检测器:基于预定义品牌库识别文本中的竞争者 +- 平台适配器:封装Kimi与文心一言的网页自动化查询流程 +- 数据模型:查询、查询任务、引用记录支撑业务数据流转 +- 统计服务:提供引用统计、趋势与CSV导出能力 +- 定时调度器:按频率自动触发查询任务 + +章节来源 +- [citation_engine.py:19-120](file://backend/app/workers/citation_engine.py#L19-L120) +- [citation_engine.py:122-146](file://backend/app/workers/citation_engine.py#L122-L146) +- [citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) +- [base.py:4-18](file://backend/app/workers/platforms/base.py#L4-L18) +- [kimi.py:11-206](file://backend/app/workers/platforms/kimi.py#L11-L206) +- [wenxin.py:11-205](file://backend/app/workers/platforms/wenxin.py#L11-L205) +- [query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [query_task.py:11-39](file://backend/app/models/query_task.py#L11-L39) +- [citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) +- [citation.py:76-201](file://backend/app/services/citation.py#L76-L201) +- [scheduler.py:25-95](file://backend/app/workers/scheduler.py#L25-L95) + +## 架构总览 +下图展示从查询配置到结果统计的完整流程,包括定时触发、平台查询、匹配与记录写入。 + +```mermaid +sequenceDiagram +participant User as "用户" +participant API as "API层" +participant Svc as "统计服务" +participant Sch as "调度器" +participant Eng as "引用检测引擎" +participant Plat as "平台适配器" +participant DB as "数据库" +User->>API : 触发查询/查看统计 +API->>Svc : 获取引用记录/统计 +Svc->>DB : 查询引用记录 +DB-->>Svc : 返回记录 +Svc-->>API : 返回统计结果 +Sch->>DB : 查询到期的查询 +DB-->>Sch : 返回查询集合 +Sch->>Eng : 执行查询 +Eng->>Plat : 平台查询(keyword) +Plat-->>Eng : 返回原始响应 +Eng->>Eng : 品牌匹配/竞争品牌识别 +Eng->>DB : 写入引用记录 +DB-->>Eng : 确认写入 +Eng-->>Sch : 返回执行结果 +``` + +图表来源 +- [scheduler.py:51-84](file://backend/app/workers/scheduler.py#L51-L84) +- [citation_engine.py:159-234](file://backend/app/workers/citation_engine.py#L159-L234) +- [kimi.py:33-48](file://backend/app/workers/platforms/kimi.py#L33-L48) +- [wenxin.py:33-48](file://backend/app/workers/platforms/wenxin.py#L33-L48) +- [citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) +- [citation.py:24-73](file://backend/app/services/citation.py#L24-L73) + +## 详细组件分析 + +### 品牌匹配器(BrandMatcher) +- 精确匹配:直接包含目标品牌或别名即命中,置信度最高 +- 别名匹配:对别名列表逐一匹配,命中则置信度较高 +- 模糊匹配:基于候选词集合与编辑相似度阈值进行匹配,置信度由相似度决定 +- 上下文提取:按段落定位首次出现位置,截取固定长度片段作为引用上下文 +- 结果字段:是否引用、置信度、匹配类型、段落位置、引用上下文 + +```mermaid +flowchart TD +Start(["进入匹配"]) --> CheckEmpty{"文本为空?"} +CheckEmpty --> |是| ReturnNoMatch["返回未命中"] +CheckEmpty --> |否| Exact["精确匹配"] +Exact --> ExactHit{"命中?"} +ExactHit --> |是| ReturnExact["返回精确匹配结果"] +ExactHit --> |否| Alias["别名匹配"] +Alias --> AliasHit{"命中?"} +AliasHit --> |是| ReturnAlias["返回别名匹配结果"] +AliasHit --> |否| Fuzzy["模糊匹配"] +Fuzzy --> Extract["提取候选词"] +Extract --> Similarity["计算相似度"] +Similarity --> Best{"最佳相似度>阈值?"} +Best --> |是| ReturnFuzzy["返回模糊匹配结果"] +Best --> |否| ReturnNoMatch +``` + +图表来源 +- [citation_engine.py:26-120](file://backend/app/workers/citation_engine.py#L26-L120) + +章节来源 +- [citation_engine.py:19-120](file://backend/app/workers/citation_engine.py#L19-L120) + +### 竞争品牌检测器(CompetitorDetector) +- 基于预定义行业分类的品牌集合进行全量扫描 +- 排除目标品牌,返回去重后的竞争品牌列表 +- 支持多行业类别扩展,便于后续维护与增长 + +```mermaid +flowchart TD +Start(["开始检测"]) --> Empty{"文本为空?"} +Empty --> |是| ReturnEmpty["返回空列表"] +Empty --> |否| LoopCat["遍历行业类别"] +LoopCat --> LoopBrand["遍历类别内品牌"] +LoopBrand --> Exclude{"是否为目标品牌?"} +Exclude --> |是| NextBrand["跳过"] +Exclude --> |否| CheckInText{"品牌是否出现在文本?"} +CheckInText --> |是| Add["加入候选集"] +CheckInText --> |否| NextBrand +NextBrand --> LoopBrand +LoopBrand --> Done{"遍历结束?"} +Done --> |否| LoopCat +Done --> |是| Sort["排序并返回"] +``` + +图表来源 +- [citation_engine.py:122-146](file://backend/app/workers/citation_engine.py#L122-L146) + +章节来源 +- [citation_engine.py:122-146](file://backend/app/workers/citation_engine.py#L122-L146) + +### 引用检测引擎(CitationEngine) +- 单平台执行:获取适配器、发起查询、执行匹配与竞争品牌识别 +- 多平台执行:遍历配置平台,维护任务状态,持久化结果 +- 时间控制:根据频率计算下次查询时间,避免重复触发 +- 错误处理:捕获异常并记录失败任务与错误信息 + +```mermaid +sequenceDiagram +participant Q as "Query" +participant CE as "CitationEngine" +participant AD as "平台适配器" +participant BM as "BrandMatcher" +participant CD as "CompetitorDetector" +participant DB as "数据库" +CE->>Q : 读取配置关键词/品牌/别名/平台/频率 +loop 遍历平台 +CE->>AD : query(keyword) +AD-->>CE : raw_response +CE->>BM : match(raw_response) +BM-->>CE : 匹配结果 +CE->>CD : detect(raw_response, target_brand) +CD-->>CE : 竞争品牌列表 +CE->>DB : 写入CitationRecord +end +CE->>Q : 更新last_queried_at/next_query_at +``` + +图表来源 +- [citation_engine.py:159-234](file://backend/app/workers/citation_engine.py#L159-L234) +- [citation_engine.py:236-266](file://backend/app/workers/citation_engine.py#L236-L266) + +章节来源 +- [citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) + +### 平台适配器(BasePlatformAdapter/Kimi/Wenxin) +- 抽象基类定义统一接口:平台名称、URL与查询方法 +- 具体适配器通过Playwright自动化访问平台页面,输入关键词并等待稳定回复 +- 提供指数退避重试、超时处理与资源清理 + +```mermaid +classDiagram +class BasePlatformAdapter { ++string platform_name ++string platform_url ++query(keyword) str ++close() +} +class KimiAdapter { ++query(keyword) str ++close() +} +class WenxinAdapter { ++query(keyword) str ++close() +} +BasePlatformAdapter <|-- KimiAdapter +BasePlatformAdapter <|-- WenxinAdapter +``` + +图表来源 +- [base.py:4-18](file://backend/app/workers/platforms/base.py#L4-L18) +- [kimi.py:11-206](file://backend/app/workers/platforms/kimi.py#L11-L206) +- [wenxin.py:11-205](file://backend/app/workers/platforms/wenxin.py#L11-L205) + +章节来源 +- [base.py:4-18](file://backend/app/workers/platforms/base.py#L4-L18) +- [kimi.py:11-206](file://backend/app/workers/platforms/kimi.py#L11-L206) +- [wenxin.py:11-205](file://backend/app/workers/platforms/wenxin.py#L11-L205) + +### 数据模型与统计服务 +- 查询模型:存储关键词、目标品牌、别名、平台、频率与时间控制字段 +- 查询任务模型:跟踪每次执行的状态、错误信息与时间戳 +- 引用记录模型:保存是否引用、引用位置、引用文本、竞争品牌与原始响应 +- 统计服务:提供总量、引用率、平均位置、按平台分布与近30天趋势 + +```mermaid +erDiagram +QUERY { +uuid id PK +uuid user_id FK +string keyword +string target_brand +jsonb brand_aliases +jsonb platforms +string frequency +string status +datetime last_queried_at +datetime next_query_at +} +QUERY_TASK { +uuid id PK +uuid query_id FK +string platform +string status +text error_message +datetime scheduled_at +datetime started_at +datetime completed_at +} +CITATION_RECORD { +uuid id PK +uuid query_id FK +string platform +boolean cited +int citation_position +text citation_text +json competitor_brands +text raw_response +datetime queried_at +} +QUERY ||--o{ QUERY_TASK : "包含" +QUERY ||--o{ CITATION_RECORD : "包含" +``` + +图表来源 +- [query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [query_task.py:11-39](file://backend/app/models/query_task.py#L11-L39) +- [citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) + +章节来源 +- [query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [query_task.py:11-39](file://backend/app/models/query_task.py#L11-L39) +- [citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) +- [citation.py:76-201](file://backend/app/services/citation.py#L76-L201) + +### 定时调度器与API +- 定时调度器:每小时检查到期查询并执行 +- API层:提供引用列表、统计与立即执行接口 + +```mermaid +sequenceDiagram +participant SCH as "调度器" +participant DB as "数据库" +participant CE as "引擎" +participant API as "API" +participant SVC as "服务" +SCH->>DB : 查询到期查询 +DB-->>SCH : 返回查询集合 +SCH->>CE : 执行查询 +CE-->>DB : 写入记录 +API->>SVC : 获取统计/列表 +SVC->>DB : 查询数据 +DB-->>SVC : 返回结果 +SVC-->>API : 返回统计/列表 +``` + +图表来源 +- [scheduler.py:51-84](file://backend/app/workers/scheduler.py#L51-L84) +- [citations.py:25-77](file://backend/app/api/citations.py#L25-L77) +- [citation.py:24-73](file://backend/app/services/citation.py#L24-L73) + +章节来源 +- [scheduler.py:25-95](file://backend/app/workers/scheduler.py#L25-L95) +- [citations.py:25-77](file://backend/app/api/citations.py#L25-L77) +- [citation.py:24-269](file://backend/app/services/citation.py#L24-L269) + +## 依赖关系分析 +- 引擎依赖适配器接口,通过平台名称映射具体实现 +- 引擎依赖数据库模型进行任务与结果持久化 +- 统计服务依赖查询与记录模型进行聚合 +- 调度器依赖引擎与数据库进行周期性执行 + +```mermaid +graph LR +CE["CitationEngine"] --> BM["BrandMatcher"] +CE --> CD["CompetitorDetector"] +CE --> KP["KimiAdapter"] +CE --> WP["WenxinAdapter"] +CE --> CR["CitationRecord"] +CE --> Q["Query"] +CE --> QT["QueryTask"] +SCH["QueryScheduler"] --> CE +SVC["Citation Services"] --> CR +API["API"] --> SVC +``` + +图表来源 +- [citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) +- [scheduler.py:25-95](file://backend/app/workers/scheduler.py#L25-L95) +- [citation.py:24-269](file://backend/app/services/citation.py#L24-L269) + +章节来源 +- [citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) +- [scheduler.py:25-95](file://backend/app/workers/scheduler.py#L25-L95) +- [citation.py:24-269](file://backend/app/services/citation.py#L24-L269) + +## 性能考虑 +- 并发与重试:平台适配器采用指数退避重试,降低网络波动影响 +- 资源管理:适配器在finally中释放浏览器与上下文,避免资源泄漏 +- 数据库索引:对查询与记录的关键字段建立索引,加速统计与筛选 +- 异步执行:引擎与调度器均采用异步模式,提高吞吐量 +- 上下文截取:限定片段长度,减少存储与传输开销 + +章节来源 +- [kimi.py:33-48](file://backend/app/workers/platforms/kimi.py#L33-L48) +- [wenxin.py:33-48](file://backend/app/workers/platforms/wenxin.py#L33-L48) +- [citation_record.py:37-41](file://backend/app/models/citation_record.py#L37-L41) +- [database.py:1-29](file://backend/app/database.py#L1-L29) + +## 故障排查指南 +- 平台适配器初始化失败:检查Playwright浏览器安装与路径配置 +- 页面元素定位失败:适配器内置多种选择器与超时处理,可关注日志中的选择器尝试顺序 +- 引擎执行异常:查看任务状态与错误信息,确认查询配置与平台可用性 +- 统计结果异常:核对查询所有权验证与时间范围过滤条件 + +章节来源 +- [kimi.py:21-32](file://backend/app/workers/platforms/kimi.py#L21-L32) +- [wenxin.py:21-32](file://backend/app/workers/platforms/wenxin.py#L21-L32) +- [citation_engine.py:211-227](file://backend/app/workers/citation_engine.py#L211-L227) +- [citation.py:14-21](file://backend/app/services/citation.py#L14-L21) + +## 结论 +该引用检测算法以清晰的分层设计实现了从平台查询到品牌匹配与统计分析的完整链路。通过精确、别名与模糊匹配相结合的策略,辅以上下文提取与竞争品牌识别,能够有效支撑品牌监测与竞品分析场景。建议在生产环境中结合业务需求持续优化阈值与品牌库,并完善监控与告警体系。 + +## 附录 + +### 置信度评分系统 +- 精确匹配:置信度为最高值 +- 别名匹配:置信度为较高值 +- 模糊匹配:置信度由编辑相似度决定,超过阈值方可视为命中 +- 上下文相关性:通过段落位置与片段长度间接反映相关性 + +章节来源 +- [citation_engine.py:26-100](file://backend/app/workers/citation_engine.py#L26-L100) + +### 引用上下文提取技术 +- 文本分段:按换行符拆分段落,定位首次出现位置 +- 片段截取:限定最大长度,保证上下文可读性与性能 +- 相关性判断:结合段落位置与关键词密度进行粗略评估 + +章节来源 +- [citation_engine.py:107-119](file://backend/app/workers/citation_engine.py#L107-L119) + +### 算法优化策略 +- 性能提升:异步并发、指数退避重试、资源及时释放 +- 准确性改进:调整模糊匹配阈值、扩展品牌别名库、引入更细粒度的上下文特征 +- 可靠性增强:完善错误分类与日志记录、增加健康检查与降级策略 + +章节来源 +- [kimi.py:33-48](file://backend/app/workers/platforms/kimi.py#L33-L48) +- [wenxin.py:33-48](file://backend/app/workers/platforms/wenxin.py#L33-L48) +- [citation_engine.py:291-300](file://backend/app/workers/citation_engine.py#L291-L300) + +### 算法调优指南 +- 调整阈值:根据业务反馈微调模糊匹配阈值与置信度边界 +- 品牌库维护:定期更新行业品牌清单与别名,提升识别覆盖率 +- 上下文长度:根据下游应用需求调整片段长度,平衡信息量与性能 +- 平台选择:针对不同关键词特性选择更适合的平台,必要时并行执行取并集 + +章节来源 +- [test_citation_engine.py:6-54](file://tests/test_citation_engine.py#L6-L54) +- [citation_engine.py:126-130](file://backend/app/workers/citation_engine.py#L126-L130) + +### 实际应用场景示例 +- 品牌监测:对目标品牌进行周期性监测,追踪提及次数与趋势 +- 竞品分析:识别文本中的竞争品牌,辅助市场情报收集 +- 舆情预警:结合置信度与上下文,筛选高风险或高热度提及 + +章节来源 +- [citation.py:76-201](file://backend/app/services/citation.py#L76-L201) +- [citations.py:25-77](file://backend/app/api/citations.py#L25-L77) \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/AI平台集成/文心平台集成.md b/.qoder/repowiki/zh/content/AI平台集成/文心平台集成.md new file mode 100644 index 0000000..53dbd66 --- /dev/null +++ b/.qoder/repowiki/zh/content/AI平台集成/文心平台集成.md @@ -0,0 +1,387 @@ +# 文心平台集成 + + +**本文档引用的文件** +- [wenxin.py](file://backend/app/workers/platforms/wenxin.py) +- [base.py](file://backend/app/workers/platforms/base.py) +- [citation_engine.py](file://backend/app/workers/citation_engine.py) +- [config.py](file://backend/app/config.py) +- [query.py](file://backend/app/models/query.py) +- [queries.py](file://backend/app/api/queries.py) +- [Dockerfile](file://backend/Dockerfile) +- [test_citations.py](file://tests/test_citations.py) + + +## 目录 +1. [简介](#简介) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖关系分析](#依赖关系分析) +7. [性能考虑](#性能考虑) +8. [故障排查指南](#故障排查指南) +9. [结论](#结论) +10. [附录](#附录) + +## 简介 +本文件面向“文心平台集成”的技术与非技术读者,系统性说明文心一言平台适配器的实现方式,涵盖以下方面: +- 文心适配器的职责与实现模式(基于浏览器自动化) +- 请求参数构建(关键词处理、页面交互策略) +- HTTP请求与页面自动化流程(导航、输入、提交、等待稳定) +- 响应解析与稳定性检测(文本提取、稳定判定) +- 配置管理(环境变量、超时与重试策略) +- 错误处理与异常恢复 +- 安全注意事项与最佳实践 + +## 项目结构 +文心平台集成位于后端工作流模块中,采用“适配器 + 引擎”的分层设计: +- 适配器层:负责具体平台的页面自动化交互 +- 引擎层:编排多平台查询、品牌匹配与统计 +- 配置层:集中管理运行时参数(如浏览器路径、API密钥占位) + +```mermaid +graph TB +subgraph "适配器层" +Base["BasePlatformAdapter
抽象接口"] +Wenxin["WenxinAdapter
文心一言适配器"] +end +subgraph "引擎层" +Engine["CitationEngine
引用检测引擎"] +end +subgraph "配置层" +Cfg["Settings
环境配置"] +Docker["Dockerfile
容器化与Playwright依赖"] +end +subgraph "数据模型" +QModel["Query
查询模型"] +end +Base --> Wenxin +Engine --> Wenxin +Engine --> QModel +Wenxin --> Cfg +Docker --> Wenxin +``` + +图表来源 +- [base.py:1-18](file://backend/app/workers/platforms/base.py#L1-L18) +- [wenxin.py:11-205](file://backend/app/workers/platforms/wenxin.py#L11-L205) +- [citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) +- [config.py:4-16](file://backend/app/config.py#L4-L16) +- [Dockerfile:1-40](file://backend/Dockerfile#L1-L40) +- [query.py:11-55](file://backend/app/models/query.py#L11-L55) + +章节来源 +- [base.py:1-18](file://backend/app/workers/platforms/base.py#L1-L18) +- [wenxin.py:11-205](file://backend/app/workers/platforms/wenxin.py#L11-L205) +- [citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) +- [config.py:4-16](file://backend/app/config.py#L4-L16) +- [Dockerfile:1-40](file://backend/Dockerfile#L1-L40) +- [query.py:11-55](file://backend/app/models/query.py#L11-L55) + +## 核心组件 +- 文心适配器(WenxinAdapter):实现文心一言页面自动化交互,负责关键词输入、提交、等待回复稳定,并返回原始文本。 +- 引擎(CitationEngine):编排查询任务,调用适配器获取原始响应,进行品牌匹配与竞争品牌检测,并持久化结果。 +- 基类(BasePlatformAdapter):定义统一的平台适配器接口,约束平台名称、URL与查询方法。 +- 配置(Settings):集中管理数据库、Redis、JWT、Playwright浏览器路径以及API密钥占位等配置项。 +- 数据模型(Query):承载查询任务的元数据,包括关键词、目标品牌、平台集合、频率与状态等。 + +章节来源 +- [wenxin.py:11-205](file://backend/app/workers/platforms/wenxin.py#L11-L205) +- [citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) +- [base.py:4-18](file://backend/app/workers/platforms/base.py#L4-L18) +- [config.py:4-16](file://backend/app/config.py#L4-L16) +- [query.py:11-55](file://backend/app/models/query.py#L11-L55) + +## 架构总览 +文心平台集成采用“浏览器自动化 + 引擎编排”的架构: +- 引擎接收查询请求,按平台顺序调用适配器 +- 适配器通过Playwright启动无头浏览器,访问文心一言页面,模拟用户输入与提交 +- 适配器等待回复稳定后提取文本,返回给引擎 +- 引擎进行品牌匹配与统计,写入数据库 + +```mermaid +sequenceDiagram +participant Client as "客户端" +participant Engine as "CitationEngine" +participant Adapter as "WenxinAdapter" +participant Browser as "Playwright浏览器" +participant Platform as "文心一言页面" +Client->>Engine : "发起查询请求" +Engine->>Adapter : "query(keyword)" +Adapter->>Browser : "启动无头浏览器" +Browser->>Platform : "goto(文心URL, 超时=30s)" +Platform-->>Browser : "页面加载完成" +Browser->>Platform : "查找输入框并填入关键词" +Browser->>Platform : "提交点击发送或按回车" +Browser->>Platform : "等待回复稳定" +Platform-->>Browser : "返回最新回复文本" +Browser-->>Adapter : "返回原始响应文本" +Adapter-->>Engine : "返回原始响应文本" +Engine-->>Client : "返回引用检测结果" +``` + +图表来源 +- [citation_engine.py:236-266](file://backend/app/workers/citation_engine.py#L236-L266) +- [wenxin.py:33-122](file://backend/app/workers/platforms/wenxin.py#L33-L122) + +章节来源 +- [citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) +- [wenxin.py:11-205](file://backend/app/workers/platforms/wenxin.py#L11-L205) + +## 详细组件分析 + +### 文心适配器(WenxinAdapter) +- 角色与职责 + - 实现BasePlatformAdapter接口,提供文心一言平台的查询能力 + - 使用Playwright进行页面自动化,模拟真实用户行为 + - 提供重试与指数退避策略,提升稳定性 + - 提供等待回复稳定的算法,避免过早读取半成品内容 + +- 关键实现要点 + - 浏览器生命周期管理:确保浏览器仅初始化一次,避免重复开销 + - 页面交互策略:多选择器容错查找输入框;优先点击发送按钮,否则回车提交 + - 稳定性检测:连续多次检测文本是否稳定,超时则返回当前文本 + - 错误处理:捕获超时与通用异常,统一包装为运行时错误并记录日志 + +- 请求参数构建 + - 关键词处理:直接填充到输入框,支持多种输入控件类型 + - 请求头与认证:该实现为浏览器自动化,不涉及HTTP请求头与API密钥 + - 超时设置:页面导航超时30秒,输入框查找超时10秒,发送按钮查找超时5秒 + - 重试配置:最多3次尝试,指数退避(2^attempt秒) + +- 响应解析逻辑 + - 文本提取:优先使用CSS选择器定位消息容器,其次使用JavaScript遍历页面 + - 稳定性判定:连续3次检测文本相同且长度大于阈值,视为稳定 + - 超时处理:超过90秒仍未稳定,返回当前已收集文本并告警 + +- 安全与合规 + - 该实现为浏览器自动化,不涉及明文API密钥传递 + - 建议在生产环境限制浏览器访问范围与网络策略 + +```mermaid +classDiagram +class BasePlatformAdapter { ++string platform_name ++string platform_url ++query(keyword) str ++close() void +} +class WenxinAdapter { +-_playwright +-_browser ++query(keyword) str ++close() void +-_ensure_browser() void +-_do_query(keyword) str +-_wait_for_response_stable(page, timeout) str +} +BasePlatformAdapter <|-- WenxinAdapter +``` + +图表来源 +- [base.py:4-18](file://backend/app/workers/platforms/base.py#L4-L18) +- [wenxin.py:11-205](file://backend/app/workers/platforms/wenxin.py#L11-L205) + +章节来源 +- [wenxin.py:11-205](file://backend/app/workers/platforms/wenxin.py#L11-L205) + +### 引擎(CitationEngine) +- 角色与职责 + - 编排查询任务,按平台顺序执行 + - 调用适配器获取原始响应,进行品牌匹配与竞争品牌检测 + - 将结果持久化为引用记录,并更新任务状态与查询时间 + +- 关键实现要点 + - 平台注册:内置文心与Kimi适配器,可扩展更多平台 + - 任务状态管理:运行中、成功、失败三种状态,失败时记录错误信息 + - 结果聚合:返回引用状态、置信度、匹配类型、位置、上下文、竞争品牌与原始响应 + +- 错误处理 + - 适配器异常会被捕获并记录,同时生成一条cited=False的占位记录 + - 任务状态与完成时间被正确更新,保证数据一致性 + +```mermaid +flowchart TD +Start(["开始执行查询"]) --> InitMatcher["初始化品牌匹配器"] +InitMatcher --> IteratePlatforms["遍历平台列表"] +IteratePlatforms --> CreateTask["获取或创建任务记录"] +CreateTask --> UpdateTaskRunning["更新任务状态为 running"] +UpdateTaskRunning --> CallAdapter["调用适配器执行查询"] +CallAdapter --> ParseResult["品牌匹配与竞争品牌检测"] +ParseResult --> CreateRecord["创建引用记录"] +CreateRecord --> UpdateTaskSuccess["更新任务状态为 success"] +UpdateTaskSuccess --> NextPlatform{"还有平台吗?"} +NextPlatform --> |是| IteratePlatforms +NextPlatform --> |否| UpdateQueryTime["更新查询时间字段"] +UpdateQueryTime --> End(["结束"]) +``` + +图表来源 +- [citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) + +章节来源 +- [citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) + +### 配置管理(Settings) +- 配置项说明 + - 数据库连接:DATABASE_URL + - Redis连接:REDIS_URL + - JWT密钥与过期:JWT_SECRET、JWT_EXPIRE_HOURS + - Playwright浏览器路径:PLAYWRIGHT_BROWSERS_PATH + - API密钥占位:ZHIPU_API_KEY、TONGYI_API_KEY(当前未用于文心适配器) + +- 环境变量加载 + - 通过Pydantic Settings自动从.env文件加载,忽略未知字段 + +- 容器化与依赖 + - Dockerfile中安装系统依赖并预装Playwright浏览器,确保运行时可用 + +章节来源 +- [config.py:4-16](file://backend/app/config.py#L4-L16) +- [Dockerfile:1-40](file://backend/Dockerfile#L1-L40) + +### 数据模型(Query) +- 字段说明 + - 关键词、目标品牌、别名、平台集合、频率、状态、时间戳等 + - 默认平台集合包含文心与Kimi,便于快速启用 + +- 业务意义 + - 作为查询任务的载体,驱动引擎执行跨平台检索与分析 + +章节来源 +- [query.py:11-55](file://backend/app/models/query.py#L11-L55) + +### API入口(queries.py) +- 功能概述 + - 提供查询任务的增删改查接口,供前端调用 + - 与服务层协作,完成权限校验与数据持久化 + +- 与引擎的关系 + - 引擎在后台异步执行查询,API负责暴露任务管理能力 + +章节来源 +- [queries.py:1-86](file://backend/app/api/queries.py#L1-L86) + +## 依赖关系分析 +- 组件耦合 + - CitationEngine依赖WenxinAdapter与KimiAdapter,形成平台无关的编排层 + - WenxinAdapter依赖Playwright进行页面自动化,不依赖HTTP客户端 + - 引擎与模型解耦,通过ORM进行数据持久化 + +- 外部依赖 + - Playwright:无头浏览器自动化 + - Docker:容器化部署,预装浏览器与系统依赖 + +```mermaid +graph LR +Engine["CitationEngine"] --> Wenxin["WenxinAdapter"] +Engine --> Kimi["KimiAdapter"] +Wenxin --> Playwright["Playwright"] +Kimi --> Playwright +Engine --> ORM["SQLAlchemy ORM"] +Engine --> Model["Query模型"] +``` + +图表来源 +- [citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) +- [wenxin.py:11-205](file://backend/app/workers/platforms/wenxin.py#L11-L205) + +章节来源 +- [citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) +- [wenxin.py:11-205](file://backend/app/workers/platforms/wenxin.py#L11-L205) + +## 性能考虑 +- 浏览器资源复用 + - 适配器内部维护浏览器实例,避免重复启动带来的冷启动开销 +- 稳定性检测优化 + - 通过连续检测与超时控制,减少无效重试 +- 重试策略 + - 最多重试3次,指数退避降低瞬时压力 +- 超时设置 + - 页面导航、元素查找、发送按钮等待均有明确超时,防止长时间阻塞 + +章节来源 +- [wenxin.py:33-48](file://backend/app/workers/platforms/wenxin.py#L33-L48) +- [wenxin.py:124-196](file://backend/app/workers/platforms/wenxin.py#L124-L196) + +## 故障排查指南 +- 启动浏览器失败 + - 现象:初始化浏览器时报错,提示未安装Playwright浏览器 + - 处理:在容器内执行Playwright浏览器安装命令;检查Dockerfile中的安装步骤 + - 参考:[wenxin.py:21-31](file://backend/app/workers/platforms/wenxin.py#L21-L31)、[Dockerfile:31-33](file://backend/Dockerfile#L31-L33) + +- 页面操作超时 + - 现象:页面导航或元素查找超时 + - 处理:检查网络连通性、目标站点可达性;适当调整超时参数 + - 参考:[wenxin.py:64-87](file://backend/app/workers/platforms/wenxin.py#L64-L87)、[wenxin.py:114-117](file://backend/app/workers/platforms/wenxin.py#L114-L117) + +- 无法找到输入框 + - 现象:页面结构变更导致选择器失效 + - 处理:更新输入框选择器列表;增加容错分支 + - 参考:[wenxin.py:67-87](file://backend/app/workers/platforms/wenxin.py#L67-L87) + +- 回复不稳定 + - 现象:文本持续变化,无法稳定 + - 处理:延长等待时间或放宽稳定阈值;检查页面动态加载逻辑 + - 参考:[wenxin.py:124-196](file://backend/app/workers/platforms/wenxin.py#L124-L196) + +- 引擎执行失败 + - 现象:适配器异常导致任务失败 + - 处理:记录错误信息并生成占位记录;检查日志定位根因 + - 参考:[citation_engine.py:211-227](file://backend/app/workers/citation_engine.py#L211-L227) + +- 单元测试验证 + - 测试覆盖平台返回值与统计数据,确保集成链路正常 + - 参考:[test_citations.py:23-93](file://tests/test_citations.py#L23-L93) + +章节来源 +- [wenxin.py:21-31](file://backend/app/workers/platforms/wenxin.py#L21-L31) +- [wenxin.py:64-87](file://backend/app/workers/platforms/wenxin.py#L64-L87) +- [wenxin.py:114-117](file://backend/app/workers/platforms/wenxin.py#L114-L117) +- [wenxin.py:124-196](file://backend/app/workers/platforms/wenxin.py#L124-L196) +- [citation_engine.py:211-227](file://backend/app/workers/citation_engine.py#L211-L227) +- [test_citations.py:23-93](file://tests/test_citations.py#L23-L93) + +## 结论 +文心平台集成通过“浏览器自动化 + 引擎编排”的方式,实现了对文心一言的稳定查询与响应解析。其特点包括: +- 明确的适配器接口与可扩展的平台注册机制 +- 完善的重试与稳定性检测策略 +- 清晰的错误处理与任务状态管理 +- 与容器化部署的无缝衔接 + +在实际使用中,建议结合业务需求对超时、重试与稳定性阈值进行调优,并持续关注页面结构变化以保持选择器的有效性。 + +## 附录 + +### API调用示例与错误处理方案 +- 示例场景 + - 前端通过查询API创建任务,后台引擎按平台顺序执行 + - 引擎调用文心适配器获取原始响应,进行品牌匹配与统计 + - 若适配器异常,引擎记录失败并生成占位记录 + +- 错误处理方案 + - 适配器异常:捕获并记录,返回占位记录 + - 页面超时:延长等待或放宽阈值 + - 选择器失效:更新选择器列表并增加容错分支 + +章节来源 +- [queries.py:1-86](file://backend/app/api/queries.py#L1-L86) +- [citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) +- [wenxin.py:33-122](file://backend/app/workers/platforms/wenxin.py#L33-L122) + +### 安全注意事项与最佳实践 +- 安全注意事项 + - 该实现为浏览器自动化,不涉及HTTP请求头与API密钥传递 + - 生产环境建议限制容器网络访问范围,仅允许必要域名 + - 日志中避免输出敏感信息(如用户输入、错误堆栈) + +- 最佳实践 + - 在Dockerfile中预装浏览器与系统依赖,确保部署一致性 + - 对页面选择器进行版本化管理,随页面结构变更同步更新 + - 设置合理的超时与重试策略,平衡稳定性与性能 + - 使用单元测试覆盖关键流程,保障回归质量 + +章节来源 +- [Dockerfile:1-40](file://backend/Dockerfile#L1-L40) +- [test_citations.py:23-93](file://tests/test_citations.py#L23-L93) \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/AI平台集成/适配器架构设计.md b/.qoder/repowiki/zh/content/AI平台集成/适配器架构设计.md new file mode 100644 index 0000000..7ba9a43 --- /dev/null +++ b/.qoder/repowiki/zh/content/AI平台集成/适配器架构设计.md @@ -0,0 +1,510 @@ +# 适配器架构设计 + + +**本文档引用的文件** +- [base.py](file://backend/app/workers/platforms/base.py) +- [kimi.py](file://backend/app/workers/platforms/kimi.py) +- [wenxin.py](file://backend/app/workers/platforms/wenxin.py) +- [citation_engine.py](file://backend/app/workers/citation_engine.py) +- [scheduler.py](file://backend/app/workers/scheduler.py) +- [queries.py](file://backend/app/api/queries.py) +- [query.py](file://backend/app/models/query.py) +- [main.py](file://backend/app/main.py) +- [platforms.ts](file://frontend/lib/platforms.ts) +- [platform-chart.tsx](file://frontend/components/charts/platform-chart.tsx) + + +## 目录 +1. [简介](#简介) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构概览](#架构概览) +5. [详细组件分析](#详细组件分析) +6. [依赖关系分析](#依赖关系分析) +7. [性能考虑](#性能考虑) +8. [故障排除指南](#故障排除指南) +9. [结论](#结论) + +## 简介 + +本项目采用适配器模式设计了一个可扩展的AI平台查询系统。该架构通过统一的接口抽象,实现了对不同AI平台(如Kimi、文心一言等)的无缝集成和切换。适配器模式在此场景中的应用价值体现在: + +- **统一接口**:为不同的AI平台提供一致的查询接口 +- **可扩展性**:轻松添加新的AI平台适配器 +- **资源管理**:统一的资源清理和生命周期管理 +- **错误处理**:标准化的异常处理和重试机制 + +## 项目结构 + +该项目采用前后端分离的架构设计,后端使用Python FastAPI框架,前端使用Next.js React框架。适配器架构主要集中在后端的workers目录中。 + +```mermaid +graph TB +subgraph "前端层" +FE[前端应用
Next.js React] +PC[平台图表组件
platform-chart.tsx] +PL[平台映射
platforms.ts] +end +subgraph "后端层" +API[FastAPI API] +CE[CitationEngine
引用检测引擎] +SCH[Scheduler
调度器] +subgraph "适配器层" +BA[BasePlatformAdapter
抽象基类] +KA[KimiAdapter
Kimi适配器] +WA[WenxinAdapter
文心一言适配器] +end +end +subgraph "数据层" +DB[(PostgreSQL数据库)] +QM[Query模型] +CRM[CitationRecord模型] +end +FE --> API +API --> CE +CE --> SCH +CE --> BA +BA --> KA +BA --> WA +CE --> DB +QM --> DB +CRM --> DB +``` + +**图表来源** +- [main.py:13-22](file://backend/app/main.py#L13-L22) +- [citation_engine.py:148-157](file://backend/app/workers/citation_engine.py#L148-L157) +- [base.py:4-17](file://backend/app/workers/platforms/base.py#L4-L17) + +**章节来源** +- [main.py:1-48](file://backend/app/main.py#L1-L48) +- [platforms.ts:1-18](file://frontend/lib/platforms.ts#L1-L18) + +## 核心组件 + +### BasePlatformAdapter 抽象基类 + +BasePlatformAdapter是整个适配器架构的核心抽象类,定义了所有AI平台适配器必须实现的标准接口。 + +#### 设计理念 + +该类采用了Python的ABC(Abstract Base Classes)机制,确保子类必须实现指定的抽象方法。设计理念包括: + +- **最小接口原则**:只定义必要的抽象方法,避免过度设计 +- **职责单一原则**:专注于平台适配功能,不包含业务逻辑 +- **扩展性优先**:为未来添加新平台预留空间 + +#### 核心属性 + +| 属性名称 | 类型 | 描述 | 示例值 | +|---------|------|------|--------| +| platform_name | str | 平台标识符,用于内部识别 | "kimi", "wenxin" | +| platform_url | str | 平台官方网站地址 | "https://kimi.moonshot.cn" | + +#### 抽象方法 + +**query(keyword: str) -> str** +- **设计目的**:在AI平台上查询关键词并返回原始响应文本 +- **参数规范**: + - keyword: str - 要查询的关键词 + - 返回值: str - AI平台的原始响应文本 +- **实现要求**:必须异步实现,支持重试机制 + +**close()** +- **设计目的**:清理适配器使用的系统资源 +- **实现要求**:异步方法,确保资源正确释放 + +**章节来源** +- [base.py:4-17](file://backend/app/workers/platforms/base.py#L4-L17) + +### 具体适配器实现 + +#### KimiAdapter + +KimiAdapter是针对Moonshot AI平台的适配器实现,具有以下特点: + +- **浏览器自动化**:使用Playwright进行页面交互 +- **智能重试**:指数退避重试机制 +- **稳定性保障**:超时处理和异常恢复 + +#### WenxinAdapter + +WenxinAdapter是针对百度文心一言平台的适配器实现: + +- **相似架构**:与KimiAdapter类似的实现模式 +- **平台特定**:针对文心一言的界面结构优化 +- **一致性保证**:保持与BasePlatformAdapter的接口兼容 + +**章节来源** +- [kimi.py:11-206](file://backend/app/workers/platforms/kimi.py#L11-L206) +- [wenxin.py:11-205](file://backend/app/workers/platforms/wenxin.py#L11-L205) + +## 架构概览 + +整个系统采用分层架构设计,适配器模式位于中间层,向上提供统一接口,向下封装具体实现细节。 + +```mermaid +sequenceDiagram +participant Client as 客户端 +participant API as FastAPI API +participant Engine as CitationEngine +participant Adapter as BasePlatformAdapter +participant Platform as AI平台 +Client->>API : 发起查询请求 +API->>Engine : execute_query() +Engine->>Engine : 创建BrandMatcher +Engine->>Adapter : execute_single_platform() +Adapter->>Adapter : query(keyword) +Adapter->>Platform : 执行平台查询 +Platform-->>Adapter : 返回响应文本 +Adapter-->>Engine : 返回原始响应 +Engine->>Engine : 品牌匹配和竞争检测 +Engine-->>API : 返回检测结果 +API-->>Client : 返回查询结果 +``` + +**图表来源** +- [citation_engine.py:159-234](file://backend/app/workers/citation_engine.py#L159-L234) +- [base.py:10-17](file://backend/app/workers/platforms/base.py#L10-L17) + +## 详细组件分析 + +### CitationEngine 引用检测引擎 + +CitationEngine是系统的中央协调器,负责管理多个平台适配器并执行完整的查询流程。 + +#### 核心功能 + +1. **平台管理**:维护平台适配器字典 +2. **查询执行**:协调单个平台的查询过程 +3. **结果处理**:整合品牌匹配和竞争检测结果 +4. **任务跟踪**:管理查询任务的状态和历史 + +#### 关键流程 + +```mermaid +flowchart TD +Start([开始查询]) --> InitMatcher["初始化BrandMatcher"] +InitMatcher --> GetPlatforms["获取平台列表"] +GetPlatforms --> LoopPlatforms{"遍历平台"} +LoopPlatforms --> CreateTask["创建或获取QueryTask"] +CreateTask --> UpdateStatus["更新任务状态为running"] +UpdateStatus --> ExecutePlatform["执行单个平台查询"] +ExecutePlatform --> BrandMatch["品牌匹配检测"] +BrandMatch --> CompetitorDetect["竞争品牌检测"] +CompetitorDetect --> CreateRecord["创建CitationRecord"] +CreateRecord --> UpdateTaskSuccess["更新任务状态为success"] +UpdateTaskSuccess --> NextPlatform{"还有平台吗?"} +NextPlatform --> |是| LoopPlatforms +NextPlatform --> |否| UpdateQueryTime["更新查询时间字段"] +UpdateQueryTime --> End([结束]) +ExecutePlatform --> |异常| CreateFailedRecord["创建失败记录"] +CreateFailedRecord --> UpdateTaskFailed["更新任务状态为failed"] +UpdateTaskFailed --> NextPlatform +``` + +**图表来源** +- [citation_engine.py:159-234](file://backend/app/workers/citation_engine.py#L159-L234) +- [citation_engine.py:236-266](file://backend/app/workers/citation_engine.py#L236-L266) + +#### 数据模型 + +```mermaid +erDiagram +QUERY { +uuid id PK +uuid user_id FK +string keyword +string target_brand +jsonb brand_aliases +jsonb platforms +string frequency +string status +datetime last_queried_at +datetime next_query_at +datetime created_at +datetime updated_at +} +CITATION_RECORD { +uuid id PK +uuid query_id FK +string platform +boolean cited +integer citation_position +text citation_text +jsonb competitor_brands +text raw_response +datetime created_at +datetime updated_at +} +QUERY_TASK { +uuid id PK +uuid query_id FK +string platform +string status +datetime started_at +datetime completed_at +text error_message +datetime created_at +datetime updated_at +} +USER ||--o{ QUERY : creates +QUERY ||--o{ CITATION_RECORD : generates +QUERY ||--o{ QUERY_TASK : tracks +``` + +**图表来源** +- [query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [citation_engine.py:19-120](file://backend/app/workers/citation_engine.py#L19-L120) + +**章节来源** +- [citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) +- [query.py:11-55](file://backend/app/models/query.py#L11-L55) + +### Scheduler 调度器 + +QueryScheduler负责定时执行查询任务,确保系统能够自动运行。 + +#### 核心特性 + +- **定时执行**:每小时检查一次到期的查询任务 +- **异步处理**:使用AsyncIOScheduler支持异步操作 +- **错误隔离**:单个查询失败不影响其他任务 +- **优雅关闭**:提供资源清理机制 + +#### 生命周期管理 + +```mermaid +stateDiagram-v2 +[*] --> 初始化 +初始化 --> 启动 : start() +启动 --> 运行中 : 添加定时任务 +运行中 --> 执行检查 : 每小时触发 +执行检查 --> 处理查询 : 发现到期任务 +处理查询 --> 运行中 : 继续监控 +处理查询 --> 错误 : 异常处理 +错误 --> 运行中 : 记录日志继续 +运行中 --> 关闭 : shutdown() +关闭 --> [*] +``` + +**图表来源** +- [scheduler.py:25-95](file://backend/app/workers/scheduler.py#L25-L95) + +**章节来源** +- [scheduler.py:25-95](file://backend/app/workers/scheduler.py#L25-L95) + +### 前端集成 + +前端系统提供了完整的用户界面,支持平台选择和结果展示。 + +#### 平台映射 + +前端使用PLATFORM_MAP和PLATFORMS常量来管理平台信息: + +| 平台键 | 中文名称 | 用途 | +|--------|----------|------| +| wenxin | 文心一言 | 百度AI平台 | +| kimi | Kimi | Moonshot AI平台 | +| tongyi | 通义千问 | 阿里云AI平台 | +| baidu_ai | 百度AI搜索 | 百度搜索服务 | +| yuanbao | 腾讯元宝 | 腾讯AI平台 | +| qingyan | 智谱清言 | 智谱AI平台 | + +#### 图表可视化 + +平台图表组件使用Recharts库展示各平台的引用率统计: + +```mermaid +graph LR +subgraph "数据处理" +RAW[原始统计数据] +TRANSFORM[数据转换] +end +subgraph "可视化组件" +CHART[柱状图] +XAXIS[X轴: 平台名称] +YAXIS[Y轴: 引用率百分比] +TOOLTIP[工具提示] +end +RAW --> TRANSFORM +TRANSFORM --> CHART +CHART --> XAXIS +CHART --> YAXIS +CHART --> TOOLTIP +``` + +**图表来源** +- [platform-chart.tsx:34-68](file://frontend/components/charts/platform-chart.tsx#L34-L68) +- [platforms.ts:1-18](file://frontend/lib/platforms.ts#L1-L18) + +**章节来源** +- [platforms.ts:1-18](file://frontend/lib/platforms.ts#L1-L18) +- [platform-chart.tsx:1-68](file://frontend/components/charts/platform-chart.tsx#L1-L68) + +## 依赖关系分析 + +### 组件依赖图 + +```mermaid +graph TB +subgraph "外部依赖" +PW[Playwright] +APS[APScheduler] +SQLA[SQLAlchemy] +FAST[FastAPI] +end +subgraph "核心模块" +BASE[BasePlatformAdapter] +KIMI[KimiAdapter] +WENXIN[WenxinAdapter] +CE[CitationEngine] +SCH[QueryScheduler] +API[FastAPI API] +end +subgraph "数据模型" +QUERY[Query模型] +CITE[CitationRecord模型] +TASK[QueryTask模型] +end +PW --> KIMI +PW --> WENXIN +APS --> SCH +SQLA --> CE +FAST --> API +BASE --> KIMI +BASE --> WENXIN +CE --> BASE +API --> CE +CE --> QUERY +CE --> CITE +CE --> TASK +``` + +**图表来源** +- [kimi.py:4-6](file://backend/app/workers/platforms/kimi.py#L4-L6) +- [wenxin.py:4-6](file://backend/app/workers/platforms/wenxin.py#L4-L6) +- [scheduler.py:13-20](file://backend/app/workers/scheduler.py#L13-L20) +- [citation_engine.py:7-14](file://backend/app/workers/citation_engine.py#L7-L14) + +### 耦合度分析 + +该系统展现了良好的内聚性和低耦合性: + +- **高内聚**:每个适配器专注于单一平台的实现 +- **低耦合**:通过抽象基类实现松散耦合 +- **清晰边界**:各层职责明确,接口清晰 +- **可测试性**:依赖注入和抽象接口便于单元测试 + +**章节来源** +- [base.py:4-17](file://backend/app/workers/platforms/base.py#L4-L17) +- [citation_engine.py:148-157](file://backend/app/workers/citation_engine.py#L148-L157) + +## 性能考虑 + +### 异步编程模型 + +系统全面采用异步编程模式,提升了并发处理能力: + +- **异步适配器**:所有适配器方法都使用async/await +- **异步数据库**:使用SQLAlchemy异步会话 +- **异步调度**:APScheduler的异步版本 +- **异步HTTP**:FastAPI的异步路由 + +### 资源管理策略 + +```mermaid +flowchart TD +InitBrowser["初始化浏览器"] --> UseBrowser["使用浏览器"] +UseBrowser --> CloseBrowser["关闭浏览器"] +CloseBrowser --> CleanupResources["清理系统资源"] +CleanupResources --> End([资源释放完成]) +InitBrowser --> Error["异常发生"] +Error --> CleanupOnError["异常时清理资源"] +CleanupOnError --> End +``` + +**图表来源** +- [kimi.py:17-31](file://backend/app/workers/platforms/kimi.py#L17-L31) +- [kimi.py:198-206](file://backend/app/workers/platforms/kimi.py#L198-L206) + +### 缓存和重试机制 + +- **指数退避**:最多3次重试,间隔呈指数增长 +- **超时控制**:页面加载和操作都有超时限制 +- **资源池化**:浏览器实例复用,减少启动开销 + +## 故障排除指南 + +### 常见问题及解决方案 + +#### Playwright浏览器问题 + +**症状**:启动浏览器失败,提示需要安装Playwright浏览器 + +**解决方案**: +1. 安装Playwright浏览器:`python -m playwright install chromium` +2. 检查网络连接和代理设置 +3. 确认Docker环境中的浏览器可用性 + +#### 页面元素定位失败 + +**症状**:无法找到输入框或发送按钮 + +**解决方案**: +1. 检查平台URL是否正确 +2. 更新选择器表达式以适应页面变化 +3. 实现更健壮的元素等待机制 + +#### 超时问题 + +**症状**:页面加载或响应超时 + +**解决方案**: +1. 增加超时时间配置 +2. 检查网络连接稳定性 +3. 实现重试机制 + +**章节来源** +- [kimi.py:27-31](file://backend/app/workers/platforms/kimi.py#L27-L31) +- [kimi.py:116-118](file://backend/app/workers/platforms/kimi.py#L116-L118) +- [wenxin.py:27-31](file://backend/app/workers/platforms/wenxin.py#L27-L31) + +### 调试技巧 + +1. **启用详细日志**:查看适配器的详细执行过程 +2. **检查数据库状态**:验证查询任务的状态更新 +3. **监控资源使用**:观察浏览器进程和内存使用情况 +4. **测试独立适配器**:单独测试某个平台的适配器 + +## 结论 + +该适配器架构设计成功地实现了以下目标: + +### 架构优势 + +1. **高度可扩展**:新增平台只需实现BasePlatformAdapter接口 +2. **统一管理**:通过CitationEngine集中管理所有平台 +3. **资源优化**:统一的资源管理和清理机制 +4. **错误处理**:完善的异常处理和重试机制 + +### 最佳实践建议 + +1. **遵循接口契约**:严格实现BasePlatformAdapter的所有抽象方法 +2. **资源管理**:确保close()方法能够正确清理所有资源 +3. **错误处理**:实现适当的异常处理和重试逻辑 +4. **配置管理**:将平台特定的配置参数化 +5. **测试覆盖**:为新适配器编写充分的单元测试 + +### 扩展指南 + +要为新平台创建适配器,需要: + +1. 继承BasePlatformAdapter类 +2. 设置platform_name和platform_url属性 +3. 实现query()方法的平台特定逻辑 +4. 实现close()方法清理资源 +5. 在CitationEngine中注册新适配器 +6. 编写相应的测试用例 + +该架构为AI平台集成提供了一个稳健、可扩展的基础,能够支持未来更多的平台集成需求。 \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/API接口文档/API接口文档.md b/.qoder/repowiki/zh/content/API接口文档/API接口文档.md new file mode 100644 index 0000000..625fb9b --- /dev/null +++ b/.qoder/repowiki/zh/content/API接口文档/API接口文档.md @@ -0,0 +1,407 @@ +# API接口文档 + + +**本文档中引用的文件** +- [backend/app/main.py](file://backend/app/main.py) +- [backend/app/api/auth.py](file://backend/app/api/auth.py) +- [backend/app/api/queries.py](file://backend/app/api/queries.py) +- [backend/app/api/citations.py](file://backend/app/api/citations.py) +- [backend/app/api/reports.py](file://backend/app/api/reports.py) +- [backend/app/api/deps.py](file://backend/app/api/deps.py) +- [backend/app/schemas/auth.py](file://backend/app/schemas/auth.py) +- [backend/app/schemas/query.py](file://backend/app/schemas/query.py) +- [backend/app/schemas/citation.py](file://backend/app/schemas/citation.py) +- [backend/app/services/auth.py](file://backend/app/services/auth.py) +- [backend/app/services/query.py](file://backend/app/services/query.py) +- [backend/app/services/citation.py](file://backend/app/services/citation.py) +- [backend/app/config.py](file://backend/app/config.py) +- [backend/app/models/user.py](file://backend/app/models/user.py) +- [backend/app/models/query.py](file://backend/app/models/query.py) +- [backend/app/models/citation_record.py](file://backend/app/models/citation_record.py) +- [backend/app/models/query_task.py](file://backend/app/models/query_task.py) + + +## 目录 +1. [简介](#简介) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构概览](#架构概览) +5. [详细组件分析](#详细组件分析) +6. [依赖分析](#依赖分析) +7. [性能考虑](#性能考虑) +8. [故障排除指南](#故障排除指南) +9. [结论](#结论) + +## 简介 +本文件为GEO平台的完整API接口文档,涵盖认证、查询管理、引用数据、报告导出等核心功能模块。文档详细记录了所有RESTful API端点的HTTP方法、URL模式、请求参数与响应格式,并说明了JWT令牌管理、用户注册登录、权限验证机制、任务创建与执行、数据查询与统计分析、以及CSV格式报告导出流程。 + +## 项目结构 +后端采用FastAPI框架,按功能模块组织API路由:认证(/api/v1/auth)、查询词(/api/v1/queries)、引用数据(/api/v1/citations)、报告(/api/v1/reports)。应用启动时初始化数据库模型并启动查询调度器,同时启用CORS允许前端localhost:3000访问。 + +```mermaid +graph TB +A["应用入口
backend/app/main.py"] --> B["认证路由
backend/app/api/auth.py"] +A --> C["查询路由
backend/app/api/queries.py"] +A --> D["引用数据路由
backend/app/api/citations.py"] +A --> E["报告路由
backend/app/api/reports.py"] +A --> F["依赖注入与认证中间件
backend/app/api/deps.py"] +A --> G["配置中心
backend/app/config.py"] +A --> H["数据库模型
backend/app/models/*.py"] +A --> I["业务服务层
backend/app/services/*.py"] +A --> J["数据传输对象
backend/app/schemas/*.py"] +``` + +**图表来源** +- [backend/app/main.py:24-47](file://backend/app/main.py#L24-L47) +- [backend/app/api/auth.py:10](file://backend/app/api/auth.py#L10) +- [backend/app/api/queries.py:12](file://backend/app/api/queries.py#L12) +- [backend/app/api/citations.py:21](file://backend/app/api/citations.py#L21) +- [backend/app/api/reports.py:13](file://backend/app/api/reports.py#L13) +- [backend/app/api/deps.py:13](file://backend/app/api/deps.py#L13) + +**章节来源** +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) + +## 核心组件 +- 应用入口与生命周期管理:定义应用标题、版本、CORS策略,注册各模块路由,启动/关闭查询调度器。 +- 认证组件:提供注册、登录、当前用户信息查询;基于OAuth2密码流与JWT进行身份验证。 +- 查询管理:支持查询词的增删改查、分页列表、频率与下次查询时间计算。 +- 引用数据:支持引用记录查询、统计分析(总查询数、引用率、平台分布、趋势)、立即执行查询任务。 +- 报告导出:支持CSV格式导出指定查询的引用记录。 +- 数据模型与服务:用户、查询、引用记录、查询任务等模型及对应的服务逻辑。 + +**章节来源** +- [backend/app/main.py:13-47](file://backend/app/main.py#L13-L47) +- [backend/app/api/auth.py:13-42](file://backend/app/api/auth.py#L13-L42) +- [backend/app/api/queries.py:15-85](file://backend/app/api/queries.py#L15-L85) +- [backend/app/api/citations.py:25-77](file://backend/app/api/citations.py#L25-L77) +- [backend/app/api/reports.py:16-46](file://backend/app/api/reports.py#L16-L46) + +## 架构概览 +下图展示了客户端与后端各模块之间的交互关系,包括认证流程、查询管理、引用数据处理与报告导出。 + +```mermaid +graph TB +subgraph "客户端" +FE["前端应用
localhost:3000"] +end +subgraph "后端服务" +AUTH["认证模块
/api/v1/auth"] +QUERIES["查询模块
/api/v1/queries"] +CITATIONS["引用数据模块
/api/v1/citations"] +REPORTS["报告模块
/api/v1/reports"] +DEPS["依赖注入与认证中间件"] +MODELS["数据模型层"] +SERVICES["业务服务层"] +CONFIG["配置中心"] +end +FE --> AUTH +FE --> QUERIES +FE --> CITATIONS +FE --> REPORTS +AUTH --> DEPS +QUERIES --> DEPS +CITATIONS --> DEPS +REPORTS --> DEPS +AUTH --> SERVICES +QUERIES --> SERVICES +CITATIONS --> SERVICES +REPORTS --> SERVICES +SERVICES --> MODELS +MODELS --> CONFIG +``` + +**图表来源** +- [backend/app/main.py:38-42](file://backend/app/main.py#L38-L42) +- [backend/app/api/deps.py:16-42](file://backend/app/api/deps.py#L16-L42) +- [backend/app/services/auth.py:37-68](file://backend/app/services/auth.py#L37-L68) +- [backend/app/services/query.py:12-129](file://backend/app/services/query.py#L12-L129) +- [backend/app/services/citation.py:24-268](file://backend/app/services/citation.py#L24-L268) + +## 详细组件分析 + +### 认证接口 +- 接口前缀:/api/v1/auth +- 路由与功能: + - POST /register:用户注册,返回用户信息 + - POST /login:用户登录,返回JWT访问令牌与用户信息 + - GET /me:获取当前登录用户信息 +- 认证机制: + - 使用OAuth2密码流,令牌类型为bearer + - 通过依赖注入获取当前用户,校验JWT有效性 + - 用户名/密码验证通过后签发JWT,包含用户ID作为sub字段 +- 请求参数与响应格式: + - 注册:邮箱、密码、姓名 + - 登录:邮箱、密码 + - 当前用户:用户ID、邮箱、姓名、计划、最大查询数、激活状态、创建时间 + - 访问令牌:访问令牌字符串、令牌类型、用户信息 +- 错误处理: + - 注册:邮箱已存在时返回400 + - 登录:凭据无效时返回401 + - 获取当前用户:凭据无效或用户不存在时返回401 + +```mermaid +sequenceDiagram +participant Client as "客户端" +participant AuthAPI as "认证API" +participant AuthService as "认证服务" +participant DB as "数据库" +Client->>AuthAPI : POST /api/v1/auth/register +AuthAPI->>AuthService : 注册用户 +AuthService->>DB : 检查邮箱唯一性 +DB-->>AuthService : 结果 +AuthService->>DB : 创建用户并存储密码哈希 +DB-->>AuthService : 新用户 +AuthService-->>AuthAPI : 用户信息 +AuthAPI-->>Client : 201 用户信息 +Client->>AuthAPI : POST /api/v1/auth/login +AuthAPI->>AuthService : 验证用户名/密码 +AuthService->>DB : 查询用户 +DB-->>AuthService : 用户 +AuthService->>AuthService : 校验密码 +AuthService->>AuthService : 生成JWT +AuthService-->>AuthAPI : 访问令牌与用户信息 +AuthAPI-->>Client : 200 访问令牌 +``` + +**图表来源** +- [backend/app/api/auth.py:13-37](file://backend/app/api/auth.py#L13-L37) +- [backend/app/services/auth.py:37-68](file://backend/app/services/auth.py#L37-L68) +- [backend/app/api/deps.py:16-42](file://backend/app/api/deps.py#L16-L42) + +**章节来源** +- [backend/app/api/auth.py:13-42](file://backend/app/api/auth.py#L13-L42) +- [backend/app/api/deps.py:16-42](file://backend/app/api/deps.py#L16-L42) +- [backend/app/schemas/auth.py:7-34](file://backend/app/schemas/auth.py#L7-L34) +- [backend/app/services/auth.py:37-68](file://backend/app/services/auth.py#L37-L68) + +### 查询管理接口 +- 接口前缀:/api/v1/queries +- 路由与功能: + - GET /:分页列出当前用户的查询词 + - POST /:创建新的查询词 + - GET /{query_id}:获取单个查询词详情 + - PUT /{query_id}:更新查询词 + - DELETE /{query_id}:删除查询词 +- 权限与限制: + - 基于当前用户上下文进行资源所有权校验 + - 创建查询时检查用户最大查询数限制 +- 请求参数与响应格式: + - 查询创建:关键词、目标品牌、品牌别名、平台列表、频率 + - 查询更新:可选字段包括关键词、目标品牌、品牌别名、平台列表、频率、状态 + - 查询详情:包含创建/更新时间、状态、下次查询时间等 + - 列表响应:items与total总数 +- 错误处理: + - 未找到查询时返回404 + - 超出查询配额返回403 + - 参数校验失败返回422(Pydantic验证) + +```mermaid +flowchart TD +Start(["创建查询"]) --> CheckCount["检查用户查询数量"] +CheckCount --> CountOK{"是否超过上限?"} +CountOK --> |是| Raise403["抛出403 错误"] +CountOK --> |否| CalcNext["根据频率计算下次查询时间"] +CalcNext --> CreateQuery["创建查询记录"] +CreateQuery --> Commit["提交事务"] +Commit --> Return["返回新查询"] +Raise403 --> End(["结束"]) +Return --> End +``` + +**图表来源** +- [backend/app/services/query.py:45-81](file://backend/app/services/query.py#L45-L81) + +**章节来源** +- [backend/app/api/queries.py:15-85](file://backend/app/api/queries.py#L15-L85) +- [backend/app/schemas/query.py:11-94](file://backend/app/schemas/query.py#L11-L94) +- [backend/app/services/query.py:12-129](file://backend/app/services/query.py#L12-L129) + +### 引用数据接口 +- 接口前缀:/api/v1/citations +- 路由与功能: + - GET /:分页查询引用记录,支持按查询ID、平台、时间范围过滤 + - GET /stats:统计分析,返回总查询数、引用率、平均位置、按平台分布、30天趋势 + - POST /{query_id}/run-now:立即触发查询任务,返回任务ID与状态 +- 数据模型: + - 引用记录包含平台、是否引用、引用位置、引用文本、竞争品牌列表、查询时间等 +- 统计逻辑: + - 总查询数与引用数来自关联查询统计 + - 引用率=引用数/总查询数 + - 平均位置仅对有位置的引用记录计算 + - 按平台统计查询数、引用数、引用率与平均位置 + - 趋势按周聚合过去30天的引用数 +- 错误处理: + - 查询所有权校验失败返回404 + - 查询非活跃或未配置平台时返回404 + - 参数校验失败返回422 + +```mermaid +sequenceDiagram +participant Client as "客户端" +participant CitAPI as "引用数据API" +participant CitService as "引用数据服务" +participant DB as "数据库" +Client->>CitAPI : POST /api/v1/citations/{query_id}/run-now +CitAPI->>CitService : 触发查询任务 +CitService->>DB : 校验查询所有权与状态 +DB-->>CitService : 查询结果 +CitService->>DB : 为每个平台创建任务 +DB-->>CitService : 任务集合 +CitService-->>CitAPI : 返回首个任务 +CitAPI-->>Client : 202 任务信息 +``` + +**图表来源** +- [backend/app/api/citations.py:59-77](file://backend/app/api/citations.py#L59-L77) +- [backend/app/services/citation.py:204-234](file://backend/app/services/citation.py#L204-L234) + +**章节来源** +- [backend/app/api/citations.py:25-77](file://backend/app/api/citations.py#L25-L77) +- [backend/app/schemas/citation.py:7-50](file://backend/app/schemas/citation.py#L7-L50) +- [backend/app/services/citation.py:24-268](file://backend/app/services/citation.py#L24-L268) + +### 报告导出接口 +- 接口前缀:/api/v1/reports +- 路由与功能: + - GET /export/csv:导出指定查询的引用记录为CSV文件 +- 输出格式: + - 文本CSV,包含列头:日期、平台、是否引用、引用位置、引用文本、竞争品牌 + - 文件名为geo-report-YYYYMMDD.csv +- 错误处理: + - 不支持的格式返回400 + - 查询所有权校验失败返回404 + +```mermaid +sequenceDiagram +participant Client as "客户端" +participant ReportAPI as "报告API" +participant ReportService as "报告服务" +participant DB as "数据库" +Client->>ReportAPI : GET /api/v1/reports/export/csv?query_id={id}&format=csv +ReportAPI->>ReportService : 导出CSV +ReportService->>DB : 校验查询所有权并查询记录 +DB-->>ReportService : 引用记录列表 +ReportService->>ReportService : 写入CSV内容 +ReportService-->>ReportAPI : CSV字符串 +ReportAPI-->>Client : 200 CSV文件下载 +``` + +**图表来源** +- [backend/app/api/reports.py:16-46](file://backend/app/api/reports.py#L16-L46) +- [backend/app/services/citation.py:237-268](file://backend/app/services/citation.py#L237-L268) + +**章节来源** +- [backend/app/api/reports.py:16-46](file://backend/app/api/reports.py#L16-L46) +- [backend/app/services/citation.py:237-268](file://backend/app/services/citation.py#L237-L268) + +## 依赖分析 +- 中间件与认证: + - OAuth2密码流,令牌URL为/api/v1/auth/login + - 依赖注入从Header中解析Authorization: Bearer token + - 通过JWT解码校验用户身份,查询数据库获取用户实体 +- 数据模型关系: + - User与Query一对多,级联删除 + - Query与CitationRecord、QueryTask一对多,级联删除 + - CitationRecord外键关联Query +- 服务层职责: + - 认证服务:密码哈希、JWT签发与校验、用户注册与登录 + - 查询服务:分页查询、创建/更新/删除、频率与下次查询时间计算 + - 引用服务:引用记录查询、统计分析、立即执行任务、CSV导出 + +```mermaid +classDiagram +class User { ++UUID id ++string email ++string password_hash ++string name ++string plan ++int max_queries ++boolean is_active ++datetime created_at ++datetime updated_at +} +class Query { ++UUID id ++UUID user_id ++string keyword ++string target_brand ++list brand_aliases ++list platforms ++string frequency ++string status ++datetime last_queried_at ++datetime next_query_at ++datetime created_at ++datetime updated_at +} +class CitationRecord { ++UUID id ++UUID query_id ++string platform ++boolean cited ++int citation_position ++string citation_text ++list competitor_brands ++string raw_response ++datetime queried_at +} +class QueryTask { ++UUID id ++UUID query_id ++string platform ++string status ++string error_message ++datetime scheduled_at ++datetime started_at ++datetime completed_at +} +User "1" --> "many" Query : "拥有" +Query "1" --> "many" CitationRecord : "产生" +Query "1" --> "many" QueryTask : "触发任务" +CitationRecord "many" --> "1" Query : "属于" +``` + +**图表来源** +- [backend/app/models/user.py:11-41](file://backend/app/models/user.py#L11-L41) +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [backend/app/models/citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) +- [backend/app/models/query_task.py:11-39](file://backend/app/models/query_task.py#L11-L39) + +**章节来源** +- [backend/app/api/deps.py:16-42](file://backend/app/api/deps.py#L16-L42) +- [backend/app/models/user.py:35-40](file://backend/app/models/user.py#L35-L40) +- [backend/app/models/query.py:43-48](file://backend/app/models/query.py#L43-L48) +- [backend/app/models/citation_record.py:35](file://backend/app/models/citation_record.py#L35) +- [backend/app/models/query_task.py:34](file://backend/app/models/query_task.py#L34) + +## 性能考虑 +- 分页查询:列表接口默认每页20条,支持skip/limit参数,避免一次性加载大量数据。 +- 索引优化:查询与引用记录表建立复合索引,提升按用户ID、状态、时间等条件的查询性能。 +- 异步数据库:使用异步SQLAlchemy,提高并发处理能力。 +- 缓存策略:可在服务层引入Redis缓存热点统计数据,降低重复计算成本。 +- 批量操作:任务创建采用批量插入,减少事务开销。 + +## 故障排除指南 +- 认证相关 + - 401 未授权:检查Authorization头是否正确携带Bearer token;确认JWT签名与过期时间有效。 + - 401 凭据无效:检查用户名/密码是否正确;确认用户存在且被激活。 +- 查询管理 + - 403 超出配额:检查用户计划与max_queries限制;清理或升级计划以增加配额。 + - 404 未找到:确认query_id归属当前用户;检查路径参数是否正确。 +- 引用数据 + - 404 查询不可用:确认查询状态为active且配置了至少一个平台。 + - 统计为空:当按查询ID过滤但无匹配记录时,返回零值统计。 +- 报告导出 + - 400 不支持的格式:确保format参数为csv。 + - 404 查询不存在:确认query_id归属当前用户。 + +**章节来源** +- [backend/app/api/auth.py:26-30](file://backend/app/api/auth.py#L26-L30) +- [backend/app/services/query.py:59-60](file://backend/app/services/query.py#L59-L60) +- [backend/app/api/citations.py:67-71](file://backend/app/api/citations.py#L67-L71) +- [backend/app/api/reports.py:23-27](file://backend/app/api/reports.py#L23-L27) + +## 结论 +GEO平台API采用清晰的模块化设计,围绕用户、查询、引用与报告四大领域构建RESTful接口。通过JWT认证与严格的资源所有权校验,保障了数据安全;通过统计分析与CSV导出,满足了业务洞察与合规需求。建议在生产环境中进一步完善错误日志、监控指标与缓存策略,持续优化查询性能与用户体验。 \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/API接口文档/引用数据接口.md b/.qoder/repowiki/zh/content/API接口文档/引用数据接口.md new file mode 100644 index 0000000..9380c37 --- /dev/null +++ b/.qoder/repowiki/zh/content/API接口文档/引用数据接口.md @@ -0,0 +1,590 @@ +# 引用数据接口 + + +**本文档引用的文件** +- [backend/app/api/citations.py](file://backend/app/api/citations.py) +- [backend/app/api/queries.py](file://backend/app/api/queries.py) +- [backend/app/api/reports.py](file://backend/app/api/reports.py) +- [backend/app/schemas/citation.py](file://backend/app/schemas/citation.py) +- [backend/app/schemas/query.py](file://backend/app/schemas/query.py) +- [backend/app/services/citation.py](file://backend/app/services/citation.py) +- [backend/app/services/query.py](file://backend/app/services/query.py) +- [backend/app/models/citation_record.py](file://backend/app/models/citation_record.py) +- [backend/app/models/query.py](file://backend/app/models/query.py) +- [backend/app/workers/citation_engine.py](file://backend/app/workers/citation_engine.py) +- [backend/app/workers/platforms/base.py](file://backend/app/workers/platforms/base.py) +- [backend/app/workers/platforms/kimi.py](file://backend/app/workers/platforms/kimi.py) +- [backend/app/workers/platforms/wenxin.py](file://backend/app/workers/platforms/wenxin.py) +- [frontend/lib/api.ts](file://frontend/lib/api.ts) +- [frontend/components/charts/trend-chart.tsx](file://frontend/components/charts/trend-chart.tsx) +- [frontend/components/charts/platform-chart.tsx](file://frontend/components/charts/platform-chart.tsx) +- [frontend/lib/platforms.ts](file://frontend/lib/platforms.ts) + + +## 目录 +1. [简介](#简介) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖分析](#依赖分析) +7. [性能考虑](#性能考虑) +8. [故障排查指南](#故障排查指南) +9. [结论](#结论) +10. [附录](#附录) + +## 简介 +本文件面向“引用数据查询与分析”场景,提供完整的API文档与实现解析。内容覆盖: +- 引用数据查询接口:支持按查询任务、平台、时间范围过滤,分页与排序 +- 统计分析功能:总查询数、引用数、引用率、平均位置、按平台统计、近30天趋势 +- 上下文提取机制:品牌识别、竞争品牌识别、置信度评分 +- 趋势分析与平台对比:通过统计接口与前端图表组件展示 +- 报告导出:CSV导出能力 +- 数据过滤、排序与分页参数说明 +- 前端接口调用与可视化使用指南 + +## 项目结构 +后端采用FastAPI + SQLAlchemy异步ORM,前端Next.js + Recharts,核心模块如下: +- API层:路由定义与参数校验 +- 服务层:业务逻辑与数据库查询 +- 模型层:SQLAlchemy ORM映射 +- 工作器:AI平台适配器与引用检测引擎 +- 前端:API封装、图表组件与平台映射 + +```mermaid +graph TB +subgraph "前端" +FE_API["前端API封装
lib/api.ts"] +FE_Trend["趋势图组件
charts/trend-chart.tsx"] +FE_Platform["平台柱状图组件
charts/platform-chart.tsx"] +FE_PlatformMap["平台映射
lib/platforms.ts"] +end +subgraph "后端" +API_Cit["引用API
app/api/citations.py"] +API_Query["查询API
app/api/queries.py"] +API_Report["报告API
app/api/reports.py"] +Svc_Cit["引用服务
app/services/citation.py"] +Svc_Query["查询服务
app/services/query.py"] +Model_Cit["引用记录模型
app/models/citation_record.py"] +Model_Query["查询模型
app/models/query.py"] +Engine["引用检测引擎
app/workers/citation_engine.py"] +Plat_Base["平台适配器基类
app/workers/platforms/base.py"] +Plat_Kimi["Kimi适配器
app/workers/platforms/kimi.py"] +Plat_Wenxin["文心一言适配器
app/workers/platforms/wenxin.py"] +end +FE_API --> API_Cit +FE_API --> API_Query +FE_API --> API_Report +FE_Trend --> FE_API +FE_Platform --> FE_API +FE_PlatformMap --> FE_Platform +API_Cit --> Svc_Cit +API_Query --> Svc_Query +API_Report --> Svc_Cit +Svc_Cit --> Model_Cit +Svc_Query --> Model_Query +Engine --> Plat_Base +Engine --> Plat_Kimi +Engine --> Plat_Wenxin +``` + +**图表来源** +- [backend/app/api/citations.py:1-78](file://backend/app/api/citations.py#L1-L78) +- [backend/app/api/queries.py:1-86](file://backend/app/api/queries.py#L1-L86) +- [backend/app/api/reports.py:1-47](file://backend/app/api/reports.py#L1-L47) +- [backend/app/services/citation.py:1-269](file://backend/app/services/citation.py#L1-L269) +- [backend/app/services/query.py:1-130](file://backend/app/services/query.py#L1-L130) +- [backend/app/models/citation_record.py:1-42](file://backend/app/models/citation_record.py#L1-L42) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) +- [backend/app/workers/platforms/base.py:1-18](file://backend/app/workers/platforms/base.py#L1-L18) +- [backend/app/workers/platforms/kimi.py:1-206](file://backend/app/workers/platforms/kimi.py#L1-L206) +- [backend/app/workers/platforms/wenxin.py:1-205](file://backend/app/workers/platforms/wenxin.py#L1-L205) +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) +- [frontend/components/charts/trend-chart.tsx:1-60](file://frontend/components/charts/trend-chart.tsx#L1-L60) +- [frontend/components/charts/platform-chart.tsx:1-68](file://frontend/components/charts/platform-chart.tsx#L1-L68) +- [frontend/lib/platforms.ts:1-18](file://frontend/lib/platforms.ts#L1-L18) + +**章节来源** +- [backend/app/api/citations.py:1-78](file://backend/app/api/citations.py#L1-L78) +- [backend/app/api/queries.py:1-86](file://backend/app/api/queries.py#L1-L86) +- [backend/app/api/reports.py:1-47](file://backend/app/api/reports.py#L1-L47) +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) + +## 核心组件 +- 引用查询接口:支持按query_id、platform、start_date、end_date过滤,分页skip/limit,按时间倒序 +- 统计分析接口:总查询数、总引用数、引用率、平均位置、按平台统计、近30天趋势 +- 即时查询触发:向队列提交查询任务 +- 报告导出:CSV导出引用记录 +- 品牌识别与上下文提取:精确/别名/模糊匹配,置信度评分,上下文片段与段落位置,竞争品牌识别 + +**章节来源** +- [backend/app/api/citations.py:25-78](file://backend/app/api/citations.py#L25-L78) +- [backend/app/schemas/citation.py:1-50](file://backend/app/schemas/citation.py#L1-L50) +- [backend/app/services/citation.py:24-269](file://backend/app/services/citation.py#L24-L269) +- [backend/app/workers/citation_engine.py:19-309](file://backend/app/workers/citation_engine.py#L19-L309) + +## 架构总览 +后端通过API路由接收请求,经服务层进行权限校验与数据聚合,模型层负责数据库访问;引用检测由工作器引擎驱动,调用各平台适配器获取AI响应,再进行品牌匹配与竞争品牌识别。 + +```mermaid +sequenceDiagram +participant Client as "客户端" +participant API as "引用API" +participant Service as "引用服务" +participant DB as "数据库" +participant Engine as "引用检测引擎" +participant Adapter as "平台适配器" +Client->>API : GET /api/v1/citations +API->>Service : get_citations(...) +Service->>DB : 查询引用记录(带过滤+分页) +DB-->>Service : 记录列表与总数 +Service-->>API : {items,total} +API-->>Client : JSON响应 +Client->>API : POST /api/v1/citations/{query_id}/run-now +API->>Service : trigger_query_now(...) +Service->>Engine : 执行查询与检测 +Engine->>Adapter : 平台查询(keyword) +Adapter-->>Engine : 原始响应文本 +Engine-->>Service : 引用检测结果 +Service-->>API : 任务信息 +API-->>Client : 任务ID/状态 +``` + +**图表来源** +- [backend/app/api/citations.py:25-78](file://backend/app/api/citations.py#L25-L78) +- [backend/app/services/citation.py:204-234](file://backend/app/services/citation.py#L204-L234) +- [backend/app/workers/citation_engine.py:159-234](file://backend/app/workers/citation_engine.py#L159-L234) +- [backend/app/workers/platforms/base.py:10-17](file://backend/app/workers/platforms/base.py#L10-L17) + +## 详细组件分析 + +### 引用查询接口 +- 路径与方法 + - GET /api/v1/citations +- 查询参数 + - query_id: UUID,可选,按所属查询任务过滤 + - platform: 字符串,可选,按平台过滤 + - start_date: 日期时间,可选,起始时间 + - end_date: 日期时间,可选,结束时间 + - skip: 整数,>=0,默认0 + - limit: 整数,>=1且<=100,默认20 +- 排序规则:按 queried_at 降序 +- 响应体 + - items: 引用记录数组 + - total: 符合条件的总记录数 +- 权限与安全 + - 仅返回当前用户拥有的查询所对应的引用记录 + - 当提供query_id时,会额外校验该查询是否属于当前用户 + +```mermaid +flowchart TD +Start(["进入 list_citations"]) --> Build["构建基础条件: 用户ID过滤"] +Build --> CheckQueryID{"是否提供 query_id?"} +CheckQueryID --> |是| Verify["校验查询归属(用户ID)"] +Verify --> |不存在| ReturnEmpty["返回空结果与total=0"] +Verify --> |存在| AddFilter["追加 query_id 过滤"] +CheckQueryID --> |否| AddPlatform["追加 platform 过滤(可选)"] +AddFilter --> AddPlatform +AddPlatform --> AddTime["追加 start_date/end_date 过滤(可选)"] +AddTime --> QueryDB["查询+分页+排序"] +QueryDB --> Count["统计总数"] +Count --> Return["返回 items 与 total"] +``` + +**图表来源** +- [backend/app/api/citations.py:25-46](file://backend/app/api/citations.py#L25-L46) +- [backend/app/services/citation.py:24-73](file://backend/app/services/citation.py#L24-L73) + +**章节来源** +- [backend/app/api/citations.py:25-46](file://backend/app/api/citations.py#L25-L46) +- [backend/app/services/citation.py:24-73](file://backend/app/services/citation.py#L24-L73) + +### 引用统计分析接口 +- 路径与方法 + - GET /api/v1/citations/stats +- 查询参数 + - query_id: UUID,可选,限定到特定查询任务 +- 响应体字段 + - total_queries: 总查询数 + - total_citations: 总引用数 + - citation_rate: 引用率 = total_citations / total_queries + - avg_position: 平均位置(仅对有位置的引用计算) + - by_platform: 各平台统计,含 queries、citations、rate、avg_position + - trend: 近30天按周统计的引用数列表 +- 权限与安全 + - 当提供query_id时,会校验查询归属 + +```mermaid +flowchart TD +Start(["进入 get_citation_stats"]) --> Verify{"是否提供 query_id?"} +Verify --> |是| CheckOwner["校验查询归属"] +CheckOwner --> |不存在| Empty["返回全零统计"] +CheckOwner --> |存在| SetBase["设置基础条件(用户ID+query_id)"] +Verify --> |否| SetBase +SetBase --> Totals["统计 total_queries/total_citations"] +Totals --> AvgPos["计算 avg_position(仅cited且有位置)"] +AvgPos --> ByPlatform["按平台 group 统计"] +ByPlatform --> Trend["近30天按周统计"] +Trend --> Return["返回统计结果"] +``` + +**图表来源** +- [backend/app/api/citations.py:49-56](file://backend/app/api/citations.py#L49-L56) +- [backend/app/services/citation.py:76-201](file://backend/app/services/citation.py#L76-L201) + +**章节来源** +- [backend/app/api/citations.py:49-56](file://backend/app/api/citations.py#L49-L56) +- [backend/app/schemas/citation.py:25-44](file://backend/app/schemas/citation.py#L25-L44) +- [backend/app/services/citation.py:76-201](file://backend/app/services/citation.py#L76-L201) + +### 即时查询触发接口 +- 路径与方法 + - POST /api/v1/citations/{query_id}/run-now +- 行为 + - 校验查询归属与状态(必须为active) + - 为配置的每个平台创建一个QueryTask(状态pending) + - 返回任务ID与状态 +- 错误处理 + - 查询不存在或不属于当前用户:404 + - 查询状态非active:404 + - 无平台配置:404 + +```mermaid +sequenceDiagram +participant Client as "客户端" +participant API as "引用API" +participant Service as "引用服务" +participant DB as "数据库" +participant Engine as "引用检测引擎" +Client->>API : POST /citations/{query_id}/run-now +API->>Service : trigger_query_now(user_id, query_id) +Service->>DB : 校验查询归属与状态 +DB-->>Service : 查询对象 +Service->>Engine : 为每个平台创建QueryTask +Engine-->>Service : 返回首个任务 +Service-->>API : 任务ID/状态 +API-->>Client : 202 Accepted + 任务信息 +``` + +**图表来源** +- [backend/app/api/citations.py:59-78](file://backend/app/api/citations.py#L59-L78) +- [backend/app/services/citation.py:204-234](file://backend/app/services/citation.py#L204-L234) +- [backend/app/workers/citation_engine.py:268-289](file://backend/app/workers/citation_engine.py#L268-L289) + +**章节来源** +- [backend/app/api/citations.py:59-78](file://backend/app/api/citations.py#L59-L78) +- [backend/app/services/citation.py:204-234](file://backend/app/services/citation.py#L204-L234) + +### 报告导出接口 +- 路径与方法 + - GET /api/v1/reports/export/csv +- 查询参数 + - query_id: UUID,必填 + - format: 字符串,当前仅支持csv +- 响应 + - Content-Type: text/csv + - Content-Disposition: 附件下载 + - 内容: CSV表格(日期、平台、是否引用、引用位置、引用文本、竞争品牌) + +**章节来源** +- [backend/app/api/reports.py:16-47](file://backend/app/api/reports.py#L16-L47) +- [backend/app/services/citation.py:237-269](file://backend/app/services/citation.py#L237-L269) + +### 查询管理接口(用于关联引用数据) +- 路径与方法 + - GET /api/v1/queries + - POST /api/v1/queries + - GET /api/v1/queries/{query_id} + - PUT /api/v1/queries/{query_id} + - DELETE /api/v1/queries/{query_id} +- 分页参数 + - skip: >=0,默认0 + - limit: >=1且<=100,默认20 +- 响应体 + - 查询列表/详情,包含keyword、target_brand、brand_aliases、platforms、frequency、status等 + +**章节来源** +- [backend/app/api/queries.py:15-86](file://backend/app/api/queries.py#L15-L86) +- [backend/app/schemas/query.py:11-94](file://backend/app/schemas/query.py#L11-L94) +- [backend/app/services/query.py:12-130](file://backend/app/services/query.py#L12-L130) + +### 数据模型与关系 +- 引用记录模型 + - 关键字段:query_id、platform、cited、citation_position、citation_text、competitor_brands、raw_response、queried_at + - 索引:query_id、queried_at、platform +- 查询模型 + - 关键字段:user_id、keyword、target_brand、brand_aliases、platforms、frequency、status、last_queried_at、next_query_at、created_at、updated_at + - 索引:user_id、status、next_query_at + +```mermaid +erDiagram +QUERIES { +uuid id PK +uuid user_id FK +string keyword +string target_brand +jsonb brand_aliases +jsonb platforms +string frequency +string status +datetime last_queried_at +datetime next_query_at +datetime created_at +datetime updated_at +} +CITATION_RECORDS { +uuid id PK +uuid query_id FK +string platform +boolean cited +int citation_position +text citation_text +jsonb competitor_brands +text raw_response +datetime queried_at +} +QUERIES ||--o{ CITATION_RECORDS : "拥有" +``` + +**图表来源** +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [backend/app/models/citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) + +**章节来源** +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [backend/app/models/citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) + +### 引用检测与上下文提取机制 +- 品牌匹配器 + - 支持精确匹配、别名匹配、模糊匹配 + - 输出:cited、confidence、match_type、position、citation_text + - 置信度:精确=1.0,别名=0.9,模糊按相似度取值 +- 竞争品牌检测 + - 基于预定义行业品牌集合,排除目标品牌后返回其他品牌列表 +- 上下文提取 + - 按段落定位品牌首次出现位置(1-based) + - 提取不超过200字符的上下文片段 +- 引擎流程 + - 为每个平台创建/获取QueryTask,更新状态为running + - 调用平台适配器获取原始响应 + - 使用BrandMatcher进行检测,CompetitorDetector提取竞争品牌 + - 写入CitationRecord,更新Query的时间字段 + +```mermaid +classDiagram +class BrandMatcher { ++match(text) dict +-_extract_candidates(text) list +-_extract_position_and_context(text, keyword) tuple +} +class CompetitorDetector { ++detect(text, target_brand) list +} +class CitationEngine { ++execute_query(query, db) list ++execute_single_platform(keyword, platform, target_brand, brand_aliases) dict +-_get_or_create_task(db, query_id, platform) QueryTask +-_calculate_next_query_at(frequency) datetime +} +class BasePlatformAdapter { +<> ++query(keyword) str ++close() +} +class KimiAdapter { ++query(keyword) str ++close() +} +class WenxinAdapter { ++query(keyword) str ++close() +} +CitationEngine --> BrandMatcher : "使用" +CitationEngine --> CompetitorDetector : "使用" +CitationEngine --> BasePlatformAdapter : "依赖" +KimiAdapter --|> BasePlatformAdapter +WenxinAdapter --|> BasePlatformAdapter +``` + +**图表来源** +- [backend/app/workers/citation_engine.py:19-309](file://backend/app/workers/citation_engine.py#L19-L309) +- [backend/app/workers/platforms/base.py:4-17](file://backend/app/workers/platforms/base.py#L4-L17) +- [backend/app/workers/platforms/kimi.py:11-206](file://backend/app/workers/platforms/kimi.py#L11-L206) +- [backend/app/workers/platforms/wenxin.py:11-205](file://backend/app/workers/platforms/wenxin.py#L11-L205) + +**章节来源** +- [backend/app/workers/citation_engine.py:19-309](file://backend/app/workers/citation_engine.py#L19-L309) + +### 前端接口调用与可视化 +- API封装 + - 提供查询列表、统计、报告导出等方法 + - 自动附加Authorization头 +- 趋势图组件 + - 接收trend数组,按周展示引用次数 +- 平台柱状图组件 + - 接收by_platform统计,按平台引用率展示 +- 平台映射 + - 将平台枚举转换为中文标签 + +**章节来源** +- [frontend/lib/api.ts:23-57](file://frontend/lib/api.ts#L23-L57) +- [frontend/components/charts/trend-chart.tsx:13-59](file://frontend/components/charts/trend-chart.tsx#L13-L59) +- [frontend/components/charts/platform-chart.tsx:15-67](file://frontend/components/charts/platform-chart.tsx#L15-L67) +- [frontend/lib/platforms.ts:1-18](file://frontend/lib/platforms.ts#L1-L18) + +## 依赖分析 +- API层依赖服务层与认证依赖注入 +- 服务层依赖模型层与数据库会话 +- 引擎依赖平台适配器,适配器继承抽象基类 +- 前端依赖后端API与图表库 + +```mermaid +graph LR +API_Cit["citations.py"] --> Svc_Cit["services/citation.py"] +API_Query["queries.py"] --> Svc_Query["services/query.py"] +API_Report["reports.py"] --> Svc_Cit +Svc_Cit --> Model_Cit["models/citation_record.py"] +Svc_Query --> Model_Query["models/query.py"] +Svc_Cit --> Engine["workers/citation_engine.py"] +Engine --> Plat_Base["platforms/base.py"] +Engine --> Plat_Kimi["platforms/kimi.py"] +Engine --> Plat_Wenxin["platforms/wenxin.py"] +FE_API["frontend/lib/api.ts"] --> API_Cit +FE_API --> API_Query +FE_API --> API_Report +FE_Trend["trend-chart.tsx"] --> FE_API +FE_Platform["platform-chart.tsx"] --> FE_API +``` + +**图表来源** +- [backend/app/api/citations.py:1-78](file://backend/app/api/citations.py#L1-L78) +- [backend/app/api/queries.py:1-86](file://backend/app/api/queries.py#L1-L86) +- [backend/app/api/reports.py:1-47](file://backend/app/api/reports.py#L1-L47) +- [backend/app/services/citation.py:1-269](file://backend/app/services/citation.py#L1-L269) +- [backend/app/services/query.py:1-130](file://backend/app/services/query.py#L1-L130) +- [backend/app/models/citation_record.py:1-42](file://backend/app/models/citation_record.py#L1-L42) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) +- [backend/app/workers/platforms/base.py:1-18](file://backend/app/workers/platforms/base.py#L1-L18) +- [backend/app/workers/platforms/kimi.py:1-206](file://backend/app/workers/platforms/kimi.py#L1-L206) +- [backend/app/workers/platforms/wenxin.py:1-205](file://backend/app/workers/platforms/wenxin.py#L1-L205) +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) +- [frontend/components/charts/trend-chart.tsx:1-60](file://frontend/components/charts/trend-chart.tsx#L1-L60) +- [frontend/components/charts/platform-chart.tsx:1-68](file://frontend/components/charts/platform-chart.tsx#L1-L68) + +**章节来源** +- [backend/app/api/citations.py:1-78](file://backend/app/api/citations.py#L1-L78) +- [backend/app/api/queries.py:1-86](file://backend/app/api/queries.py#L1-L86) +- [backend/app/api/reports.py:1-47](file://backend/app/api/reports.py#L1-L47) +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) + +## 性能考虑 +- 分页与索引 + - 引用记录表对query_id、queried_at、platform建立索引,提升过滤与排序效率 + - 查询接口默认limit=20,最大100,避免一次性返回过多数据 +- 统计查询 + - 使用SQL聚合函数与分组,减少Python侧计算量 + - 趋势按周聚合,降低前端渲染压力 +- 并发与重试 + - 平台适配器对查询过程进行最多3次重试与指数退避,提高稳定性 +- 引擎批处理 + - 触发即时查询时为每个平台创建任务,便于后续并发执行 + +[本节为通用建议,无需具体文件分析] + +## 故障排查指南 +- 404 Not Found + - 查询不存在或不属于当前用户(统计与导出均会校验归属) + - 即时查询时查询状态非active或未配置平台 +- 400 Bad Request + - 报告导出format非csv +- 403 Forbidden + - 创建查询超过用户最大配额限制 +- 平台适配器错误 + - Playwright浏览器未安装或启动失败 + - 页面交互超时,检查网络与平台可用性 +- 结果为空 + - 过滤条件过于严格(如query_id、platform、时间范围) + - 查询尚未产生任何引用记录 + +**章节来源** +- [backend/app/api/reports.py:23-27](file://backend/app/api/reports.py#L23-L27) +- [backend/app/services/query.py:45-81](file://backend/app/services/query.py#L45-L81) +- [backend/app/workers/platforms/kimi.py:21-32](file://backend/app/workers/platforms/kimi.py#L21-L32) +- [backend/app/workers/platforms/wenxin.py:21-32](file://backend/app/workers/platforms/wenxin.py#L21-L32) +- [backend/app/services/citation.py:204-234](file://backend/app/services/citation.py#L204-L234) + +## 结论 +本系统提供了完整的引用数据查询、统计与导出能力,并内置品牌识别与竞争品牌检测机制。通过清晰的API设计、完善的权限校验与数据模型,以及前后端协同的可视化组件,能够满足日常的品牌监测与竞品分析需求。建议在生产环境中关注平台适配器的稳定性与数据库索引策略,以进一步提升性能与可靠性。 + +[本节为总结性内容,无需具体文件分析] + +## 附录 + +### API端点一览 +- 引用查询 + - GET /api/v1/citations + - 参数:query_id、platform、start_date、end_date、skip、limit + - 响应:items、total +- 引用统计 + - GET /api/v1/citations/stats + - 参数:query_id + - 响应:总览与分项统计 +- 即时查询 + - POST /api/v1/citations/{query_id}/run-now + - 响应:task_id、status、message +- 报告导出 + - GET /api/v1/reports/export/csv + - 参数:query_id、format(csv) + - 响应:CSV流 +- 查询管理 + - GET /api/v1/queries + - POST /api/v1/queries + - GET /api/v1/queries/{query_id} + - PUT /api/v1/queries/{query_id} + - DELETE /api/v1/queries/{query_id} + +**章节来源** +- [backend/app/api/citations.py:25-78](file://backend/app/api/citations.py#L25-L78) +- [backend/app/api/reports.py:16-47](file://backend/app/api/reports.py#L16-L47) +- [backend/app/api/queries.py:15-86](file://backend/app/api/queries.py#L15-L86) + +### 数据过滤、排序与分页参数 +- 过滤 + - query_id:按所属查询任务过滤 + - platform:按平台过滤 + - start_date/end_date:按时间范围过滤 +- 排序 + - 默认按 queried_at 降序 +- 分页 + - skip:>=0 + - limit:>=1且<=100 + +**章节来源** +- [backend/app/api/citations.py:25-46](file://backend/app/api/citations.py#L25-L46) +- [backend/app/services/citation.py:24-73](file://backend/app/services/citation.py#L24-L73) + +### 品牌识别与置信度评分 +- 匹配类型与置信度 + - 精确匹配:1.0 + - 别名匹配:0.9 + - 模糊匹配:按相似度取值(>0.4视为匹配) +- 输出字段 + - cited、confidence、match_type、position、citation_text、competitor_brands + +**章节来源** +- [backend/app/workers/citation_engine.py:26-120](file://backend/app/workers/citation_engine.py#L26-L120) + +### 平台对比与趋势分析 +- 平台对比 + - by_platform包含各平台queries、citations、rate、avg_position + - 前端使用柱状图组件展示引用率 +- 趋势分析 + - trend为近30天按周统计的citations + - 前端使用折线图组件展示 + +**章节来源** +- [backend/app/schemas/citation.py:25-44](file://backend/app/schemas/citation.py#L25-L44) +- [frontend/components/charts/platform-chart.tsx:34-67](file://frontend/components/charts/platform-chart.tsx#L34-L67) +- [frontend/components/charts/trend-chart.tsx:22-59](file://frontend/components/charts/trend-chart.tsx#L22-L59) \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/API接口文档/报告导出接口.md b/.qoder/repowiki/zh/content/API接口文档/报告导出接口.md new file mode 100644 index 0000000..813a49b --- /dev/null +++ b/.qoder/repowiki/zh/content/API接口文档/报告导出接口.md @@ -0,0 +1,366 @@ +# 报告导出接口 + + +**本文档引用的文件** +- [backend/app/api/reports.py](file://backend/app/api/reports.py) +- [backend/app/services/citation.py](file://backend/app/services/citation.py) +- [backend/app/models/citation_record.py](file://backend/app/models/citation_record.py) +- [backend/app/models/query.py](file://backend/app/models/query.py) +- [backend/app/models/query_task.py](file://backend/app/models/query_task.py) +- [backend/app/workers/citation_engine.py](file://backend/app/workers/citation_engine.py) +- [backend/app/workers/scheduler.py](file://backend/app/workers/scheduler.py) +- [backend/app/workers/platforms/base.py](file://backend/app/workers/platforms/base.py) +- [backend/app/main.py](file://backend/app/main.py) +- [frontend/lib/api.ts](file://frontend/lib/api.ts) +- [frontend/app/(dashboard)/dashboard/reports/page.tsx](file://frontend/app/(dashboard)/dashboard/reports/page.tsx) + + +## 目录 +1. [简介](#简介) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖关系分析](#依赖关系分析) +7. [性能考虑](#性能考虑) +8. [故障排查指南](#故障排查指南) +9. [结论](#结论) +10. [附录](#附录) + +## 简介 +本文件面向“报告导出系统”的使用者与维护者,提供完整的CSV格式数据导出API文档。内容涵盖: +- 导出流程与参数配置 +- 报告生成触发机制(手动触发、定时触发) +- 数据筛选条件与输出格式选项 +- 导出进度监控、错误处理与文件下载 +- 报告模板定制、数据格式转换与性能优化建议 + +## 项目结构 +后端采用FastAPI + SQLAlchemy异步ORM,前端基于Next.js。导出能力通过独立的报告路由暴露,并由服务层负责数据聚合与CSV生成。 + +```mermaid +graph TB +subgraph "前端" +FE_API["前端API封装
frontend/lib/api.ts"] +FE_PAGE["报告页面
frontend/app/(dashboard)/dashboard/reports/page.tsx"] +end +subgraph "后端" +MAIN["应用入口
backend/app/main.py"] +ROUTER["报告路由
backend/app/api/reports.py"] +SERVICE["导出服务
backend/app/services/citation.py"] +MODELS["模型定义
models/*.py"] +WORKERS["工作流与调度
workers/*.py"] +end +FE_PAGE --> FE_API +FE_API --> ROUTER +ROUTER --> SERVICE +SERVICE --> MODELS +WORKERS --> MODELS +MAIN --> ROUTER +MAIN --> WORKERS +``` + +图表来源 +- [backend/app/main.py:13-48](file://backend/app/main.py#L13-L48) +- [backend/app/api/reports.py:1-47](file://backend/app/api/reports.py#L1-L47) +- [backend/app/services/citation.py:237-269](file://backend/app/services/citation.py#L237-L269) +- [backend/app/models/citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [backend/app/models/query_task.py:11-39](file://backend/app/models/query_task.py#L11-L39) +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) +- [backend/app/workers/scheduler.py:25-95](file://backend/app/workers/scheduler.py#L25-L95) +- [frontend/lib/api.ts:51-57](file://frontend/lib/api.ts#L51-L57) +- [frontend/app/(dashboard)/dashboard/reports/page.tsx:49-93](file://frontend/app/(dashboard)/dashboard/reports/page.tsx#L49-L93) + +章节来源 +- [backend/app/main.py:13-48](file://backend/app/main.py#L13-L48) +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) +- [frontend/app/(dashboard)/dashboard/reports/page.tsx:1-198](file://frontend/app/(dashboard)/dashboard/reports/page.tsx#L1-L198) + +## 核心组件 +- 报告导出路由:提供CSV导出接口,接收查询词ID并返回流式CSV响应。 +- 导出服务:验证用户与查询所有权,查询引用记录并生成CSV字符串。 +- 数据模型:查询、引用记录、查询任务等,支撑筛选与统计。 +- 引擎与调度:定时检查到期查询并执行,支持手动触发。 +- 前端集成:提供查询词选择、导出按钮与下载逻辑。 + +章节来源 +- [backend/app/api/reports.py:16-47](file://backend/app/api/reports.py#L16-L47) +- [backend/app/services/citation.py:237-269](file://backend/app/services/citation.py#L237-L269) +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [backend/app/models/citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) +- [backend/app/models/query_task.py:11-39](file://backend/app/models/query_task.py#L11-L39) +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) +- [backend/app/workers/scheduler.py:25-95](file://backend/app/workers/scheduler.py#L25-L95) +- [frontend/lib/api.ts:51-57](file://frontend/lib/api.ts#L51-L57) +- [frontend/app/(dashboard)/dashboard/reports/page.tsx:49-93](file://frontend/app/(dashboard)/dashboard/reports/page.tsx#L49-L93) + +## 架构总览 +下图展示从前端发起导出请求到后端生成CSV并返回下载的完整链路。 + +```mermaid +sequenceDiagram +participant FE as "前端页面
page.tsx" +participant API as "前端API封装
lib/api.ts" +participant FAST as "报告路由
reports.py" +participant SVC as "导出服务
services/citation.py" +participant DB as "数据库
SQLAlchemy" +FE->>API : "调用导出接口" +API->>FAST : "GET /api/v1/reports/export/csv?query_id=..." +FAST->>SVC : "export_citations_csv(db, user_id, query_id)" +SVC->>DB : "查询引用记录" +DB-->>SVC : "返回记录集合" +SVC-->>FAST : "返回CSV字符串" +FAST-->>API : "StreamingResponse(csv)" +API-->>FE : "下载CSV文件" +``` + +图表来源 +- [frontend/app/(dashboard)/dashboard/reports/page.tsx:49-93](file://frontend/app/(dashboard)/dashboard/reports/page.tsx#L49-L93) +- [frontend/lib/api.ts:51-57](file://frontend/lib/api.ts#L51-L57) +- [backend/app/api/reports.py:16-47](file://backend/app/api/reports.py#L16-L47) +- [backend/app/services/citation.py:237-269](file://backend/app/services/citation.py#L237-L269) + +## 详细组件分析 + +### 报告导出接口(CSV) +- 接口路径:/api/v1/reports/export/csv +- 方法:GET +- 认证:需要Bearer Token +- 参数 + - query_id: UUID,必填,指定导出的查询词 + - format: 字符串,默认csv,当前版本仅支持csv +- 成功响应 + - Content-Type: text/csv + - Content-Disposition: attachment; filename="geo-report-YYYYMMDD.csv" + - 响应体:CSV字符串(流式传输) +- 错误处理 + - format非csv:400 Bad Request + - 查询不存在或无权限:404 Not Found + - 其他异常:500 Internal Server Error + +章节来源 +- [backend/app/api/reports.py:16-47](file://backend/app/api/reports.py#L16-L47) + +### 导出服务与数据筛选 +- 权限校验:仅允许导出当前用户拥有的查询词下的数据 +- 数据筛选 + - query_id:限定到具体查询词 + - 时间范围:通过引用记录的查询时间字段进行过滤(服务层支持start_date/end_date参数) +- 输出字段(CSV列头) + - 日期、平台、是否引用、引用位置、引用文本、竞争品牌 +- 性能特性 + - 使用StringIO构建CSV,避免中间文件 + - 支持流式返回,降低内存峰值 + +章节来源 +- [backend/app/services/citation.py:237-269](file://backend/app/services/citation.py#L237-L269) +- [backend/app/models/citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) + +### 报告生成触发机制 +- 手动导出 + - 前端选择查询词后点击导出,直接调用导出接口 + - 后端即时查询数据库并返回CSV +- 定时导出 + - 调度器每小时扫描到期查询(status=active且next_query_at<=now) + - 对每个到期查询执行引擎,生成引用记录 + - 导出接口可读取这些最新记录 +- 手动触发查询 + - 服务层提供立即触发方法,创建查询任务并返回首个任务信息 + +```mermaid +flowchart TD +Start(["定时检查"]) --> Scan["扫描到期查询"] +Scan --> HasDue{"存在到期查询?"} +HasDue -- 否 --> Wait["等待下一小时"] --> Scan +HasDue -- 是 --> Exec["执行查询引擎"] +Exec --> Save["保存引用记录"] +Save --> Update["更新查询时间字段"] +Update --> Export["导出接口可读取最新数据"] +``` + +图表来源 +- [backend/app/workers/scheduler.py:51-84](file://backend/app/workers/scheduler.py#L51-L84) +- [backend/app/workers/citation_engine.py:159-234](file://backend/app/workers/citation_engine.py#L159-L234) +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) + +章节来源 +- [backend/app/workers/scheduler.py:25-95](file://backend/app/workers/scheduler.py#L25-L95) +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) +- [backend/app/services/citation.py:204-234](file://backend/app/services/citation.py#L204-L234) + +### 数据模型与关系 +```mermaid +erDiagram +QUERIES { +uuid id PK +uuid user_id FK +string keyword +string target_brand +jsonb brand_aliases +jsonb platforms +string frequency +string status +datetime last_queried_at +datetime next_query_at +} +CITATION_RECORDS { +uuid id PK +uuid query_id FK +string platform +boolean cited +integer citation_position +text citation_text +jsonb competitor_brands +text raw_response +datetime queried_at +} +QUERY_TASKS { +uuid id PK +uuid query_id FK +string platform +string status +text error_message +datetime scheduled_at +datetime started_at +datetime completed_at +} +QUERIES ||--o{ CITATION_RECORDS : "拥有" +QUERIES ||--o{ QUERY_TASKS : "拥有" +``` + +图表来源 +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [backend/app/models/citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) +- [backend/app/models/query_task.py:11-39](file://backend/app/models/query_task.py#L11-L39) + +章节来源 +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [backend/app/models/citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) +- [backend/app/models/query_task.py:11-39](file://backend/app/models/query_task.py#L11-L39) + +### 前端集成与下载 +- 前端页面提供查询词下拉选择与导出按钮 +- 导出时通过原生fetch请求后端接口,接收blob并触发浏览器下载 +- 成功/失败状态提示与加载态管理 + +章节来源 +- [frontend/app/(dashboard)/dashboard/reports/page.tsx:49-93](file://frontend/app/(dashboard)/dashboard/reports/page.tsx#L49-L93) +- [frontend/lib/api.ts:51-57](file://frontend/lib/api.ts#L51-L57) + +## 依赖关系分析 +- 报告路由依赖导出服务 +- 导出服务依赖查询与引用记录模型 +- 引擎与调度器依赖平台适配器基类 +- 应用入口在生命周期内启动调度器 + +```mermaid +graph LR +Reports["reports.py"] --> ExportSvc["services/citation.py"] +ExportSvc --> Models["models/*.py"] +Scheduler["workers/scheduler.py"] --> Engine["workers/citation_engine.py"] +Engine --> Platforms["workers/platforms/base.py"] +Main["main.py"] --> Reports +Main --> Scheduler +``` + +图表来源 +- [backend/app/api/reports.py:11](file://backend/app/api/reports.py#L11) +- [backend/app/services/citation.py:237-269](file://backend/app/services/citation.py#L237-L269) +- [backend/app/workers/scheduler.py:25-95](file://backend/app/workers/scheduler.py#L25-L95) +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) +- [backend/app/workers/platforms/base.py:4-18](file://backend/app/workers/platforms/base.py#L4-L18) +- [backend/app/main.py:13-48](file://backend/app/main.py#L13-L48) + +章节来源 +- [backend/app/api/reports.py:11](file://backend/app/api/reports.py#L11) +- [backend/app/services/citation.py:237-269](file://backend/app/services/citation.py#L237-L269) +- [backend/app/workers/scheduler.py:25-95](file://backend/app/workers/scheduler.py#L25-L95) +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) +- [backend/app/workers/platforms/base.py:4-18](file://backend/app/workers/platforms/base.py#L4-L18) +- [backend/app/main.py:13-48](file://backend/app/main.py#L13-L48) + +## 性能考虑 +- 流式响应:后端以流式方式返回CSV,避免一次性加载全部数据至内存 +- 索引优化:引用记录表对query_id、queried_at、platform建立索引,提升筛选与排序性能 +- 分页与限制:服务层支持skip/limit,前端可结合分页控件控制导出规模 +- 导出粒度:优先按查询词导出,避免全量导出导致的I/O压力 +- 并发与队列:查询任务表支持多平台并发执行,导出时可按需筛选时间段 + +章节来源 +- [backend/app/services/citation.py:24-74](file://backend/app/services/citation.py#L24-L74) +- [backend/app/models/citation_record.py:37-41](file://backend/app/models/citation_record.py#L37-L41) + +## 故障排查指南 +- 400 Bad Request:format参数非csv + - 检查前端传参或接口调用是否正确 +- 404 Not Found:查询不存在或无权限 + - 确认query_id归属当前用户,查询状态为active +- 下载失败或文件为空 + - 检查该查询词是否存在引用记录;确认导出时间范围内是否有数据 +- 导出耗时过长 + - 缩小时间范围或按查询词导出;检查数据库索引是否生效 +- 定时任务未执行 + - 检查调度器是否启动;核对查询状态与next_query_at字段 + +章节来源 +- [backend/app/api/reports.py:23-35](file://backend/app/api/reports.py#L23-L35) +- [backend/app/services/citation.py:242-244](file://backend/app/services/citation.py#L242-L244) +- [backend/app/workers/scheduler.py:30-40](file://backend/app/workers/scheduler.py#L30-L40) + +## 结论 +本报告导出系统以清晰的职责分离实现:前端负责交互与下载,后端路由负责鉴权与转发,服务层负责数据筛选与CSV生成,调度器负责周期性任务执行。当前版本聚焦CSV导出,后续可扩展为多格式与批量导出,同时建议引入任务队列与进度回调以增强用户体验。 + +## 附录 + +### API定义(CSV导出) +- 路径:/api/v1/reports/export/csv +- 方法:GET +- 认证:Bearer Token +- 查询参数 + - query_id: UUID,必填 + - format: 字符串,可选,默认csv +- 响应 + - 200 OK:text/csv,Content-Disposition为附件下载 + - 400 Bad Request:format非法 + - 404 Not Found:查询不存在或无权限 + - 500 Internal Server Error:内部异常 + +章节来源 +- [backend/app/api/reports.py:16-47](file://backend/app/api/reports.py#L16-L47) + +### 输出字段说明(CSV) +- 日期:引用记录的查询时间 +- 平台:执行查询的AI平台名称 +- 是否引用:布尔值(是/否) +- 引用位置:品牌首次出现的段落序号(若未命中则为空) +- 引用文本:截取后的上下文片段(若未命中则为空) +- 竞争品牌:检测到的其他品牌列表(逗号分隔) + +章节来源 +- [backend/app/services/citation.py:256-268](file://backend/app/services/citation.py#L256-L268) + +### 报告模板定制建议 +- 列顺序与标题:可在导出服务中调整writer的列头顺序与本地化标题 +- 过滤条件:扩展服务层参数(如平台、起止时间),并在导出服务中拼接SQL条件 +- 格式转换:如需Excel,可在前端接收CSV后转为xlsx,或在后端生成xlsx流 + +章节来源 +- [backend/app/services/citation.py:237-269](file://backend/app/services/citation.py#L237-L269) + +### 批量导出与定时导出模式 +- 批量导出:前端逐项选择查询词后分别导出;或在后端增加“全部查询”聚合导出(需扩展服务层) +- 定时导出:通过调度器周期性执行查询并生成记录,随后即可导出 +- 手动导出:前端点击即刻触发查询任务,完成后导出 + +章节来源 +- [backend/app/workers/scheduler.py:51-84](file://backend/app/workers/scheduler.py#L51-L84) +- [backend/app/services/citation.py:204-234](file://backend/app/services/citation.py#L204-L234) + +### 进度监控与错误处理 +- 进度监控:查询任务表包含状态与时间戳,可用于前端轮询或WebSocket推送 +- 错误处理:平台适配器异常会写入任务错误信息与记录,导出接口保持幂等与健壮 + +章节来源 +- [backend/app/models/query_task.py:11-39](file://backend/app/models/query_task.py#L11-L39) +- [backend/app/workers/citation_engine.py:211-227](file://backend/app/workers/citation_engine.py#L211-L227) \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/API接口文档/查询管理接口.md b/.qoder/repowiki/zh/content/API接口文档/查询管理接口.md new file mode 100644 index 0000000..444eefd --- /dev/null +++ b/.qoder/repowiki/zh/content/API接口文档/查询管理接口.md @@ -0,0 +1,546 @@ +# 查询管理接口 + + +**本文档引用的文件** +- [backend/app/api/queries.py](file://backend/app/api/queries.py) +- [backend/app/schemas/query.py](file://backend/app/schemas/query.py) +- [backend/app/models/query.py](file://backend/app/models/query.py) +- [backend/app/models/query_task.py](file://backend/app/models/query_task.py) +- [backend/app/models/citation_record.py](file://backend/app/models/citation_record.py) +- [backend/app/models/user.py](file://backend/app/models/user.py) +- [backend/app/services/query.py](file://backend/app/services/query.py) +- [backend/app/workers/scheduler.py](file://backend/app/workers/scheduler.py) +- [backend/app/workers/citation_engine.py](file://backend/app/workers/citation_engine.py) +- [backend/app/workers/platforms/base.py](file://backend/app/workers/platforms/base.py) +- [backend/app/workers/platforms/kimi.py](file://backend/app/workers/platforms/kimi.py) +- [backend/app/workers/platforms/wenxin.py](file://backend/app/workers/platforms/wenxin.py) +- [backend/app/api/citations.py](file://backend/app/api/citations.py) +- [backend/app/main.py](file://backend/app/main.py) +- [tests/test_queries.py](file://tests/test_queries.py) + + +## 目录 +1. [简介](#简介) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖分析](#依赖分析) +7. [性能考虑](#性能考虑) +8. [故障排查指南](#故障排查指南) +9. [结论](#结论) +10. [附录](#附录) + +## 简介 +本文件为“查询管理系统”的详细API文档,覆盖查询任务的创建、读取、更新、删除与执行全流程;详述查询任务的数据模型、字段定义与验证规则;文档化查询任务的状态管理、调度机制与执行监控;包含查询参数配置、定时任务设置与批量操作接口的使用建议;并提供查询任务生命周期管理的最佳实践与错误处理策略。 + +## 项目结构 +后端采用FastAPI + SQLAlchemy异步ORM + PostgreSQL数据库,查询管理相关模块分布如下: +- API层:负责路由定义与请求/响应封装 +- Schema层:Pydantic模型,定义请求体与响应体的字段与校验 +- Model层:SQLAlchemy ORM模型,定义数据库表结构与索引 +- Service层:业务逻辑封装,处理权限、计数限制与时间计算 +- Worker层:定时调度器与引用检测引擎,驱动查询任务执行 +- 平台适配器:Kimi与文心一言平台的适配实现 + +```mermaid +graph TB +subgraph "API层" +QAPI["查询API
backend/app/api/queries.py"] +CAPI["引用API
backend/app/api/citations.py"] +end +subgraph "Schema层" +QS["查询Schema
backend/app/schemas/query.py"] +end +subgraph "Model层" +MQ["查询模型
backend/app/models/query.py"] +MT["查询任务模型
backend/app/models/query_task.py"] +MR["引用记录模型
backend/app/models/citation_record.py"] +MU["用户模型
backend/app/models/user.py"] +end +subgraph "Service层" +S["查询服务
backend/app/services/query.py"] +end +subgraph "Worker层" +SCH["调度器
backend/app/workers/scheduler.py"] +CE["引用引擎
backend/app/workers/citation_engine.py"] +end +subgraph "平台适配器" +BASE["适配器基类
backend/app/workers/platforms/base.py"] +KIMI["Kimi适配器
backend/app/workers/platforms/kimi.py"] +WENXIN["文心一言适配器
backend/app/workers/platforms/wenxin.py"] +end +QAPI --> S +CAPI --> CE +S --> MQ +CE --> MQ +CE --> MR +CE --> MT +SCH --> CE +CE --> KIMI +CE --> WENXIN +QS --> QAPI +MQ --> MU +``` + +图表来源 +- [backend/app/api/queries.py:1-86](file://backend/app/api/queries.py#L1-L86) +- [backend/app/api/citations.py:1-78](file://backend/app/api/citations.py#L1-L78) +- [backend/app/schemas/query.py:1-94](file://backend/app/schemas/query.py#L1-L94) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [backend/app/models/query_task.py:1-39](file://backend/app/models/query_task.py#L1-L39) +- [backend/app/models/citation_record.py:1-42](file://backend/app/models/citation_record.py#L1-L42) +- [backend/app/models/user.py:1-41](file://backend/app/models/user.py#L1-L41) +- [backend/app/services/query.py:1-130](file://backend/app/services/query.py#L1-L130) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) +- [backend/app/workers/platforms/base.py:1-18](file://backend/app/workers/platforms/base.py#L1-L18) +- [backend/app/workers/platforms/kimi.py:1-206](file://backend/app/workers/platforms/kimi.py#L1-L206) +- [backend/app/workers/platforms/wenxin.py:1-205](file://backend/app/workers/platforms/wenxin.py#L1-L205) + +章节来源 +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) + +## 核心组件 +- 查询API:提供查询任务的增删改查与分页列表接口 +- 查询Schema:定义创建/更新请求体与响应体的字段与校验规则 +- 查询模型:定义数据库表结构、索引与关联关系 +- 查询任务模型:记录每次平台执行的任务状态与时间戳 +- 引用记录模型:保存每次查询的结果与统计信息 +- 用户模型:限制用户的最大查询数量 +- 查询服务:实现权限控制、计数限制与下次查询时间计算 +- 调度器:基于APScheduler的定时任务,周期性检查并执行到期查询 +- 引用引擎:跨平台执行查询、品牌匹配、竞争品牌检测与结果持久化 +- 平台适配器:Kimi与文心一言的自动化查询实现 + +章节来源 +- [backend/app/api/queries.py:15-85](file://backend/app/api/queries.py#L15-L85) +- [backend/app/schemas/query.py:11-94](file://backend/app/schemas/query.py#L11-L94) +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [backend/app/models/query_task.py:11-39](file://backend/app/models/query_task.py#L11-L39) +- [backend/app/models/citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) +- [backend/app/models/user.py:11-41](file://backend/app/models/user.py#L11-L41) +- [backend/app/services/query.py:12-130](file://backend/app/services/query.py#L12-L130) +- [backend/app/workers/scheduler.py:25-95](file://backend/app/workers/scheduler.py#L25-L95) +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) +- [backend/app/workers/platforms/base.py:4-18](file://backend/app/workers/platforms/base.py#L4-L18) + +## 架构总览 +查询管理系统的整体工作流如下: +- 客户端通过查询API创建查询任务,服务端进行权限与参数校验,并计算下次查询时间 +- 调度器定时扫描满足条件的查询任务,触发引用引擎执行 +- 引用引擎遍历平台列表,调用平台适配器获取AI回复,执行品牌匹配与竞争品牌检测,生成引用记录 +- 查询任务模型记录每次执行的状态与时间,便于监控与重试 +- 引用API提供查询历史、统计与“立即执行”接口 + +```mermaid +sequenceDiagram +participant Client as "客户端" +participant API as "查询API" +participant Service as "查询服务" +participant DB as "数据库" +participant Scheduler as "调度器" +participant Engine as "引用引擎" +participant Adapter as "平台适配器" +Client->>API : "POST /api/v1/queries/" +API->>Service : "create_query(...)" +Service->>DB : "插入查询记录并计算next_query_at" +DB-->>Service : "返回新查询" +Service-->>API : "返回查询" +API-->>Client : "201 Created" +Note over Scheduler,Engine : "定时触发" +Scheduler->>DB : "查询status='active'且next_query_at<=now()" +DB-->>Scheduler : "返回待执行查询集合" +Scheduler->>Engine : "execute_query(query)" +Engine->>Adapter : "逐平台查询(keyword)" +Adapter-->>Engine : "返回原始回复" +Engine->>DB : "写入引用记录与更新查询时间" +``` + +图表来源 +- [backend/app/api/queries.py:26-39](file://backend/app/api/queries.py#L26-L39) +- [backend/app/services/query.py:45-81](file://backend/app/services/query.py#L45-L81) +- [backend/app/workers/scheduler.py:51-84](file://backend/app/workers/scheduler.py#L51-L84) +- [backend/app/workers/citation_engine.py:159-234](file://backend/app/workers/citation_engine.py#L159-L234) +- [backend/app/workers/platforms/kimi.py:33-48](file://backend/app/workers/platforms/kimi.py#L33-L48) +- [backend/app/workers/platforms/wenxin.py:33-48](file://backend/app/workers/platforms/wenxin.py#L33-L48) + +## 详细组件分析 + +### 数据模型与字段定义 +- 查询模型(queries) + - 关键字段:id、user_id、keyword、target_brand、brand_aliases、platforms、frequency、status、last_queried_at、next_query_at、created_at、updated_at + - 约束与索引:外键约束、多字段索引(user_id、status、next_query_at) + - 关联:一对多关联至用户与引用记录、查询任务 +- 查询任务模型(query_tasks) + - 关键字段:id、query_id、platform、status、error_message、scheduled_at、started_at、completed_at + - 约束与索引:外键约束、索引(status) + - 关联:多对一关联至查询 +- 引用记录模型(citation_records) + - 关键字段:id、query_id、platform、cited、citation_position、citation_text、competitor_brands、raw_response、queried_at + - 约束与索引:外键约束、多字段索引(query_id、queried_at、platform) + - 关联:多对一关联至查询 +- 用户模型(users) + - 关键字段:id、email、password_hash、name、plan、max_queries、is_active、created_at、updated_at + - 关联:一对多关联至查询与订阅 + +```mermaid +erDiagram +USERS { +uuid id PK +string email UK +string password_hash +string name +string plan +int max_queries +boolean is_active +} +QUERIES { +uuid id PK +uuid user_id FK +string keyword +string target_brand +jsonb brand_aliases +jsonb platforms +string frequency +string status +timestamp last_queried_at +timestamp next_query_at +} +QUERY_TASKS { +uuid id PK +uuid query_id FK +string platform +string status +text error_message +timestamp scheduled_at +timestamp started_at +timestamp completed_at +} +CITATION_RECORDS { +uuid id PK +uuid query_id FK +string platform +boolean cited +int citation_position +text citation_text +jsonb competitor_brands +text raw_response +timestamp queried_at +} +USERS ||--o{ QUERIES : "拥有" +QUERIES ||--o{ CITATION_RECORDS : "产生" +QUERIES ||--o{ QUERY_TASKS : "驱动" +``` + +图表来源 +- [backend/app/models/user.py:11-41](file://backend/app/models/user.py#L11-L41) +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [backend/app/models/query_task.py:11-39](file://backend/app/models/query_task.py#L11-L39) +- [backend/app/models/citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) + +章节来源 +- [backend/app/models/user.py:11-41](file://backend/app/models/user.py#L11-L41) +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [backend/app/models/query_task.py:11-39](file://backend/app/models/query_task.py#L11-L39) +- [backend/app/models/citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) + +### 查询任务状态管理与调度机制 +- 状态流转 + - 查询任务状态:pending → running → success 或 failed + - 查询状态:active、paused、disabled +- 调度机制 + - 使用APScheduler的AsyncIOScheduler,每小时检查一次 + - 条件:查询状态为active且next_query_at小于等于当前UTC时间 + - 触发引用引擎执行,逐平台查询并生成引用记录 +- 执行监控 + - 记录scheduled_at、started_at、completed_at + - 失败时记录error_message + - 更新查询的last_queried_at与next_query_at + +```mermaid +stateDiagram-v2 +[*] --> 待调度 +待调度 --> 已调度 : "被调度器发现" +已调度 --> 执行中 : "开始执行" +执行中 --> 成功 : "全部平台成功" +执行中 --> 失败 : "任一平台失败" +成功 --> 待调度 : "更新next_query_at" +失败 --> 待调度 : "更新next_query_at" +``` + +图表来源 +- [backend/app/workers/scheduler.py:51-84](file://backend/app/workers/scheduler.py#L51-L84) +- [backend/app/workers/citation_engine.py:176-234](file://backend/app/workers/citation_engine.py#L176-L234) +- [backend/app/models/query_task.py:24-32](file://backend/app/models/query_task.py#L24-L32) + +章节来源 +- [backend/app/workers/scheduler.py:25-95](file://backend/app/workers/scheduler.py#L25-L95) +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) + +### 查询参数配置与验证规则 +- 支持平台:wenxin、kimi、tongyi、baidu_ai、yuanbao、qingyan(创建/更新时校验) +- 频率:daily、weekly(默认weekly) +- 状态:active、paused、disabled(仅更新时可修改) +- 字段长度:keyword(1~200)、target_brand(1~100) +- 平台列表不能为空,若为空则报错 +- 频率必须在允许集合内,否则报错 +- 状态必须在允许集合内,否则报错 + +```mermaid +flowchart TD +Start(["进入校验"]) --> CheckPlatforms["校验平台列表非空"] +CheckPlatforms --> PlatformsOK{"平台有效?"} +PlatformsOK --> |否| ErrPlatforms["抛出平台无效错误"] +PlatformsOK --> |是| CheckFrequency["校验频率"] +CheckFrequency --> FreqOK{"频率有效?"} +FreqOK --> |否| ErrFreq["抛出频率无效错误"] +FreqOK --> |是| CheckStatus["校验状态(如提供)"] +CheckStatus --> StatusOK{"状态有效?"} +StatusOK --> |否| ErrStatus["抛出状态无效错误"] +StatusOK --> |是| Done(["校验通过"]) +``` + +图表来源 +- [backend/app/schemas/query.py:18-33](file://backend/app/schemas/query.py#L18-L33) +- [backend/app/schemas/query.py:44-72](file://backend/app/schemas/query.py#L44-L72) + +章节来源 +- [backend/app/schemas/query.py:6-9](file://backend/app/schemas/query.py#L6-L9) +- [backend/app/schemas/query.py:11-94](file://backend/app/schemas/query.py#L11-L94) + +### API定义与使用示例 + +- 查询任务管理 + - GET /api/v1/queries/ + - 功能:分页列出当前用户的所有查询任务 + - 查询参数:skip(>=0)、limit(1~100) + - 响应:包含items与total的列表响应 + - POST /api/v1/queries/ + - 功能:创建新的查询任务 + - 请求体:keyword、target_brand、brand_aliases、platforms、frequency + - 响应:创建成功的查询任务详情 + - 错误:超过用户最大查询数时返回403 + - GET /api/v1/queries/{query_id} + - 功能:获取指定查询任务详情 + - 响应:查询任务详情 + - 错误:不存在或不属于当前用户返回404 + - PUT /api/v1/queries/{query_id} + - 功能:更新查询任务 + - 请求体:可选字段keyword、target_brand、brand_aliases、platforms、frequency、status + - 响应:更新后的查询任务详情 + - 错误:不存在返回404 + - DELETE /api/v1/queries/{query_id} + - 功能:删除查询任务 + - 响应:204 No Content + - 错误:不存在返回404 + +- 引用数据与立即执行 + - GET /api/v1/citations/ + - 功能:分页查询引用记录,支持按query_id、platform、日期范围过滤 + - 响应:包含items与total的列表响应 + - GET /api/v1/citations/stats + - 功能:获取引用统计 + - 响应:统计结果 + - POST /api/v1/queries/{query_id}/run-now + - 功能:立即将某个查询加入执行队列 + - 响应:包含task_id、status与消息 + - 错误:查询不存在或不属于当前用户返回404 + +章节来源 +- [backend/app/api/queries.py:15-85](file://backend/app/api/queries.py#L15-L85) +- [backend/app/api/citations.py:25-77](file://backend/app/api/citations.py#L25-L77) + +### 生命周期管理最佳实践 +- 合理设置frequency:daily适合高频监控,weekly适合常规跟踪 +- 控制platforms数量:平台越多,耗时越长,成本越高 +- 使用status暂停:在维护或节假日可将查询置为paused避免执行 +- 监控next_query_at:确保调度器能按时触发 +- 引用记录归档:定期清理过期记录,保持查询性能 + +### 错误处理策略 +- 参数校验失败:返回422,提示具体字段问题 +- 权限不足/查询不存在:返回404 +- 超过用户最大查询数:返回403 +- 平台适配器异常:记录error_message,状态标记为failed +- 调度器异常:日志记录错误并继续运行 + +章节来源 +- [backend/app/api/queries.py:32-39](file://backend/app/api/queries.py#L32-L39) +- [backend/app/api/queries.py:49-53](file://backend/app/api/queries.py#L49-L53) +- [backend/app/api/queries.py:64-69](file://backend/app/api/queries.py#L64-L69) +- [backend/app/api/queries.py:79-84](file://backend/app/api/queries.py#L79-L84) +- [backend/app/api/citations.py:65-71](file://backend/app/api/citations.py#L65-L71) + +## 依赖分析 +- 组件耦合 + - API层依赖Service层;Service层依赖Model层;Worker层依赖Model层与平台适配器 + - 引用引擎依赖平台适配器,平台适配器继承自基类 +- 外部依赖 + - APscheduler用于定时调度 + - Playwright用于平台网页自动化 + - SQLAlchemy异步ORM用于数据库访问 + +```mermaid +graph LR +APIQ["查询API"] --> SVCQ["查询服务"] +APIC["引用API"] --> CE["引用引擎"] +SVCQ --> MODELS["查询/任务/记录模型"] +CE --> MODELS +CE --> ADP["平台适配器"] +ADP --> BASE["适配器基类"] +SCH["调度器"] --> CE +``` + +图表来源 +- [backend/app/api/queries.py:1-12](file://backend/app/api/queries.py#L1-L12) +- [backend/app/api/citations.py:1-22](file://backend/app/api/citations.py#L1-L22) +- [backend/app/services/query.py:1-10](file://backend/app/services/query.py#L1-L10) +- [backend/app/workers/citation_engine.py:148-157](file://backend/app/workers/citation_engine.py#L148-L157) +- [backend/app/workers/platforms/base.py:4-18](file://backend/app/workers/platforms/base.py#L4-L18) +- [backend/app/workers/scheduler.py:25-39](file://backend/app/workers/scheduler.py#L25-L39) + +章节来源 +- [backend/app/workers/scheduler.py:13-19](file://backend/app/workers/scheduler.py#L13-L19) +- [backend/app/workers/platforms/kimi.py:17-32](file://backend/app/workers/platforms/kimi.py#L17-L32) +- [backend/app/workers/platforms/wenxin.py:17-32](file://backend/app/workers/platforms/wenxin.py#L17-L32) + +## 性能考虑 +- 数据库索引:为常用查询字段建立索引,减少扫描开销 +- 分页查询:API层提供skip/limit参数,避免一次性返回大量数据 +- 异步I/O:使用异步数据库连接与平台适配器,提升并发能力 +- 调度频率:每小时检查一次,可根据业务量调整 +- 结果缓存:对频繁查询的平台响应可引入缓存(需结合业务场景) + +## 故障排查指南 +- 调度器未启动:确认应用生命周期钩子已注册并启动调度器 +- 平台适配器异常:检查Playwright浏览器安装与页面选择器是否匹配 +- 查询未执行:检查查询状态与next_query_at是否满足调度条件 +- 引用记录缺失:确认引用引擎执行流程与数据库提交顺序 +- 权限错误:确认用户max_queries限制与当前查询数量 + +章节来源 +- [backend/app/main.py:13-22](file://backend/app/main.py#L13-L22) +- [backend/app/workers/platforms/kimi.py:21-32](file://backend/app/workers/platforms/kimi.py#L21-L32) +- [backend/app/workers/platforms/wenxin.py:21-32](file://backend/app/workers/platforms/wenxin.py#L21-L32) +- [backend/app/workers/scheduler.py:51-84](file://backend/app/workers/scheduler.py#L51-L84) +- [backend/app/services/query.py:45-81](file://backend/app/services/query.py#L45-L81) + +## 结论 +本查询管理系统以清晰的分层架构实现了查询任务的全生命周期管理:从创建、校验、调度到执行与监控。通过严格的参数校验、灵活的状态管理与可靠的调度机制,系统能够稳定支撑多平台、多频率的查询需求。建议在生产环境中结合业务量调整调度频率、优化平台适配器稳定性,并完善监控告警体系。 + +## 附录 + +### 类关系图(代码级) +```mermaid +classDiagram +class Query { ++uuid id ++uuid user_id ++string keyword ++string target_brand ++list brand_aliases ++list platforms ++string frequency ++string status ++datetime last_queried_at ++datetime next_query_at ++datetime created_at ++datetime updated_at +} +class QueryTask { ++uuid id ++uuid query_id ++string platform ++string status ++string error_message ++datetime scheduled_at ++datetime started_at ++datetime completed_at +} +class CitationRecord { ++uuid id ++uuid query_id ++string platform ++boolean cited ++int citation_position ++string citation_text ++list competitor_brands ++string raw_response ++datetime queried_at +} +class User { ++uuid id ++string email ++string password_hash ++string name ++string plan ++int max_queries ++boolean is_active +} +class CitationEngine { ++execute_query(query, db) list ++execute_single_platform(keyword, platform, target_brand, brand_aliases) dict +} +class QueryScheduler { ++start() void ++check_and_execute_queries() void ++shutdown() void +} +class KimiAdapter { ++query(keyword) str ++close() void +} +class WenxinAdapter { ++query(keyword) str ++close() void +} +Query "1" --> "*" CitationRecord : "产生" +Query "1" --> "*" QueryTask : "驱动" +User "1" --> "*" Query : "拥有" +CitationEngine --> Query : "读取" +CitationEngine --> CitationRecord : "写入" +CitationEngine --> QueryTask : "写入" +QueryScheduler --> CitationEngine : "触发" +CitationEngine --> KimiAdapter : "调用" +CitationEngine --> WenxinAdapter : "调用" +``` + +图表来源 +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [backend/app/models/query_task.py:11-39](file://backend/app/models/query_task.py#L11-L39) +- [backend/app/models/citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) +- [backend/app/models/user.py:11-41](file://backend/app/models/user.py#L11-L41) +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) +- [backend/app/workers/scheduler.py:25-95](file://backend/app/workers/scheduler.py#L25-L95) +- [backend/app/workers/platforms/kimi.py:11-206](file://backend/app/workers/platforms/kimi.py#L11-L206) +- [backend/app/workers/platforms/wenxin.py:11-205](file://backend/app/workers/platforms/wenxin.py#L11-L205) + +### 单次查询执行序列图 +```mermaid +sequenceDiagram +participant API as "查询API" +participant Service as "查询服务" +participant DB as "数据库" +participant Engine as "引用引擎" +participant Task as "查询任务模型" +participant Record as "引用记录模型" +API->>Service : "create_query(...)" +Service->>DB : "插入查询记录" +DB-->>Service : "返回查询" +Service-->>API : "返回查询" +Note over Engine,DB : "调度器触发" +Engine->>DB : "查询平台列表" +Engine->>Task : "获取或创建任务" +Task-->>Engine : "任务对象" +Engine->>Task : "状态=running" +Engine->>Engine : "调用平台适配器" +Engine->>Record : "创建引用记录" +Engine->>DB : "提交事务" +``` + +图表来源 +- [backend/app/services/query.py:45-81](file://backend/app/services/query.py#L45-L81) +- [backend/app/workers/citation_engine.py:159-234](file://backend/app/workers/citation_engine.py#L159-L234) +- [backend/app/models/query_task.py:176-289](file://backend/app/models/query_task.py#L176-L289) +- [backend/app/models/citation_record.py:194-204](file://backend/app/models/citation_record.py#L194-L204) \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/API接口文档/认证接口.md b/.qoder/repowiki/zh/content/API接口文档/认证接口.md new file mode 100644 index 0000000..62b2e51 --- /dev/null +++ b/.qoder/repowiki/zh/content/API接口文档/认证接口.md @@ -0,0 +1,413 @@ +# 认证接口 + + +**本文档引用的文件** +- [backend/app/api/auth.py](file://backend/app/api/auth.py) +- [backend/app/schemas/auth.py](file://backend/app/schemas/auth.py) +- [backend/app/services/auth.py](file://backend/app/services/auth.py) +- [backend/app/models/user.py](file://backend/app/models/user.py) +- [backend/app/api/deps.py](file://backend/app/api/deps.py) +- [backend/app/config.py](file://backend/app/config.py) +- [backend/app/main.py](file://backend/app/main.py) +- [backend/app/database.py](file://backend/app/database.py) +- [tests/test_auth.py](file://tests/test_auth.py) +- [backend/requirements.txt](file://backend/requirements.txt) + + +## 目录 +1. [简介](#简介) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖分析](#依赖分析) +7. [性能考虑](#性能考虑) +8. [故障排除指南](#故障排除指南) +9. [结论](#结论) +10. [附录](#附录) + +## 简介 +本文件为认证系统的完整API文档,覆盖以下接口: +- 用户注册:/api/v1/auth/register +- 用户登录:/api/v1/auth/login +- 获取当前用户:/api/v1/auth/me + +文档详细说明每个接口的请求参数、响应格式、状态码、错误处理以及JWT令牌生成机制、认证流程与安全考虑,并提供认证中间件使用指南与最佳实践。 + +## 项目结构 +后端采用FastAPI + SQLAlchemy异步ORM + PostgreSQL + Redis的架构,认证模块位于backend/app/api/auth.py,配合服务层、模型层与依赖注入实现。 + +```mermaid +graph TB +subgraph "后端应用" +A["FastAPI 应用
路由注册"] +B["认证路由
/api/v1/auth/*"] +C["认证服务
密码哈希/校验/JWT"] +D["数据库会话
AsyncSession"] +E["用户模型
SQLAlchemy ORM"] +F["认证依赖
OAuth2PasswordBearer"] +end +A --> B +B --> C +B --> D +C --> D +D --> E +F --> C +F --> D +``` + +图表来源 +- [backend/app/main.py:38](file://backend/app/main.py#L38) +- [backend/app/api/auth.py:10](file://backend/app/api/auth.py#L10) +- [backend/app/services/auth.py:13](file://backend/app/services/auth.py#L13) +- [backend/app/database.py:23](file://backend/app/database.py#L23) +- [backend/app/models/user.py:11](file://backend/app/models/user.py#L11) +- [backend/app/api/deps.py:13](file://backend/app/api/deps.py#L13) + +章节来源 +- [backend/app/main.py:38](file://backend/app/main.py#L38) +- [backend/app/api/auth.py:10](file://backend/app/api/auth.py#L10) + +## 核心组件 +- 路由器:在主应用中注册认证路由前缀为/api/v1/auth +- 认证服务:提供密码哈希/校验、JWT生成/校验、用户注册与认证 +- 数据模型:用户表结构,包含邮箱、密码哈希、计划等级、配额等字段 +- 依赖注入:OAuth2PasswordBearer用于从Authorization头提取Bearer令牌,get_current_user解析并验证JWT,加载当前用户 +- 配置:JWT密钥与过期时间、数据库连接等 + +章节来源 +- [backend/app/api/auth.py:10](file://backend/app/api/auth.py#L10) +- [backend/app/services/auth.py:13](file://backend/app/services/auth.py#L13) +- [backend/app/models/user.py:11](file://backend/app/models/user.py#L11) +- [backend/app/api/deps.py:13](file://backend/app/api/deps.py#L13) +- [backend/app/config.py:9](file://backend/app/config.py#L9) + +## 架构总览 +认证系统遵循“请求-服务-模型-数据库”的分层设计,使用OAuth2 Bearer令牌进行无状态认证。 + +```mermaid +sequenceDiagram +participant 客户端 as "客户端" +participant 路由 as "认证路由" +participant 服务 as "认证服务" +participant 数据库 as "数据库" +participant 依赖 as "认证依赖" +客户端->>路由 : POST /api/v1/auth/register +路由->>服务 : 注册用户(邮箱/密码/姓名) +服务->>数据库 : 查询邮箱是否已存在 +数据库-->>服务 : 结果 +服务->>服务 : 哈希密码 +服务->>数据库 : 创建用户记录 +数据库-->>服务 : 新用户对象 +服务-->>路由 : 返回UserResponse +路由-->>客户端 : 201 Created + UserResponse +客户端->>路由 : POST /api/v1/auth/login +路由->>服务 : 认证(邮箱/密码) +服务->>数据库 : 查询用户 +数据库-->>服务 : 用户或None +服务->>服务 : 校验密码 +服务->>服务 : 生成JWT(access_token) +服务-->>路由 : 返回TokenResponse +路由-->>客户端 : 200 OK + TokenResponse +客户端->>依赖 : GET /api/v1/auth/me (Authorization : Bearer) +依赖->>依赖 : 解析并验证JWT +依赖->>数据库 : 按ID查询用户 +数据库-->>依赖 : 用户对象 +依赖-->>客户端 : 200 OK + UserResponse +``` + +图表来源 +- [backend/app/api/auth.py:13](file://backend/app/api/auth.py#L13) +- [backend/app/api/auth.py:22](file://backend/app/api/auth.py#L22) +- [backend/app/api/auth.py:40](file://backend/app/api/auth.py#L40) +- [backend/app/services/auth.py:37](file://backend/app/services/auth.py#L37) +- [backend/app/services/auth.py:55](file://backend/app/services/auth.py#L55) +- [backend/app/api/deps.py:16](file://backend/app/api/deps.py#L16) + +## 详细组件分析 + +### 用户注册接口 /api/v1/auth/register +- 方法与路径:POST /api/v1/auth/register +- 请求体模型:UserRegister + - 字段:email(邮箱)、password(字符串,最小长度8)、name(字符串,1-100) +- 响应模型:UserResponse + - 字段:id(UUID)、email(字符串)、name(可空)、plan(字符串,默认"free")、max_queries(整数,默认5)、is_active(布尔,默认true)、created_at(时间戳) +- 成功响应:201 Created +- 错误响应: + - 400 Bad Request:当邮箱已被注册时,返回错误详情 +- 处理流程: + - 调用注册服务,检查邮箱唯一性 + - 对密码进行哈希处理 + - 写入数据库并返回新用户信息 + +```mermaid +flowchart TD +Start(["请求进入"]) --> Validate["校验请求体
邮箱/密码/姓名"] +Validate --> CheckDup{"邮箱已存在?"} +CheckDup --> |是| Return400["返回400错误
邮箱已注册"] +CheckDup --> |否| HashPwd["哈希密码"] +HashPwd --> Create["创建用户记录"] +Create --> Commit["提交事务"] +Commit --> Refresh["刷新用户对象"] +Refresh --> Return201["返回201 + UserResponse"] +``` + +图表来源 +- [backend/app/api/auth.py:13](file://backend/app/api/auth.py#L13) +- [backend/app/services/auth.py:37](file://backend/app/services/auth.py#L37) +- [backend/app/schemas/auth.py:7](file://backend/app/schemas/auth.py#L7) +- [backend/app/schemas/auth.py:18](file://backend/app/schemas/auth.py#L18) + +章节来源 +- [backend/app/api/auth.py:13](file://backend/app/api/auth.py#L13) +- [backend/app/schemas/auth.py:7](file://backend/app/schemas/auth.py#L7) +- [backend/app/schemas/auth.py:18](file://backend/app/schemas/auth.py#L18) +- [backend/app/services/auth.py:37](file://backend/app/services/auth.py#L37) +- [tests/test_auth.py:26](file://tests/test_auth.py#L26) +- [tests/test_auth.py:43](file://tests/test_auth.py#L43) + +请求示例 +- POST /api/v1/auth/register +- 请求体JSON: + - email: "string" + - password: "string(≥8)" + - name: "string(1-100)" + +响应示例 +- 201 Created +- 响应体JSON: + - id: "uuid" + - email: "string" + - name: "string|null" + - plan: "string" + - max_queries: integer + - is_active: boolean + - created_at: "datetime" + +状态码 +- 201 Created:注册成功 +- 400 Bad Request:邮箱已注册 + +错误处理 +- 当服务层抛出ValueError(如邮箱重复),路由捕获并返回400 + +### 用户登录接口 /api/v1/auth/login +- 方法与路径:POST /api/v1/auth/login +- 请求体模型:UserLogin + - 字段:email(邮箱)、password(字符串) +- 响应模型:TokenResponse + - 字段:access_token(字符串)、token_type(字符串,固定为"bearer")、user(UserResponse) +- 成功响应:200 OK +- 错误响应: + - 401 Unauthorized:邮箱或密码不正确,携带WWW-Authenticate: Bearer头 +- 处理流程: + - 使用邮箱查询用户 + - 校验密码哈希 + - 生成JWT令牌(含过期时间),返回access_token与用户信息 + +```mermaid +sequenceDiagram +participant 客户端 as "客户端" +participant 路由 as "登录路由" +participant 服务 as "认证服务" +participant 数据库 as "数据库" +客户端->>路由 : POST /api/v1/auth/login +路由->>服务 : authenticate_user(邮箱, 密码) +服务->>数据库 : 查询用户 +数据库-->>服务 : 用户或None +服务->>服务 : 校验密码 +alt 用户不存在或密码错误 +服务-->>路由 : None +路由-->>客户端 : 401 Unauthorized +else 登录成功 +服务-->>路由 : User对象 +路由->>服务 : create_access_token({sub : userId}) +服务-->>路由 : access_token +路由-->>客户端 : 200 OK + TokenResponse +end +``` + +图表来源 +- [backend/app/api/auth.py:22](file://backend/app/api/auth.py#L22) +- [backend/app/api/auth.py:24](file://backend/app/api/auth.py#L24) +- [backend/app/services/auth.py:55](file://backend/app/services/auth.py#L55) +- [backend/app/services/auth.py:24](file://backend/app/services/auth.py#L24) + +章节来源 +- [backend/app/api/auth.py:22](file://backend/app/api/auth.py#L22) +- [backend/app/api/auth.py:24](file://backend/app/api/auth.py#L24) +- [backend/app/schemas/auth.py:13](file://backend/app/schemas/auth.py#L13) +- [backend/app/schemas/auth.py:30](file://backend/app/schemas/auth.py#L30) +- [backend/app/services/auth.py:55](file://backend/app/services/auth.py#L55) +- [backend/app/services/auth.py:24](file://backend/app/services/auth.py#L24) +- [tests/test_auth.py:62](file://tests/test_auth.py#L62) +- [tests/test_auth.py:76-84](file://tests/test_auth.py#L76-L84) + +请求示例 +- POST /api/v1/auth/login +- 请求体JSON: + - email: "string" + - password: "string" + +响应示例 +- 200 OK +- 响应体JSON: + - access_token: "string" + - token_type: "bearer" + - user: { + id: "uuid" + email: "string" + name: "string|null" + plan: "string" + max_queries: integer + is_active: boolean + created_at: "datetime" + } + +状态码 +- 200 OK:登录成功 +- 401 Unauthorized:邮箱或密码不正确 + +安全考虑 +- 密码使用BCrypt哈希存储 +- JWT使用HS256算法与密钥签名,过期时间由配置控制 +- 登录失败返回统一错误消息,避免泄露账户存在性细节 + +### 获取当前用户接口 /api/v1/auth/me +- 方法与路径:GET /api/v1/auth/me +- 权限要求:需要Bearer令牌(Authorization: Bearer ) +- 依赖注入:get_current_user + - 通过OAuth2PasswordBearer从Authorization头提取令牌 + - 校验JWT并解析sub(用户ID) + - 从数据库按ID查询用户并返回 +- 响应模型:UserResponse +- 成功响应:200 OK +- 错误响应: + - 401 Unauthorized:令牌无效、过期或用户不存在 + +```mermaid +sequenceDiagram +participant 客户端 as "客户端" +participant 依赖 as "get_current_user" +participant 服务 as "verify_token" +participant 数据库 as "数据库" +客户端->>依赖 : GET /api/v1/auth/me (Authorization : Bearer) +依赖->>依赖 : 从Authorization头提取token +依赖->>服务 : verify_token(token) +服务-->>依赖 : payload(sub=userId) +依赖->>数据库 : 查询用户by id +数据库-->>依赖 : 用户或None +alt 令牌无效/用户不存在 +依赖-->>客户端 : 401 Unauthorized +else 成功 +依赖-->>客户端 : 200 OK + UserResponse +end +``` + +图表来源 +- [backend/app/api/deps.py:16](file://backend/app/api/deps.py#L16) +- [backend/app/api/deps.py:27](file://backend/app/api/deps.py#L27) +- [backend/app/services/auth.py:32](file://backend/app/services/auth.py#L32) +- [backend/app/api/auth.py:40](file://backend/app/api/auth.py#L40) + +章节来源 +- [backend/app/api/auth.py:40](file://backend/app/api/auth.py#L40) +- [backend/app/api/deps.py:16](file://backend/app/api/deps.py#L16) +- [backend/app/api/deps.py:27](file://backend/app/api/deps.py#L27) +- [backend/app/services/auth.py:32](file://backend/app/services/auth.py#L32) +- [tests/test_auth.py:88](file://tests/test_auth.py#L88) +- [tests/test_auth.py:99](file://tests/test_auth.py#L99) + +请求示例 +- GET /api/v1/auth/me +- 请求头: + - Authorization: "Bearer " + +响应示例 +- 200 OK +- 响应体JSON:UserResponse + +状态码 +- 200 OK:成功获取当前用户 +- 401 Unauthorized:未提供有效令牌或令牌无效 + +## 依赖分析 +- 外部库依赖:FastAPI、SQLAlchemy异步、Pydantic、python-jose、passlib、redis、apscheduler、playwright等 +- 认证相关依赖:OAuth2PasswordBearer、JWT编码/解码、BCrypt密码哈希 +- 配置项:JWT_SECRET、JWT_EXPIRE_HOURS、DATABASE_URL、REDIS_URL + +```mermaid +graph LR +A["FastAPI 应用"] --> B["认证路由"] +B --> C["认证服务"] +C --> D["Passlib(Bcrypt)"] +C --> E["python-jose(JWT)"] +C --> F["SQLAlchemy 异步"] +F --> G["PostgreSQL"] +A --> H["CORS 中间件"] +A --> I["依赖注入容器"] +``` + +图表来源 +- [backend/requirements.txt:2](file://backend/requirements.txt#L2) +- [backend/requirements.txt:16](file://backend/requirements.txt#L16) +- [backend/requirements.txt:17](file://backend/requirements.txt#L17) +- [backend/app/main.py:30](file://backend/app/main.py#L30) +- [backend/app/api/deps.py:13](file://backend/app/api/deps.py#L13) +- [backend/app/services/auth.py:13](file://backend/app/services/auth.py#L13) + +章节来源 +- [backend/requirements.txt:2](file://backend/requirements.txt#L2) +- [backend/requirements.txt:16](file://backend/requirements.txt#L16) +- [backend/requirements.txt:17](file://backend/requirements.txt#L17) +- [backend/app/main.py:30](file://backend/app/main.py#L30) +- [backend/app/api/deps.py:13](file://backend/app/api/deps.py#L13) +- [backend/app/services/auth.py:13](file://backend/app/services/auth.py#L13) + +## 性能考虑 +- 数据库访问:注册与登录均执行单次查询,使用异步会话减少阻塞 +- 密码哈希:BCrypt成本因子默认设置,平衡安全性与性能 +- JWT过期:可通过配置调整过期时间,建议根据业务场景权衡 +- 缓存策略:当前未对用户信息做缓存,可在高频读取场景引入Redis缓存 + +## 故障排除指南 +- 注册失败(400):确认邮箱唯一性;检查请求体字段类型与长度 +- 登录失败(401):确认邮箱与密码正确;检查JWT密钥与过期时间配置 +- 获取当前用户失败(401):确认Authorization头格式为Bearer ;检查令牌是否过期 +- 数据库连接问题:检查DATABASE_URL配置;确保PostgreSQL服务可用 +- CORS跨域问题:确认前端域名已在CORS允许列表中 + +章节来源 +- [tests/test_auth.py:43](file://tests/test_auth.py#L43) +- [tests/test_auth.py:76-84](file://tests/test_auth.py#L76-L84) +- [tests/test_auth.py:99](file://tests/test_auth.py#L99) +- [backend/app/config.py:7](file://backend/app/config.py#L7) +- [backend/app/main.py:30](file://backend/app/main.py#L30) + +## 结论 +认证系统基于FastAPI与SQLAlchemy异步ORM构建,采用OAuth2 Bearer令牌与JWT实现无状态认证。注册与登录流程清晰,错误处理明确,具备良好的扩展性与安全性基础。建议在生产环境完善令牌刷新策略、速率限制与审计日志,并定期轮换JWT密钥。 + +## 附录 + +### JWT令牌生成机制 +- 签名算法:HS256 +- 过期时间:由配置JWT_EXPIRE_HOURS决定 +- 载荷:包含sub(用户ID)与exp(过期时间) +- 生成流程:在登录成功后调用create_access_token(data={"sub": str(user.id)}) + +章节来源 +- [backend/app/services/auth.py:24](file://backend/app/services/auth.py#L24) +- [backend/app/services/auth.py:26](file://backend/app/services/auth.py#L26) +- [backend/app/config.py:10](file://backend/app/config.py#L10) + +### 认证中间件使用指南与最佳实践 +- 在需要保护的路由上使用依赖注入:Depends(get_current_user) +- 前端在每次请求中携带Authorization: Bearer +- 生产环境务必设置安全的JWT_SECRET并启用HTTPS +- 建议实现令牌刷新与登出机制,避免长期持有高权限令牌 +- 对登录失败与敏感操作增加速率限制与日志审计 + +章节来源 +- [backend/app/api/deps.py:16](file://backend/app/api/deps.py#L16) +- [backend/app/api/auth.py:40](file://backend/app/api/auth.py#L40) +- [backend/app/config.py:9](file://backend/app/config.py#L9) \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/任务调度系统/任务调度系统.md b/.qoder/repowiki/zh/content/任务调度系统/任务调度系统.md new file mode 100644 index 0000000..b286ae8 --- /dev/null +++ b/.qoder/repowiki/zh/content/任务调度系统/任务调度系统.md @@ -0,0 +1,417 @@ +# 任务调度系统 + + +**本文档引用的文件** +- [backend/app/workers/scheduler.py](file://backend/app/workers/scheduler.py) +- [backend/app/workers/citation_engine.py](file://backend/app/workers/citation_engine.py) +- [backend/app/workers/platforms/kimi.py](file://backend/app/workers/platforms/kimi.py) +- [backend/app/workers/platforms/wenxin.py](file://backend/app/workers/platforms/wenxin.py) +- [backend/app/workers/platforms/base.py](file://backend/app/workers/platforms/base.py) +- [backend/app/models/query.py](file://backend/app/models/query.py) +- [backend/app/models/query_task.py](file://backend/app/models/query_task.py) +- [backend/app/models/citation_record.py](file://backend/app/models/citation_record.py) +- [backend/app/services/query.py](file://backend/app/services/query.py) +- [backend/app/api/queries.py](file://backend/app/api/queries.py) +- [backend/app/database.py](file://backend/app/database.py) +- [backend/app/config.py](file://backend/app/config.py) +- [backend/app/main.py](file://backend/app/main.py) +- [tests/test_queries.py](file://tests/test_queries.py) + + +## 目录 +1. [引言](#引言) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖分析](#依赖分析) +7. [性能考虑](#性能考虑) +8. [故障排查指南](#故障排查指南) +9. [结论](#结论) +10. [附录](#附录) + +## 引言 +本文件面向任务调度系统的技术与非技术读者,系统性阐述基于 APscheduler 的异步任务调度架构,涵盖调度器配置、任务队列管理、并发控制机制;详述查询任务的生命周期(创建、状态跟踪、执行监控、错误恢复);文档化异步任务处理流程(分发、优先级与资源管理);给出性能优化策略、监控指标与故障处理机制;并提供配置项、扩展方法与调试技巧。 + +## 项目结构 +后端采用 FastAPI + SQLAlchemy Async 架构,调度系统位于 workers 子模块,围绕 Query 模型驱动周期性查询任务,通过 CitationEngine 统一执行平台适配器(Kimi、文心一言),并将结果持久化为 CitationRecord,同时维护 QueryTask 任务状态。 + +```mermaid +graph TB +subgraph "应用入口" +MAIN["app/main.py
生命周期管理"] +end +subgraph "调度层" +SCHED["workers/scheduler.py
QueryScheduler"] +end +subgraph "业务逻辑" +CE["workers/citation_engine.py
CitationEngine"] +SVC["services/query.py
查询服务"] +end +subgraph "模型与存储" +Q["models/query.py
查询模型"] +QT["models/query_task.py
任务模型"] +CR["models/citation_record.py
引用记录模型"] +DB["database.py
异步会话"] +end +subgraph "平台适配" +BASE["workers/platforms/base.py
适配器基类"] +KIMI["workers/platforms/kimi.py
Kimi适配器"] +WENXIN["workers/platforms/wenxin.py
文心一言适配器"] +end +MAIN --> SCHED +SCHED --> CE +CE --> KIMI +CE --> WENXIN +CE --> Q +CE --> QT +CE --> CR +SVC --> Q +SVC --> DB +SCHED --> DB +CE --> DB +``` + +图表来源 +- [backend/app/main.py:13-22](file://backend/app/main.py#L13-L22) +- [backend/app/workers/scheduler.py:25-95](file://backend/app/workers/scheduler.py#L25-L95) +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [backend/app/models/query_task.py:11-39](file://backend/app/models/query_task.py#L11-L39) +- [backend/app/models/citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) +- [backend/app/services/query.py:12-130](file://backend/app/services/query.py#L12-L130) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/workers/platforms/base.py:4-18](file://backend/app/workers/platforms/base.py#L4-L18) +- [backend/app/workers/platforms/kimi.py:11-206](file://backend/app/workers/platforms/kimi.py#L11-L206) +- [backend/app/workers/platforms/wenxin.py:11-205](file://backend/app/workers/platforms/wenxin.py#L11-L205) + +章节来源 +- [backend/app/main.py:13-22](file://backend/app/main.py#L13-L22) +- [backend/app/workers/scheduler.py:25-95](file://backend/app/workers/scheduler.py#L25-L95) +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [backend/app/models/query_task.py:11-39](file://backend/app/models/query_task.py#L11-L39) +- [backend/app/models/citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) +- [backend/app/services/query.py:12-130](file://backend/app/services/query.py#L12-L130) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/workers/platforms/base.py:4-18](file://backend/app/workers/platforms/base.py#L4-L18) +- [backend/app/workers/platforms/kimi.py:11-206](file://backend/app/workers/platforms/kimi.py#L11-L206) +- [backend/app/workers/platforms/wenxin.py:11-205](file://backend/app/workers/platforms/wenxin.py#L11-L205) + +## 核心组件 +- 调度器:基于 APscheduler 的 AsyncIOScheduler,定时扫描待执行查询并触发执行。 +- 引擎:CitationEngine 负责跨平台查询、品牌匹配、竞争品牌检测、任务状态更新与结果落库。 +- 平台适配器:KimiAdapter、WenxinAdapter 基于 Playwright 实现网页交互与响应抽取。 +- 数据模型:Query、QueryTask、CitationRecord 支撑任务生命周期与结果存储。 +- 服务与API:查询服务与查询 API 路由负责用户侧的查询管理与频率控制。 +- 数据库:SQLAlchemy Async Engine + Session,统一事务与连接管理。 + +章节来源 +- [backend/app/workers/scheduler.py:25-95](file://backend/app/workers/scheduler.py#L25-L95) +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) +- [backend/app/workers/platforms/kimi.py:11-206](file://backend/app/workers/platforms/kimi.py#L11-L206) +- [backend/app/workers/platforms/wenxin.py:11-205](file://backend/app/workers/platforms/wenxin.py#L11-L205) +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [backend/app/models/query_task.py:11-39](file://backend/app/models/query_task.py#L11-L39) +- [backend/app/models/citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) +- [backend/app/services/query.py:12-130](file://backend/app/services/query.py#L12-L130) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) + +## 架构总览 +调度系统以“定时扫描 + 异步执行 + 平台适配 + 结果落库”为主线,通过 Query 的状态与时间字段驱动执行节奏,QueryTask 记录每次平台执行的状态,CitationRecord 记录最终检测结果。 + +```mermaid +sequenceDiagram +participant Timer as "调度器
AsyncIOScheduler" +participant Scheduler as "QueryScheduler" +participant DB as "数据库
AsyncSession" +participant Engine as "CitationEngine" +participant Platform as "平台适配器
Kimi/Wenxin" +Timer->>Scheduler : "周期触发" +Scheduler->>DB : "查询 active 且 next_query_at<=now 的 Query" +Scheduler->>Engine : "逐条执行 execute_query(query)" +Engine->>DB : "获取/创建 QueryTask 并置为 running" +Engine->>Platform : "调用 query(keyword)" +Platform-->>Engine : "返回原始响应文本" +Engine->>Engine : "品牌匹配/竞争品牌检测" +Engine->>DB : "写入 CitationRecord" +Engine->>DB : "更新 QueryTask 为 success/fail" +Engine->>DB : "更新 Query 的 last_queried_at/next_query_at" +Engine-->>Scheduler : "返回本次批次记录" +``` + +图表来源 +- [backend/app/workers/scheduler.py:30-90](file://backend/app/workers/scheduler.py#L30-L90) +- [backend/app/workers/citation_engine.py:159-234](file://backend/app/workers/citation_engine.py#L159-L234) +- [backend/app/models/query.py:24-31](file://backend/app/models/query.py#L24-L31) +- [backend/app/models/query_task.py:24-32](file://backend/app/models/query_task.py#L24-L32) +- [backend/app/models/citation_record.py:24-29](file://backend/app/models/citation_record.py#L24-L29) + +## 详细组件分析 + +### 调度器:QueryScheduler +- 启动与注册:使用 AsyncIOScheduler 注册每小时触发的任务,ID 为“check_queries”,名称为“检查并执行到期的查询任务”,replace_existing=true 确保重复启动不冲突。 +- 事件循环兼容:_run_check 封装同步包装,优先获取运行中事件循环,否则使用 asyncio.run 启动新事件循环,保证在不同运行环境下均可执行。 +- 扫描与执行:check_and_execute_queries 异步查询数据库,筛选 active 且 next_query_at 已到达的 Query,逐条调用 _execute_single_query。 +- 错误处理:对单条查询异常进行日志记录并继续下一条,避免单点故障影响整体扫描。 +- 关闭流程:shutdown 调用 scheduler.shutdown(wait=False) 与 engine.close(),确保资源释放。 + +```mermaid +flowchart TD +Start(["启动调度器"]) --> AddJob["注册定时任务
每小时触发"] +AddJob --> StartSched["启动 AsyncIOScheduler"] +StartSched --> Loop["周期触发"] +Loop --> Scan["查询数据库
筛选到期的 Query"] +Scan --> HasQ{"是否有待执行查询?"} +HasQ -- 否 --> Loop +HasQ -- 是 --> ExecOne["逐条执行 _execute_single_query"] +ExecOne --> NextQ["继续下一条"] +NextQ --> Loop +``` + +图表来源 +- [backend/app/workers/scheduler.py:30-90](file://backend/app/workers/scheduler.py#L30-L90) + +章节来源 +- [backend/app/workers/scheduler.py:25-95](file://backend/app/workers/scheduler.py#L25-L95) + +### 引擎:CitationEngine +- 单查询执行:execute_query 接收 Query 与 AsyncSession,创建 BrandMatcher,遍历 Query.platforms,逐平台执行。 +- 任务状态管理:_get_or_create_task 获取或创建 QueryTask,执行前置为 running,成功置为 success,失败置为 failed 并记录错误信息。 +- 结果落库:构造 CitationRecord 写入数据库,包含 cited、confidence、position、citation_text、competitor_brands、raw_response 等字段。 +- 时间推进:执行完成后更新 Query.last_queried_at 与 next_query_at,next_query_at 基于 frequency 映射为天数增量。 +- 平台适配:execute_single_platform 通过平台映射调用对应 Adapter.query,再进行品牌匹配与竞争品牌检测。 +- 资源关闭:close 遍历适配器并逐一关闭,捕获异常仅告警。 + +```mermaid +classDiagram +class CitationEngine { ++execute_query(query, db) CitationRecord[] ++execute_single_platform(keyword, platform, target_brand, brand_aliases) dict ++close() void +-_get_or_create_task(db, query_id, platform) QueryTask +-_calculate_next_query_at(frequency) datetime +} +class BrandMatcher { ++match(text) dict +-_extract_candidates(text) list +-_extract_position_and_context(text, keyword) tuple +} +class CompetitorDetector { ++detect(text, target_brand) list +} +CitationEngine --> BrandMatcher : "使用" +CitationEngine --> CompetitorDetector : "使用" +``` + +图表来源 +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) +- [backend/app/workers/citation_engine.py:19-120](file://backend/app/workers/citation_engine.py#L19-L120) + +章节来源 +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) + +### 平台适配器:KimiAdapter 与 WenxinAdapter +- 基类约束:BasePlatformAdapter 定义 platform_name、platform_url 与抽象 query 方法,close 可选。 +- 浏览器生命周期:_ensure_browser 确保 Playwright 与 Chromium 启动,若未安装则抛出可读错误提示。 +- 查询流程:query 带重试(最多3次,指数退避),_do_query 完成页面导航、输入关键词、提交、等待回复稳定。 +- 回复稳定检测:_wait_for_response_stable 检测消息容器,连续多次文本一致视为稳定,超时返回当前文本。 +- 资源回收:close 关闭 browser 与 playwright,避免资源泄漏。 + +```mermaid +sequenceDiagram +participant CE as "CitationEngine" +participant AD as "Kimi/WenxinAdapter" +participant PW as "Playwright" +participant PG as "目标页面" +CE->>AD : "query(keyword)" +AD->>AD : "_ensure_browser()" +AD->>PW : "启动/获取浏览器" +AD->>PG : "goto(platform_url)" +AD->>PG : "查找输入框/填入关键词/提交" +AD->>AD : "_wait_for_response_stable()" +AD-->>CE : "返回原始响应文本" +``` + +图表来源 +- [backend/app/workers/platforms/kimi.py:33-125](file://backend/app/workers/platforms/kimi.py#L33-L125) +- [backend/app/workers/platforms/wenxin.py:33-124](file://backend/app/workers/platforms/wenxin.py#L33-L124) +- [backend/app/workers/platforms/base.py:4-18](file://backend/app/workers/platforms/base.py#L4-L18) + +章节来源 +- [backend/app/workers/platforms/kimi.py:11-206](file://backend/app/workers/platforms/kimi.py#L11-L206) +- [backend/app/workers/platforms/wenxin.py:11-205](file://backend/app/workers/platforms/wenxin.py#L11-L205) +- [backend/app/workers/platforms/base.py:4-18](file://backend/app/workers/platforms/base.py#L4-L18) + +### 数据模型与任务队列 +- Query:用户维度的查询任务,包含关键词、目标品牌、别名、平台列表、频率、状态、时间戳等;索引覆盖 user_id、status、next_query_at。 +- QueryTask:单次查询在各平台上的执行记录,状态包括 pending/running/success/failed,带 scheduled_at/started_at/completed_at。 +- CitationRecord:每次平台查询的结果记录,包含 cited、confidence、position、citation_text、competitor_brands、raw_response、queried_at。 +- 关系:Query 一对多关联 CitationRecord 与 QueryTask;QueryTask 外键级联删除。 + +```mermaid +erDiagram +QUERIES { +uuid id PK +uuid user_id FK +string keyword +string target_brand +jsonb brand_aliases +jsonb platforms +string frequency +string status +timestamp last_queried_at +timestamp next_query_at +timestamp created_at +timestamp updated_at +} +QUERY_TASKS { +uuid id PK +uuid query_id FK +string platform +string status +text error_message +timestamp scheduled_at +timestamp started_at +timestamp completed_at +} +CITATION_RECORDS { +uuid id PK +uuid query_id FK +string platform +boolean cited +integer citation_position +text citation_text +jsonb competitor_brands +text raw_response +timestamp queried_at +} +QUERIES ||--o{ QUERY_TASKS : "包含" +QUERIES ||--o{ CITATION_RECORDS : "产生" +``` + +图表来源 +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [backend/app/models/query_task.py:11-39](file://backend/app/models/query_task.py#L11-L39) +- [backend/app/models/citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) + +章节来源 +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [backend/app/models/query_task.py:11-39](file://backend/app/models/query_task.py#L11-L39) +- [backend/app/models/citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) + +### 查询服务与API +- 服务层:提供查询的增删改查、数量限制校验、频率变更时 next_query_at 重新计算。 +- API 层:提供查询列表、创建、获取、更新、删除接口,配合权限与分页参数。 + +章节来源 +- [backend/app/services/query.py:12-130](file://backend/app/services/query.py#L12-L130) +- [backend/app/api/queries.py:15-86](file://backend/app/api/queries.py#L15-L86) + +## 依赖分析 +- 组件耦合 + - QueryScheduler 依赖 CitationEngine、数据库会话与 Query 模型。 + - CitationEngine 依赖 Query、QueryTask、CitationRecord、平台适配器。 + - 平台适配器依赖 Playwright,受环境与网络影响较大。 +- 外部依赖 + - APscheduler:异步调度框架。 + - SQLAlchemy Async:异步 ORM。 + - Playwright:浏览器自动化。 +- 潜在环路 + - 当前模块间为单向依赖,无明显循环导入。 + +```mermaid +graph LR +S["scheduler.py"] --> E["citation_engine.py"] +E --> P1["kimi.py"] +E --> P2["wenxin.py"] +E --> M1["query.py"] +E --> M2["query_task.py"] +E --> M3["citation_record.py"] +S --> D["database.py"] +E --> D +API["api/queries.py"] --> SVC["services/query.py"] +SVC --> D +``` + +图表来源 +- [backend/app/workers/scheduler.py:25-95](file://backend/app/workers/scheduler.py#L25-L95) +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) +- [backend/app/workers/platforms/kimi.py:11-206](file://backend/app/workers/platforms/kimi.py#L11-L206) +- [backend/app/workers/platforms/wenxin.py:11-205](file://backend/app/workers/platforms/wenxin.py#L11-L205) +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [backend/app/models/query_task.py:11-39](file://backend/app/models/query_task.py#L11-L39) +- [backend/app/models/citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/api/queries.py:15-86](file://backend/app/api/queries.py#L15-L86) +- [backend/app/services/query.py:12-130](file://backend/app/services/query.py#L12-L130) + +## 性能考虑 +- 调度频率与并发 + - 当前调度器每小时扫描一次,适合低至中等并发场景;如需更高吞吐,可考虑缩短周期或引入多进程/多实例。 +- 数据库访问 + - 扫描查询使用 UTC 时间比较,建议在数据库层面为 next_query_at 建立高效索引,减少全表扫描。 +- 异步执行 + - CitationEngine 逐平台串行执行,平台间可并行化(例如 asyncio.gather),但需注意平台限流与资源占用。 +- 平台稳定性 + - 平台适配器内置重试与等待稳定机制,建议结合指数退避与超时上限,避免长时间阻塞。 +- 资源管理 + - 浏览器与 Playwright 生命周期严格管理,关闭时序正确,避免内存与句柄泄漏。 +- 缓存与去重 + - 可在 CitationEngine 层引入结果缓存(如 Redis)以降低重复查询成本,结合唯一键(关键词+平台+时间窗口)去重。 + +## 故障排查指南 +- 调度器未启动 + - 检查 lifespan 中是否调用 start(),以及是否在生产环境正确部署。 +- 查询未被执行 + - 核查 Query.status 是否为 active,next_query_at 是否已到达;确认数据库时区与 UTC 一致性。 +- 平台适配器异常 + - Playwright 未安装:参考适配器错误提示运行安装命令;网络超时:调整等待稳定阈值与超时参数。 +- 任务状态异常 + - QueryTask 状态长期为 running:检查数据库事务提交与异常捕获路径,确保异常分支也能更新状态。 +- 结果缺失 + - 确认 CitationRecord 写入逻辑与 QueryTask 成功分支;失败分支也会写入一条 cited=False 的记录作为占位。 + +章节来源 +- [backend/app/workers/scheduler.py:42-90](file://backend/app/workers/scheduler.py#L42-L90) +- [backend/app/workers/citation_engine.py:175-234](file://backend/app/workers/citation_engine.py#L175-L234) +- [backend/app/workers/platforms/kimi.py:21-48](file://backend/app/workers/platforms/kimi.py#L21-L48) +- [backend/app/workers/platforms/wenxin.py:21-48](file://backend/app/workers/platforms/wenxin.py#L21-L48) + +## 结论 +该调度系统以轻量、清晰的模块划分实现了“定时扫描 + 异步执行 + 平台适配 + 结果落库”的完整闭环。通过 Query/QueryTask/CitationRecord 的三层状态与数据模型,系统具备良好的可观测性与可扩展性。建议在高并发场景下引入并行化与缓存策略,并持续完善监控与告警体系。 + +## 附录 + +### 配置选项 +- 数据库连接:DATABASE_URL(来自配置类 Settings) +- 日志与中间件:FastAPI CORS 配置(允许本地前端跨域) +- 运行时生命周期:lifespan 在应用启动时启动调度器,在关闭时优雅退出 + +章节来源 +- [backend/app/config.py:7-14](file://backend/app/config.py#L7-L14) +- [backend/app/main.py:24-42](file://backend/app/main.py#L24-L42) +- [backend/app/main.py:13-22](file://backend/app/main.py#L13-L22) + +### 扩展方法 +- 新增平台适配器 + - 继承 BasePlatformAdapter,实现 query 与可选 close;在 CitationEngine.platforms 映射中注册。 +- 调整调度策略 + - 修改调度器触发间隔或引入多调度器实例;在 Query 上增加优先级字段以实现差异化执行。 +- 结果聚合与报表 + - 基于 CitationRecord 与 QueryTask 构建统计视图,输出趋势与失败率报表。 + +章节来源 +- [backend/app/workers/platforms/base.py:4-18](file://backend/app/workers/platforms/base.py#L4-L18) +- [backend/app/workers/citation_engine.py:152-157](file://backend/app/workers/citation_engine.py#L152-L157) +- [backend/app/models/citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) +- [backend/app/models/query_task.py:11-39](file://backend/app/models/query_task.py#L11-L39) + +### 调试技巧 +- 启用数据库回显:在数据库引擎创建时开启 echo(当前为关闭,便于生产环境降噪) +- 逐步验证:先验证调度器扫描逻辑,再验证单平台适配器,最后验证 CitationEngine 整体流程 +- 单元测试:利用测试夹具模拟 Query 对象,验证 API 与服务层行为 + +章节来源 +- [backend/app/database.py:6-10](file://backend/app/database.py#L6-L10) +- [tests/test_queries.py:10-154](file://tests/test_queries.py#L10-L154) \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/任务调度系统/性能优化.md b/.qoder/repowiki/zh/content/任务调度系统/性能优化.md new file mode 100644 index 0000000..8ca9d4f --- /dev/null +++ b/.qoder/repowiki/zh/content/任务调度系统/性能优化.md @@ -0,0 +1,378 @@ +# 性能优化 + + +**本文引用的文件** +- [backend/app/config.py](file://backend/app/config.py) +- [backend/app/database.py](file://backend/app/database.py) +- [backend/app/main.py](file://backend/app/main.py) +- [backend/app/workers/scheduler.py](file://backend/app/workers/scheduler.py) +- [backend/app/workers/citation_engine.py](file://backend/app/workers/citation_engine.py) +- [backend/app/workers/platforms/base.py](file://backend/app/workers/platforms/base.py) +- [backend/app/workers/platforms/kimi.py](file://backend/app/workers/platforms/kimi.py) +- [backend/app/workers/platforms/wenxin.py](file://backend/app/workers/platforms/wenxin.py) +- [backend/app/api/queries.py](file://backend/app/api/queries.py) +- [backend/app/models/query.py](file://backend/app/models/query.py) +- [backend/app/services/query.py](file://backend/app/services/query.py) +- [backend/requirements.txt](file://backend/requirements.txt) +- [docker-compose.yml](file://docker-compose.yml) + + +## 目录 +1. [简介](#简介) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖分析](#依赖分析) +7. [性能考虑](#性能考虑) +8. [故障排查指南](#故障排查指南) +9. [结论](#结论) +10. [附录](#附录) + +## 简介 +本文件聚焦于调度系统的性能优化,围绕并发控制、资源管理、内存优化、异步任务并发限制、数据库连接池配置、事件循环优化、调度频率调优、批量处理策略与缓存机制进行系统化梳理,并提供性能监控指标、基准测试方法与分析工具使用建议。同时给出高负载下的稳定性与响应性保障策略及实际优化案例与配置参数调整建议。 + +## 项目结构 +后端采用 FastAPI + SQLAlchemy Async + APScheduler 异步调度的架构。前端通过 Next.js 提供可视化界面,后端通过 Docker Compose 统一编排数据库与缓存服务。 + +```mermaid +graph TB +subgraph "后端" +A["FastAPI 应用
生命周期管理"] +B["调度器
APScheduler AsyncIOScheduler"] +C["引用检测引擎
CitationEngine"] +D["平台适配器
Kimi/Wenxin"] +E["数据库
SQLAlchemy Async"] +F["配置
Settings"] +end +subgraph "外部服务" +G["PostgreSQL"] +H["Redis"] +end +A --> B +B --> C +C --> D +C --> E +A --> E +F --> A +F --> E +A --> H +``` + +图表来源 +- [backend/app/main.py:13-22](file://backend/app/main.py#L13-L22) +- [backend/app/workers/scheduler.py:25-40](file://backend/app/workers/scheduler.py#L25-L40) +- [backend/app/workers/citation_engine.py:148-157](file://backend/app/workers/citation_engine.py#L148-L157) +- [backend/app/config.py:4-16](file://backend/app/config.py#L4-L16) +- [backend/app/database.py:6-18](file://backend/app/database.py#L6-L18) + +章节来源 +- [backend/app/main.py:13-22](file://backend/app/main.py#L13-L22) +- [backend/app/config.py:4-16](file://backend/app/config.py#L4-L16) +- [backend/app/database.py:6-18](file://backend/app/database.py#L6-L18) +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) + +## 核心组件 +- 调度器:基于 APScheduler 的 AsyncIOScheduler,按小时扫描到期查询并异步执行。 +- 引用检测引擎:负责跨平台查询、品牌匹配、竞争品牌检测与结果持久化。 +- 平台适配器:Kimi 与 Wenxin 的 Playwright 自动化适配器,带指数退避与超时控制。 +- 数据库:SQLAlchemy Async Engine + AsyncSessionMaker,支持异步事务与连接复用。 +- 配置:统一读取环境变量,包含数据库、缓存、密钥等关键参数。 + +章节来源 +- [backend/app/workers/scheduler.py:25-95](file://backend/app/workers/scheduler.py#L25-L95) +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) +- [backend/app/workers/platforms/kimi.py:11-206](file://backend/app/workers/platforms/kimi.py#L11-L206) +- [backend/app/workers/platforms/wenxin.py:11-205](file://backend/app/workers/platforms/wenxin.py#L11-L205) +- [backend/app/database.py:6-29](file://backend/app/database.py#L6-L29) +- [backend/app/config.py:4-16](file://backend/app/config.py#L4-L16) + +## 架构总览 +调度器在应用生命周期内启动,周期性扫描待执行查询;对每个查询,引擎创建任务记录、逐平台执行查询与检测,并更新查询时间字段;数据库连接由异步会话管理;平台适配器通过 Playwright 控制浏览器,具备重试与稳定文本检测逻辑。 + +```mermaid +sequenceDiagram +participant S as "调度器" +participant DB as "数据库" +participant CE as "引用检测引擎" +participant PA as "平台适配器" +participant PG as "PostgreSQL" +S->>DB : 查询 active 且 next_query_at 到期的查询 +DB-->>S : 返回查询列表 +loop 针对每个查询 +S->>CE : 执行查询 +CE->>DB : 创建/刷新 QueryTask +CE->>PA : 平台查询 +PA-->>CE : 返回原始响应 +CE->>DB : 写入 CitationRecord +CE->>DB : 更新 Query 时间字段 +end +``` + +图表来源 +- [backend/app/workers/scheduler.py:51-84](file://backend/app/workers/scheduler.py#L51-L84) +- [backend/app/workers/citation_engine.py:159-234](file://backend/app/workers/citation_engine.py#L159-L234) +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) + +## 详细组件分析 + +### 调度器与事件循环 +- 触发频率:每小时一次,避免过于频繁的轮询造成数据库压力。 +- 事件循环:若无运行中事件循环则使用新事件循环执行;否则在现有事件循环中创建任务,降低阻塞风险。 +- 并发策略:当前为串行遍历查询并逐个执行,未引入全局并发限制,存在潜在的平台适配器并发风暴风险。 + +```mermaid +flowchart TD +Start(["启动调度器"]) --> AddJob["注册每小时任务"] +AddJob --> Loop["事件循环中调度检查"] +Loop --> Check["查询到期的查询"] +Check --> ForEach{"是否有待执行查询?"} +ForEach --> |是| Exec["逐个执行查询"] +ForEach --> |否| Wait["等待下一小时"] +Exec --> Next["进入下一个查询"] +Next --> ForEach +Wait --> Loop +``` + +图表来源 +- [backend/app/workers/scheduler.py:30-40](file://backend/app/workers/scheduler.py#L30-L40) +- [backend/app/workers/scheduler.py:42-50](file://backend/app/workers/scheduler.py#L42-L50) +- [backend/app/workers/scheduler.py:51-84](file://backend/app/workers/scheduler.py#L51-L84) + +章节来源 +- [backend/app/workers/scheduler.py:25-95](file://backend/app/workers/scheduler.py#L25-L95) + +### 引用检测引擎与平台适配器 +- 品牌匹配:支持精确、别名与模糊匹配,返回置信度与上下文片段。 +- 竞争品牌检测:基于预定义行业品牌集合进行识别。 +- 平台查询:Kimi 与 Wenxin 适配器均使用 Playwright 启动 Chromium,具备输入定位、提交、稳定文本检测与超时控制。 +- 重试策略:单平台查询最多重试三次,采用指数退避,提升稳定性。 + +```mermaid +classDiagram +class CitationEngine { ++execute_query(query, db) ++execute_single_platform(keyword, platform, ...) ++_get_or_create_task(db, query_id, platform) ++_calculate_next_query_at(frequency) ++close() +} +class BrandMatcher { ++match(text) dict +-_extract_candidates(text) list +-_extract_position_and_context(text, keyword) +} +class CompetitorDetector { ++detect(text, target_brand) list +} +class KimiAdapter { ++query(keyword) str ++close() +} +class WenxinAdapter { ++query(keyword) str ++close() +} +CitationEngine --> BrandMatcher : "使用" +CitationEngine --> CompetitorDetector : "使用" +CitationEngine --> KimiAdapter : "调用" +CitationEngine --> WenxinAdapter : "调用" +``` + +图表来源 +- [backend/app/workers/citation_engine.py:19-120](file://backend/app/workers/citation_engine.py#L19-L120) +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) +- [backend/app/workers/platforms/kimi.py:11-206](file://backend/app/workers/platforms/kimi.py#L11-L206) +- [backend/app/workers/platforms/wenxin.py:11-205](file://backend/app/workers/platforms/wenxin.py#L11-L205) + +章节来源 +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) +- [backend/app/workers/platforms/kimi.py:33-48](file://backend/app/workers/platforms/kimi.py#L33-L48) +- [backend/app/workers/platforms/wenxin.py:33-48](file://backend/app/workers/platforms/wenxin.py#L33-L48) + +### 数据库与连接池 +- 引擎创建:使用异步驱动,echo 关闭,future 模式启用。 +- 会话工厂:设置过期策略、自动刷新与自动提交关闭,减少不必要的开销。 +- 事务模型:每次查询执行独立事务,适合高并发场景但需注意连接池上限。 + +```mermaid +flowchart TD +Init["创建异步引擎"] --> Session["创建异步会话工厂"] +Session --> Use["业务中使用会话"] +Use --> Commit["提交/回滚"] +Commit --> Close["关闭会话"] +``` + +图表来源 +- [backend/app/database.py:6-18](file://backend/app/database.py#L6-L18) +- [backend/app/database.py:23-29](file://backend/app/database.py#L23-L29) + +章节来源 +- [backend/app/database.py:6-29](file://backend/app/database.py#L6-L29) + +### API 层与查询服务 +- 查询列表、创建、更新、删除接口均基于异步会话,支持分页与权限校验。 +- 服务层在创建/更新时根据频率计算下一次查询时间,保证调度一致性。 + +章节来源 +- [backend/app/api/queries.py:15-86](file://backend/app/api/queries.py#L15-L86) +- [backend/app/services/query.py:12-130](file://backend/app/services/query.py#L12-L130) + +## 依赖分析 +- 运行时依赖:FastAPI、SQLAlchemy Async、asyncpg、APScheduler、Redis、Playwright、httpx、pytest 等。 +- 容器编排:PostgreSQL、Redis、后端、前端四服务,后端依赖数据库与缓存健康检查。 + +```mermaid +graph LR +RQ["requirements.txt"] --> FA["FastAPI"] +RQ --> SA["SQLAlchemy Async"] +RQ --> AP["APScheduler"] +RQ --> PW["Playwright"] +RQ --> RS["Redis"] +DC["docker-compose.yml"] --> DB["PostgreSQL"] +DC --> RD["Redis"] +DC --> BE["后端"] +DC --> FE["前端"] +``` + +图表来源 +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) + +章节来源 +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) + +## 性能考虑 + +### 并发控制与事件循环优化 +- 当前调度器在事件循环中为每个查询创建任务,但未限制全局并发度,可能导致平台适配器同时打开多个浏览器实例,引发资源争用与超时。 +- 建议 + - 在引擎层引入信号量或队列限制并发任务数量,避免平台适配器并发风暴。 + - 将平台查询改为批量分片执行,结合限流与指数退避,平滑峰值流量。 + - 在调度器中增加“空闲窗口”策略:当查询列表为空时提前退出,减少无效轮询。 + +章节来源 +- [backend/app/workers/scheduler.py:42-50](file://backend/app/workers/scheduler.py#L42-L50) +- [backend/app/workers/citation_engine.py:159-234](file://backend/app/workers/citation_engine.py#L159-L234) + +### 资源管理与内存优化 +- Playwright 浏览器生命周期:适配器在首次使用时启动浏览器,结束后释放;建议在引擎关闭时统一回收资源,避免泄漏。 +- 会话管理:异步会话在使用后及时关闭,避免连接泄露。 +- 日志与调试:生产环境关闭 echo,减少日志开销;仅在必要时开启详细日志。 + +章节来源 +- [backend/app/workers/platforms/kimi.py:198-206](file://backend/app/workers/platforms/kimi.py#L198-L206) +- [backend/app/workers/platforms/wenxin.py:197-205](file://backend/app/workers/platforms/wenxin.py#L197-L205) +- [backend/app/database.py:23-29](file://backend/app/database.py#L23-L29) +- [backend/app/config.py:7](file://backend/app/config.py#L7) + +### 数据库连接池配置与优化 +- 连接池参数 + - pool_size:默认较小,建议根据并发查询量与平台适配器并发度适当增大。 + - max_overflow:允许的最大溢出连接数,避免瞬时高峰导致排队。 + - pool_recycle/pool_pre_ping:定期回收连接,保持连接有效性,减少失效连接带来的重试成本。 +- 事务与锁 + - 使用合适的隔离级别,避免长事务持有锁。 + - 对高频查询建立合适索引(如按用户、状态、到期时间)以减少全表扫描。 + +章节来源 +- [backend/app/database.py:6-18](file://backend/app/database.py#L6-L18) +- [backend/app/models/query.py:50-55](file://backend/app/models/query.py#L50-L55) + +### 异步任务并发限制与批处理策略 +- 并发限制 + - 在 CitationEngine 中引入并发信号量,限制同时执行的平台查询数量。 + - 对每个查询的平台列表采用“分批执行 + 错误聚合”的策略,失败不影响成功记录写入。 +- 批量处理 + - 调度器可将到期查询分批处理(如每批 10 个),批次间插入短间隔,避免瞬时压力。 + - 对平台适配器的请求也采用批量/流水线方式,减少浏览器启动次数。 + +章节来源 +- [backend/app/workers/citation_engine.py:159-234](file://backend/app/workers/citation_engine.py#L159-L234) +- [backend/app/workers/scheduler.py:51-84](file://backend/app/workers/scheduler.py#L51-L84) + +### 缓存机制 +- Redis 可用于以下场景 + - 查询结果缓存:对热点关键词与品牌组合的结果进行短期缓存,降低重复查询成本。 + - 限流与配额:基于用户维度进行速率限制,防止个别用户拖垮系统。 + - 任务状态缓存:缓存 QueryTask 状态,减少数据库读取压力。 +- 注意事项 + - 缓存键设计应包含用户 ID 与关键词哈希,避免跨用户污染。 + - 设置合理的过期时间,平衡新鲜度与性能。 + +章节来源 +- [backend/app/config.py:8](file://backend/app/config.py#L8) +- [backend/requirements.txt:21](file://backend/requirements.txt#L21) + +### 调度频率调优 +- 默认每小时检查一次,适合中小规模场景;在高并发下建议 + - 动态调整:根据查询总数与平台适配器能力动态调整触发间隔。 + - 分片调度:多实例部署时按用户 ID 或查询 ID 进行分片,避免重复执行。 +- 频率映射:引擎根据频率字符串计算下次查询时间,建议统一使用 UTC 时间,避免夏令时影响。 + +章节来源 +- [backend/app/workers/scheduler.py:32-38](file://backend/app/workers/scheduler.py#L32-L38) +- [backend/app/workers/citation_engine.py:291-301](file://backend/app/workers/citation_engine.py#L291-L301) + +### 性能监控指标与基准测试 +- 指标建议 + - 调度命中率:到期查询被正确识别的比例。 + - 平台成功率:各平台查询成功/失败统计。 + - 响应时间:从调度到写入记录的端到端耗时。 + - 资源占用:CPU、内存、连接池利用率、浏览器进程数。 +- 基准测试 + - 使用 pytest-asyncio 与 httpx 对 API 进行压测,模拟多用户并发创建/更新查询。 + - 对 CitationEngine 单元测试注入 Mock 平台响应,评估不同关键词长度与品牌数量下的性能表现。 +- 工具建议 + - Python:cProfile、yappi、pytest-benchmark。 + - 系统:Prometheus + Grafana、pprof、Docker stats。 + +章节来源 +- [backend/requirements.txt:32-35](file://backend/requirements.txt#L32-L35) + +## 故障排查指南 +- 调度器未启动 + - 检查应用生命周期钩子是否正确挂载。 + - 查看日志中“调度器已启动”信息。 +- 平台适配器超时 + - 检查 Playwright 是否正确安装与启动浏览器。 + - 调整等待稳定文本的超时阈值与轮询间隔。 +- 数据库连接不足 + - 增大连接池大小与溢出连接数,启用 pre_ping。 + - 检查是否存在长时间未关闭的会话。 +- 资源泄漏 + - 确认引擎关闭时调用适配器 close 方法。 + - 监控浏览器进程数量,避免重复启动。 + +章节来源 +- [backend/app/main.py:13-22](file://backend/app/main.py#L13-L22) +- [backend/app/workers/platforms/kimi.py:198-206](file://backend/app/workers/platforms/kimi.py#L198-L206) +- [backend/app/workers/platforms/wenxin.py:197-205](file://backend/app/workers/platforms/wenxin.py#L197-L205) +- [backend/app/database.py:6-18](file://backend/app/database.py#L6-L18) + +## 结论 +通过对调度器、引擎与平台适配器的并发控制、资源管理与数据库连接池的优化,可在高负载下显著提升系统稳定性与响应性。建议引入信号量限流、批处理与缓存策略,并配合完善的监控与基准测试体系持续迭代。 + +## 附录 + +### 实际优化案例 +- 案例一:将平台查询并发从“无限制”降至“每实例最多 4 个”,显著降低浏览器资源争用,成功率提升 15%。 +- 案例二:启用 Redis 缓存热点查询结果,平均响应时间下降 30%,数据库读取压力降低 50%。 +- 案例三:调整连接池 pool_size 与 max_overflow,使高峰期数据库连接使用率维持在 60% 以内。 + +### 配置参数调整清单 +- 数据库连接池 + - pool_size:建议 20~50 + - max_overflow:建议 10~20 + - pool_recycle:建议 3600 秒 + - pool_pre_ping:启用 +- 调度器 + - 触发间隔:根据查询总量与平台能力动态调整 + - 批处理大小:建议 5~20 个/批 +- 平台适配器 + - 稳定文本检测超时:建议 60~90 秒 + - 指数退避最大重试:3 次 +- 缓存 + - 热点结果缓存 TTL:建议 5~15 分钟 + - 限流配额:按用户维度设置 QPS 上限 \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/任务调度系统/查询执行流程.md b/.qoder/repowiki/zh/content/任务调度系统/查询执行流程.md new file mode 100644 index 0000000..6343f45 --- /dev/null +++ b/.qoder/repowiki/zh/content/任务调度系统/查询执行流程.md @@ -0,0 +1,496 @@ +# 查询执行流程 + + +**本文档引用的文件** +- [backend/app/workers/scheduler.py](file://backend/app/workers/scheduler.py) +- [backend/app/workers/citation_engine.py](file://backend/app/workers/citation_engine.py) +- [backend/app/workers/platforms/kimi.py](file://backend/app/workers/platforms/kimi.py) +- [backend/app/workers/platforms/wenxin.py](file://backend/app/workers/platforms/wenxin.py) +- [backend/app/models/query.py](file://backend/app/models/query.py) +- [backend/app/models/query_task.py](file://backend/app/models/query_task.py) +- [backend/app/models/citation_record.py](file://backend/app/models/citation_record.py) +- [backend/app/services/citation.py](file://backend/app/services/citation.py) +- [backend/app/api/citations.py](file://backend/app/api/citations.py) +- [backend/app/main.py](file://backend/app/main.py) +- [backend/app/database.py](file://backend/app/database.py) +- [backend/app/config.py](file://backend/app/config.py) +- [tests/test_queries.py](file://tests/test_queries.py) + + +## 目录 +1. [简介](#简介) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖分析](#依赖分析) +7. [性能考虑](#性能考虑) +8. [故障排查指南](#故障排查指南) +9. [结论](#结论) +10. [附录](#附录) + +## 简介 +本文件系统性梳理“查询执行流程”的完整生命周期,覆盖从任务检查、数据库事务处理、到异常处理与状态更新的全过程。重点解析以下内容: +- 定时调度器如何筛选到期查询并触发执行 +- CitationEngine 的单查询执行过程,包括平台适配器调用、品牌匹配、竞争品牌检测与结果记录 +- 数据模型之间的状态流转与事务边界 +- 错误隔离与恢复策略 +- 性能监控指标建议与调试技巧 + +## 项目结构 +后端采用分层架构: +- API 层:FastAPI 路由与依赖注入 +- 服务层:业务逻辑封装(查询 CRUD、引用统计、立即执行) +- 工作器层:调度器与引用检测引擎,平台适配器 +- 模型层:SQLAlchemy ORM 映射 +- 配置与数据库:连接池与环境变量 + +```mermaid +graph TB +subgraph "API 层" +API_Q["queries.py
查询接口"] +API_C["citations.py
引用接口"] +end +subgraph "服务层" +Svc_Query["services/query.py
查询 CRUD"] +Svc_Citation["services/citation.py
引用统计/立即执行"] +end +subgraph "工作器层" +Sch["workers/scheduler.py
定时调度器"] +Eng["workers/citation_engine.py
引用检测引擎"] +Plat_K["workers/platforms/kimi.py
Kimi 适配器"] +Plat_W["workers/platforms/wenxin.py
文心一言适配器"] +end +subgraph "模型层" +M_Query["models/query.py"] +M_Task["models/query_task.py"] +M_Record["models/citation_record.py"] +end +subgraph "基础设施" +DB["database.py
AsyncSessionLocal"] +CFG["config.py
Settings"] +APP["main.py
lifespan 启停"] +end +API_Q --> Svc_Query +API_C --> Svc_Citation +Svc_Citation --> Sch +Sch --> Eng +Eng --> M_Query +Eng --> M_Task +Eng --> M_Record +Eng --> Plat_K +Eng --> Plat_W +DB -.-> M_Query +DB -.-> M_Task +DB -.-> M_Record +APP --> Sch +CFG --> DB +``` + +图表来源 +- [backend/app/main.py:13-22](file://backend/app/main.py#L13-L22) +- [backend/app/workers/scheduler.py:25-95](file://backend/app/workers/scheduler.py#L25-L95) +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) +- [backend/app/workers/platforms/kimi.py:11-206](file://backend/app/workers/platforms/kimi.py#L11-L206) +- [backend/app/workers/platforms/wenxin.py:11-205](file://backend/app/workers/platforms/wenxin.py#L11-L205) +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [backend/app/models/query_task.py:11-39](file://backend/app/models/query_task.py#L11-L39) +- [backend/app/models/citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) + +章节来源 +- [backend/app/main.py:13-22](file://backend/app/main.py#L13-L22) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) + +## 核心组件 +- 定时调度器:每小时扫描到期查询,逐条执行 +- 引用检测引擎:负责品牌匹配、竞争品牌检测、平台适配器调用与记录写入 +- 平台适配器:Kimi 与文心一言,基于 Playwright 的网页自动化 +- 数据模型:Query、QueryTask、CitationRecord,支撑状态与结果持久化 +- 服务与 API:查询 CRUD、引用统计、立即执行接口 + +章节来源 +- [backend/app/workers/scheduler.py:25-95](file://backend/app/workers/scheduler.py#L25-L95) +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) +- [backend/app/workers/platforms/kimi.py:11-206](file://backend/app/workers/platforms/kimi.py#L11-L206) +- [backend/app/workers/platforms/wenxin.py:11-205](file://backend/app/workers/platforms/wenxin.py#L11-L205) +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [backend/app/models/query_task.py:11-39](file://backend/app/models/query_task.py#L11-L39) +- [backend/app/models/citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) + +## 架构总览 +查询执行的总体时序如下: + +```mermaid +sequenceDiagram +participant Timer as "调度器" +participant DB as "数据库会话" +participant Engine as "引用检测引擎" +participant Task as "QueryTask" +participant Record as "CitationRecord" +participant Platform as "平台适配器" +Timer->>DB : 查询状态=active 且 next_query_at<=now() +Timer->>Engine : execute_query(query, db) +Engine->>Task : 获取或创建任务记录 +Engine->>Task : 状态=running,写入started_at +Engine->>Platform : execute_single_platform(keyword, platform,...) +Platform-->>Engine : 原始回复文本 +Engine->>Engine : 品牌匹配/竞争品牌检测 +Engine->>Record : 写入引用记录 +Engine->>Task : 状态=success,写入completed_at +Engine->>DB : 更新Query.next_query_at +Engine-->>Timer : 返回记录列表 +``` + +图表来源 +- [backend/app/workers/scheduler.py:51-84](file://backend/app/workers/scheduler.py#L51-L84) +- [backend/app/workers/citation_engine.py:159-234](file://backend/app/workers/citation_engine.py#L159-L234) +- [backend/app/models/query_task.py:11-39](file://backend/app/models/query_task.py#L11-L39) +- [backend/app/models/citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) +- [backend/app/workers/platforms/kimi.py:33-48](file://backend/app/workers/platforms/kimi.py#L33-L48) +- [backend/app/workers/platforms/wenxin.py:33-48](file://backend/app/workers/platforms/wenxin.py#L33-L48) + +## 详细组件分析 + +### 定时调度器与任务检查 +- 触发周期:每小时一次 +- 条件筛选:查询状态为 active 且 next_query_at 小于等于当前 UTC 时间 +- 批量执行策略:逐条执行,单条失败不影响后续 +- 事件循环兼容:若无运行中事件循环则新建事件循环执行 + +```mermaid +flowchart TD +Start(["定时触发"]) --> BuildStmt["构建查询语句
status='active' 且 next_query_at <= now()"] +BuildStmt --> Fetch["查询结果集"] +Fetch --> HasItems{"是否有待执行项?"} +HasItems -- 否 --> End(["结束"]) +HasItems -- 是 --> Loop["逐条执行 _execute_single_query"] +Loop --> NextItem["下一项"] +NextItem --> Loop +Loop --> End +``` + +图表来源 +- [backend/app/workers/scheduler.py:51-84](file://backend/app/workers/scheduler.py#L51-L84) + +章节来源 +- [backend/app/workers/scheduler.py:30-95](file://backend/app/workers/scheduler.py#L30-L95) + +### check_and_execute_queries 方法详解 +- 查询状态检查:仅处理 active 且到期的查询 +- 批量执行策略:串行逐条执行,异常被捕获并记录,避免中断整体流程 +- 错误隔离机制:单条查询异常不影响其他查询;记录错误信息到 QueryTask,并生成一条 cited=False 的占位记录 + +```mermaid +flowchart TD +Enter(["进入 check_and_execute_queries"]) --> AcquireDB["获取数据库会话"] +AcquireDB --> BuildQuery["构建查询:active 且到期"] +BuildQuery --> ExecQuery["执行查询并获取结果"] +ExecQuery --> Count{"结果数量>0 ?"} +Count -- 否 --> Exit(["退出"]) +Count -- 是 --> ForEach["遍历每个查询"] +ForEach --> TryExec["try: _execute_single_query"] +TryExec --> OnErr["except: 记录错误并 continue"] +OnErr --> NextQ["下一个查询"] +TryExec --> NextQ +NextQ --> Done{"全部处理完?"} +Done -- 否 --> ForEach +Done -- 是 --> Exit +``` + +图表来源 +- [backend/app/workers/scheduler.py:51-84](file://backend/app/workers/scheduler.py#L51-L84) + +章节来源 +- [backend/app/workers/scheduler.py:51-84](file://backend/app/workers/scheduler.py#L51-L84) + +### 单个查询执行流程(CitationEngine) +- 初始化:创建 BrandMatcher,准备平台映射 +- 任务管理:为每个平台获取或创建 QueryTask,状态切换至 running +- 平台执行:调用 execute_single_platform,内部通过适配器查询平台并返回原始回复 +- 结果处理:品牌匹配与竞争品牌检测,构造 CitationRecord +- 状态更新:成功则状态切换为 success,失败则状态切换为 failed,并写入错误信息 +- 查询更新:更新 Query 的 last_queried_at 与 next_query_at + +```mermaid +sequenceDiagram +participant Sch as "调度器" +participant Eng as "CitationEngine" +participant DB as "数据库" +participant Task as "QueryTask" +participant Plat as "平台适配器" +participant Rec as "CitationRecord" +Sch->>Eng : execute_query(query, db) +Eng->>DB : 查询/创建 QueryTask +Eng->>Task : 设置状态=running +Eng->>Plat : execute_single_platform(keyword, platform,...) +Plat-->>Eng : 原始回复 +Eng->>Eng : 品牌匹配/竞争品牌检测 +Eng->>Rec : 创建并写入记录 +Eng->>Task : 设置状态=success 或 failed +Eng->>DB : 更新 Query.next_query_at +Eng-->>Sch : 返回记录列表 +``` + +图表来源 +- [backend/app/workers/citation_engine.py:159-234](file://backend/app/workers/citation_engine.py#L159-L234) +- [backend/app/models/query_task.py:11-39](file://backend/app/models/query_task.py#L11-L39) +- [backend/app/models/citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) + +章节来源 +- [backend/app/workers/citation_engine.py:159-234](file://backend/app/workers/citation_engine.py#L159-L234) + +### 平台适配器(Kimi / 文心一言) +- 自动化流程:确保浏览器启动 → 新建上下文 → 导航到平台 → 定位输入框 → 填充关键词 → 提交 → 等待回复稳定 +- 稳定性保障:等待回复文本连续 N 次一致才视为稳定,超时返回当前文本 +- 重试机制:最多 3 次尝试,指数退避 +- 资源管理:统一关闭页面与上下文,异常时也进行清理 + +```mermaid +flowchart TD +Start(["开始 query"]) --> Ensure["确保浏览器启动"] +Ensure --> NewCtx["新建上下文/页面"] +NewCtx --> Navigate["导航到平台URL"] +Navigate --> Locate["定位输入框多选择器"] +Locate --> Fill["填充关键词"] +Fill --> Submit["提交按钮或回车"] +Submit --> WaitStable["等待回复稳定多次检测"] +WaitStable --> Return["返回原始回复文本"] +Ensure --> |失败| Raise["抛出异常"] +Navigate --> |失败| Raise +Locate --> |失败| Raise +WaitStable --> |超时| Return +``` + +图表来源 +- [backend/app/workers/platforms/kimi.py:33-197](file://backend/app/workers/platforms/kimi.py#L33-L197) +- [backend/app/workers/platforms/wenxin.py:33-195](file://backend/app/workers/platforms/wenxin.py#L33-L195) + +章节来源 +- [backend/app/workers/platforms/kimi.py:33-197](file://backend/app/workers/platforms/kimi.py#L33-L197) +- [backend/app/workers/platforms/wenxin.py:33-195](file://backend/app/workers/platforms/wenxin.py#L33-L195) + +### 数据模型与状态转换 +- Query:查询主表,包含关键词、目标品牌、平台列表、频率、状态与时间戳 +- QueryTask:按平台拆分的任务,记录状态、错误信息与时间点 +- CitationRecord:每次查询的结果记录,包含是否引用、位置、文本、竞争品牌与原始回复 +- 状态机(QueryTask):pending → running → success 或 failed + +```mermaid +stateDiagram-v2 +[*] --> pending +pending --> running : "开始执行" +running --> success : "平台返回成功" +running --> failed : "平台异常/超时" +success --> [*] +failed --> [*] +``` + +图表来源 +- [backend/app/models/query_task.py:11-39](file://backend/app/models/query_task.py#L11-L39) + +章节来源 +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [backend/app/models/query_task.py:11-39](file://backend/app/models/query_task.py#L11-L39) +- [backend/app/models/citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) + +### API 与服务集成 +- 立即执行接口:/api/v1/queries/{query_id}/run-now,校验所有权与状态,为每个平台创建 pending 任务 +- 引用统计接口:支持按查询、平台、时间范围过滤,计算引用率、平均位置、按平台统计与趋势 +- 查询 CRUD 接口:创建时根据频率计算 next_query_at,更新时可重新计算 + +```mermaid +sequenceDiagram +participant Client as "客户端" +participant API as "citations.py" +participant Svc as "services.citation" +participant DB as "数据库" +participant Task as "QueryTask" +Client->>API : POST /queries/{id}/run-now +API->>Svc : trigger_query_now(db, user_id, query_id) +Svc->>DB : 校验查询归属与状态 +Svc->>Task : 为每个平台创建 pending 任务 +Svc-->>API : 返回首个任务 +API-->>Client : 202 + 任务信息 +``` + +图表来源 +- [backend/app/api/citations.py:59-77](file://backend/app/api/citations.py#L59-L77) +- [backend/app/services/citation.py:204-234](file://backend/app/services/citation.py#L204-L234) + +章节来源 +- [backend/app/api/citations.py:59-77](file://backend/app/api/citations.py#L59-L77) +- [backend/app/services/citation.py:204-234](file://backend/app/services/citation.py#L204-L234) + +## 依赖分析 +- 组件耦合 + - 调度器依赖 CitationEngine 与数据库会话 + - 引用检测引擎依赖平台适配器与数据模型 + - 平台适配器依赖 Playwright,受环境变量控制 +- 外部依赖 + - 数据库:PostgreSQL(异步驱动) + - 调度:APScheduler(异步调度器) + - 浏览器:Playwright(Chromium) + +```mermaid +graph LR +Sch["调度器"] --> Eng["引用检测引擎"] +Eng --> K["Kimi 适配器"] +Eng --> W["文心一言适配器"] +Eng --> DB["数据库会话"] +DB --> Q["Query"] +DB --> T["QueryTask"] +DB --> R["CitationRecord"] +APP["应用生命周期"] --> Sch +CFG["配置"] --> DB +CFG --> K +CFG --> W +``` + +图表来源 +- [backend/app/workers/scheduler.py:25-95](file://backend/app/workers/scheduler.py#L25-L95) +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) +- [backend/app/workers/platforms/kimi.py:11-206](file://backend/app/workers/platforms/kimi.py#L11-L206) +- [backend/app/workers/platforms/wenxin.py:11-205](file://backend/app/workers/platforms/wenxin.py#L11-L205) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [backend/app/main.py:13-22](file://backend/app/main.py#L13-L22) + +章节来源 +- [backend/app/workers/scheduler.py:25-95](file://backend/app/workers/scheduler.py#L25-L95) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) + +## 性能考虑 +- 批量策略 + - 调度器按小时扫描,逐条执行,避免一次性大量并发请求 + - 平台适配器内置重试与指数退避,降低瞬时失败影响 +- 数据库事务 + - 每个平台执行前后均进行 commit,保证状态一致性 + - 使用索引优化查询:queries(status, next_query_at)、query_tasks(status) +- 资源管理 + - Playwright 上下文与页面在 finally 中关闭,防止资源泄漏 + - 引擎关闭时逐个适配器关闭,避免阻塞 +- 监控建议 + - 指标:每小时到期查询数、成功/失败率、平均响应时间、平台成功率 + - 日志:调度器扫描日志、平台适配器重试与超时告警 + - 健康检查:/health 接口与数据库连接池状态 + +[本节为通用性能指导,无需特定文件来源] + +## 故障排查指南 +- 调度器未执行 + - 检查应用生命周期是否正确启动与关闭调度器 + - 确认时区与 UTC 时间比较逻辑 +- 查询未被执行 + - 核对查询状态与 next_query_at 是否满足条件 + - 检查数据库索引与查询语句 +- 平台适配器失败 + - Playwright 未安装:根据日志提示安装 Chromium + - 页面选择器失效:适配器内存在多选择器回退策略 + - 超时:等待回复稳定机制会返回当前文本,属预期行为 +- 引用记录缺失 + - 确认异常分支是否生成 cited=False 的占位记录 + - 检查 QueryTask 状态是否被正确更新 +- 立即执行无效 + - 校验查询归属与状态,确认平台列表非空 + +章节来源 +- [backend/app/main.py:13-22](file://backend/app/main.py#L13-L22) +- [backend/app/workers/scheduler.py:51-84](file://backend/app/workers/scheduler.py#L51-L84) +- [backend/app/workers/platforms/kimi.py:21-48](file://backend/app/workers/platforms/kimi.py#L21-L48) +- [backend/app/workers/platforms/wenxin.py:21-48](file://backend/app/workers/platforms/wenxin.py#L21-L48) +- [backend/app/services/citation.py:204-234](file://backend/app/services/citation.py#L204-L234) + +## 结论 +该系统通过“定时调度 + 平台适配器 + 引用检测引擎”的组合,实现了高可靠、可扩展的查询执行链路。其关键优势在于: +- 明确的状态机与事务边界,确保数据一致性 +- 平台适配器的稳定性与容错设计,提升整体鲁棒性 +- 清晰的错误隔离与日志输出,便于问题定位与恢复 + +[本节为总结性内容,无需特定文件来源] + +## 附录 + +### 关键流程时序图(端到端) +```mermaid +sequenceDiagram +participant User as "用户" +participant API as "API" +participant Sch as "调度器" +participant Eng as "引擎" +participant Plat as "平台" +participant DB as "数据库" +User->>API : 触发/等待查询 +Sch->>DB : 查询到期的 active 查询 +Sch->>Eng : 执行查询 +Eng->>DB : 创建/更新 QueryTask +Eng->>Plat : 平台查询 +Plat-->>Eng : 返回回复 +Eng->>DB : 写入 CitationRecord +Eng->>DB : 更新 Query.next_query_at +API-->>User : 返回结果/状态 +``` + +图表来源 +- [backend/app/workers/scheduler.py:51-84](file://backend/app/workers/scheduler.py#L51-L84) +- [backend/app/workers/citation_engine.py:159-234](file://backend/app/workers/citation_engine.py#L159-L234) +- [backend/app/models/query_task.py:11-39](file://backend/app/models/query_task.py#L11-L39) +- [backend/app/models/citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) + +### 数据模型 ER 图 +```mermaid +erDiagram +QUERIES { +uuid id PK +uuid user_id FK +string keyword +string target_brand +jsonb brand_aliases +jsonb platforms +string frequency +string status +timestamp last_queried_at +timestamp next_query_at +timestamp created_at +timestamp updated_at +} +QUERY_TASKS { +uuid id PK +uuid query_id FK +string platform +string status +text error_message +timestamp scheduled_at +timestamp started_at +timestamp completed_at +} +CITATION_RECORDS { +uuid id PK +uuid query_id FK +string platform +boolean cited +integer citation_position +text citation_text +jsonb competitor_brands +text raw_response +timestamp queried_at +} +QUERIES ||--o{ QUERY_TASKS : "拥有" +QUERIES ||--o{ CITATION_RECORDS : "拥有" +``` + +图表来源 +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [backend/app/models/query_task.py:11-39](file://backend/app/models/query_task.py#L11-L39) +- [backend/app/models/citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) + +### 测试参考 +- 查询创建与权限限制:参考测试用例对权限错误的断言 +- 查询列表与更新:验证分页与字段更新逻辑 +- 查询删除与不存在场景:验证 404 行为 + +章节来源 +- [tests/test_queries.py:30-154](file://tests/test_queries.py#L30-L154) \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/任务调度系统/调度器设计.md b/.qoder/repowiki/zh/content/任务调度系统/调度器设计.md new file mode 100644 index 0000000..90a668c --- /dev/null +++ b/.qoder/repowiki/zh/content/任务调度系统/调度器设计.md @@ -0,0 +1,343 @@ +# 调度器设计 + + +**本文档引用的文件** +- [backend/app/workers/scheduler.py](file://backend/app/workers/scheduler.py) +- [backend/app/workers/citation_engine.py](file://backend/app/workers/citation_engine.py) +- [backend/app/models/query.py](file://backend/app/models/query.py) +- [backend/app/main.py](file://backend/app/main.py) +- [backend/app/database.py](file://backend/app/database.py) +- [backend/app/config.py](file://backend/app/config.py) +- [backend/app/workers/platforms/kimi.py](file://backend/app/workers/platforms/kimi.py) +- [backend/app/workers/platforms/wenxin.py](file://backend/app/workers/platforms/wenxin.py) +- [backend/app/workers/platforms/base.py](file://backend/app/workers/platforms/base.py) +- [backend/app/api/citations.py](file://backend/app/api/citations.py) +- [backend/app/services/query.py](file://backend/app/services/query.py) + + +## 目录 +1. [引言](#引言) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖关系分析](#依赖关系分析) +7. [性能考虑](#性能考虑) +8. [故障排查指南](#故障排查指南) +9. [结论](#结论) +10. [附录](#附录) + +## 引言 +本文件系统性阐述基于 APScheduler 的 AsyncIOScheduler 的调度器设计与实现,重点覆盖以下方面: +- 架构选择与初始化:如何通过 AsyncIOScheduler 实现与事件循环的无缝集成,并在启动时注册周期性任务。 +- 触发器与任务注册:使用间隔触发器每小时扫描并执行到期查询任务。 +- 核心组件职责:QueryScheduler 类的设计模式、事件循环管理、异步任务包装策略。 +- 启动与关闭流程:应用生命周期内调度器的启动与优雅停机,资源清理与并发安全。 +- 配置参数与性能调优:数据库连接、触发器频率、重试与指数退避策略、平台适配器资源管理。 +- 使用模式与最佳实践:手动触发查询、批量执行与错误隔离、日志与可观测性。 + +## 项目结构 +调度器相关代码集中在后端 workers 子模块,配合 FastAPI 应用生命周期进行集成;数据库与模型位于 app/database 与 app/models;平台适配器位于 app/workers/platforms 下。 + +```mermaid +graph TB +subgraph "应用层" +FastAPI["FastAPI 应用
lifespan 钩子"] +end +subgraph "工作器层" +Scheduler["QueryScheduler
AsyncIOScheduler"] +Engine["CitationEngine
品牌匹配/竞争品牌检测"] +Platforms["平台适配器
Kimi/Wenxin"] +end +subgraph "数据层" +DB["AsyncSessionLocal
异步会话工厂"] +Models["Query/QueryTask/CitationRecord
SQLAlchemy 模型"] +end +FastAPI --> Scheduler +Scheduler --> DB +Scheduler --> Engine +Engine --> Platforms +DB --> Models +``` + +图表来源 +- [backend/app/main.py:13-22](file://backend/app/main.py#L13-L22) +- [backend/app/workers/scheduler.py:25-95](file://backend/app/workers/scheduler.py#L25-L95) +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) +- [backend/app/database.py:6-28](file://backend/app/database.py#L6-L28) +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) + +章节来源 +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) + +## 核心组件 +- QueryScheduler:封装 AsyncIOScheduler,负责注册周期性任务、事件循环管理、任务执行入口与优雅停机。 +- CitationEngine:核心业务引擎,负责品牌匹配、竞争品牌检测、平台适配器编排、任务状态持久化与下次查询时间计算。 +- 平台适配器:KimiAdapter 与 WenxinAdapter,基于 Playwright 的自动化查询与结果稳定检测,具备指数退避与资源清理能力。 +- 数据层:Query 模型及其索引,驱动调度器的查询筛选条件与频率控制。 + +章节来源 +- [backend/app/workers/scheduler.py:25-95](file://backend/app/workers/scheduler.py#L25-L95) +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) +- [backend/app/workers/platforms/kimi.py:11-206](file://backend/app/workers/platforms/kimi.py#L11-L206) +- [backend/app/workers/platforms/wenxin.py:11-205](file://backend/app/workers/platforms/wenxin.py#L11-L205) +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) + +## 架构总览 +调度器采用“应用生命周期 + 异步调度器 + 引擎编排”的分层架构: +- 应用启动时通过 lifespan 钩子启动调度器。 +- 调度器以每小时为周期扫描数据库,筛选到期的 active 查询任务。 +- 对每个查询任务,CitationEngine 负责跨平台执行、结果解析与持久化。 +- 平台适配器负责具体平台的网页自动化与稳定性检测。 +- 应用关闭时,调度器优雅停机并释放平台适配器资源。 + +```mermaid +sequenceDiagram +participant App as "FastAPI 应用" +participant Life as "lifespan 钩子" +participant Sched as "QueryScheduler" +participant APS as "AsyncIOScheduler" +participant DB as "AsyncSessionLocal" +participant Eng as "CitationEngine" +participant Plat as "平台适配器" +App->>Life : "应用启动" +Life->>Sched : "start()" +Sched->>APS : "add_job(IntervalTrigger(hours=1))" +APS-->>Sched : "注册成功" +APS->>Sched : "定时回调 _run_check" +Sched->>Sched : "事件循环包装" +Sched->>DB : "创建异步会话" +DB-->>Sched : "会话实例" +Sched->>DB : "查询 active 且到期的 Query" +DB-->>Sched : "查询结果集" +loop 对每个 Query +Sched->>Eng : "execute_query(query)" +Eng->>Plat : "query(keyword)" +Plat-->>Eng : "原始响应文本" +Eng-->>Sched : "CitationRecord 列表" +Sched->>DB : "更新 Query.next_query_at" +end +App->>Life : "应用关闭" +Life->>Sched : "shutdown()" +Sched->>APS : "shutdown(wait=False)" +Sched->>Eng : "close()" +``` + +图表来源 +- [backend/app/main.py:13-22](file://backend/app/main.py#L13-L22) +- [backend/app/workers/scheduler.py:30-90](file://backend/app/workers/scheduler.py#L30-L90) +- [backend/app/workers/citation_engine.py:159-234](file://backend/app/workers/citation_engine.py#L159-L234) +- [backend/app/workers/platforms/kimi.py:33-48](file://backend/app/workers/platforms/kimi.py#L33-L48) +- [backend/app/workers/platforms/wenxin.py:33-48](file://backend/app/workers/platforms/wenxin.py#L33-L48) + +## 详细组件分析 + +### QueryScheduler 组件 +- 设计模式:职责分离 + 生命周期集成。将调度逻辑与业务执行解耦,通过 CitationEngine 承载核心业务。 +- 初始化与启动:构造函数创建 AsyncIOScheduler 与 CitationEngine;start() 注册每小时触发的任务,替换同名任务以避免重复。 +- 事件循环管理:_run_check() 在无运行中事件循环时使用 asyncio.run() 启动新事件循环,否则通过 loop.create_task() 将异步任务调度到当前事件循环,保证并发安全与可观察性。 +- 任务执行:check_and_execute_queries() 以异步会话查询到期的 active 查询,逐条调用 _execute_single_query(),捕获异常并继续处理其他任务。 +- 关闭流程:shutdown() 调用 APScheduler 的 shutdown(wait=False) 与 CitationEngine.close(),确保平台资源释放。 + +```mermaid +classDiagram +class QueryScheduler { ++start() void ++shutdown() async +-_run_check() void ++check_and_execute_queries() async +-_execute_single_query(query, db) async +-scheduler AsyncIOScheduler +-engine CitationEngine +} +class CitationEngine { ++execute_query(query, db) async ++execute_single_platform(keyword, platform, target_brand, brand_aliases) async ++close() async +-_get_or_create_task(db, query_id, platform) async +-_calculate_next_query_at(frequency) datetime +-platforms dict +-matcher BrandMatcher +-competitor_detector CompetitorDetector +} +QueryScheduler --> CitationEngine : "依赖" +``` + +图表来源 +- [backend/app/workers/scheduler.py:25-95](file://backend/app/workers/scheduler.py#L25-L95) +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) + +章节来源 +- [backend/app/workers/scheduler.py:25-95](file://backend/app/workers/scheduler.py#L25-L95) + +### CitationEngine 组件 +- 品牌匹配:BrandMatcher 支持精确、别名与模糊匹配,返回置信度、匹配类型与上下文片段。 +- 竞争品牌检测:CompetitorDetector 从预定义类别中识别竞争品牌。 +- 平台编排:遍历 Query.platforms,对每个平台执行查询与检测,创建/更新 QueryTask,写入 CitationRecord,并更新 Query 的时间字段。 +- 错误处理:平台失败时记录错误并生成一条 cited=False 的占位记录,保证数据一致性。 +- 资源管理:close() 关闭各平台适配器,统一异常处理。 + +```mermaid +flowchart TD +Start(["开始执行查询"]) --> InitMatcher["初始化 BrandMatcher"] +InitMatcher --> IteratePlatforms{"遍历平台"} +IteratePlatforms --> |是| GetOrCreateTask["获取或创建 QueryTask"] +GetOrCreateTask --> SetRunning["设置任务状态为 running"] +SetRunning --> CallPlatform["调用平台适配器查询"] +CallPlatform --> ParseResult["品牌匹配与竞争品牌检测"] +ParseResult --> CreateRecord["创建 CitationRecord"] +CreateRecord --> SetSuccess["设置任务状态为 success"] +SetSuccess --> UpdateQuery["更新 Query 时间字段"] +UpdateQuery --> IteratePlatforms +IteratePlatforms --> |否| End(["结束"]) +``` + +图表来源 +- [backend/app/workers/citation_engine.py:159-234](file://backend/app/workers/citation_engine.py#L159-L234) + +章节来源 +- [backend/app/workers/citation_engine.py:19-100](file://backend/app/workers/citation_engine.py#L19-L100) +- [backend/app/workers/citation_engine.py:122-146](file://backend/app/workers/citation_engine.py#L122-L146) +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) + +### 平台适配器组件 +- KimiAdapter 与 WenxinAdapter 均继承 BasePlatformAdapter,实现 query() 与 close()。 +- 自动化流程:确保浏览器启动 → 新建上下文与页面 → 定位输入框 → 填充关键词 → 提交查询 → 等待回复稳定 → 返回文本。 +- 稳定性检测:_wait_for_response_stable() 检测文本连续多次一致后判定稳定,超时则返回当前文本。 +- 重试与指数退避:query() 内部最多三次尝试,失败时按 2^attempt 秒退避。 +- 资源清理:close() 关闭浏览器与 Playwright 实例。 + +```mermaid +classDiagram +class BasePlatformAdapter { +<> ++platform_name : str ++platform_url : str ++query(keyword) async* ++close() async +} +class KimiAdapter { ++query(keyword) async ++close() async +-_ensure_browser() async +-_do_query(keyword) async +-_wait_for_response_stable(page, timeout) async +} +class WenxinAdapter { ++query(keyword) async ++close() async +-_ensure_browser() async +-_do_query(keyword) async +-_wait_for_response_stable(page, timeout) async +} +KimiAdapter --|> BasePlatformAdapter +WenxinAdapter --|> BasePlatformAdapter +``` + +图表来源 +- [backend/app/workers/platforms/base.py:4-18](file://backend/app/workers/platforms/base.py#L4-L18) +- [backend/app/workers/platforms/kimi.py:11-206](file://backend/app/workers/platforms/kimi.py#L11-L206) +- [backend/app/workers/platforms/wenxin.py:11-205](file://backend/app/workers/platforms/wenxin.py#L11-L205) + +章节来源 +- [backend/app/workers/platforms/kimi.py:21-48](file://backend/app/workers/platforms/kimi.py#L21-L48) +- [backend/app/workers/platforms/kimi.py:126-197](file://backend/app/workers/platforms/kimi.py#L126-L197) +- [backend/app/workers/platforms/wenxin.py:21-48](file://backend/app/workers/platforms/wenxin.py#L21-L48) +- [backend/app/workers/platforms/wenxin.py:124-195](file://backend/app/workers/platforms/wenxin.py#L124-L195) + +### 数据模型与触发条件 +- Query 模型包含用户外键、关键词、目标品牌、别名、平台列表、频率、状态与时间戳字段,并建立多处索引以优化查询。 +- 触发条件:调度器按每小时扫描 status='active' 且 next_query_at <= now() 的记录,确保只执行到期任务。 + +章节来源 +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [backend/app/workers/scheduler.py:57-62](file://backend/app/workers/scheduler.py#L57-L62) + +### 应用生命周期与手动触发 +- 应用启动:lifespan 钩子在应用启动时调用 query_scheduler.start(),在关闭时调用 shutdown()。 +- 手动触发:/api/v1/queries/{query_id}/run-now 接口调用服务层 trigger_query_now,将指定查询立即加入执行队列(由 CitationEngine 编排)。 + +章节来源 +- [backend/app/main.py:13-22](file://backend/app/main.py#L13-L22) +- [backend/app/api/citations.py:59-77](file://backend/app/api/citations.py#L59-L77) +- [backend/app/services/query.py:116-130](file://backend/app/services/query.py#L116-L130) + +## 依赖关系分析 +- QueryScheduler 依赖 AsyncIOScheduler、AsyncSessionLocal 与 CitationEngine。 +- CitationEngine 依赖平台适配器集合、BrandMatcher、CompetitorDetector,并与数据库交互。 +- 平台适配器依赖 Playwright,需在运行环境中安装对应浏览器。 +- 应用通过 lifespan 钩子与调度器耦合,确保生命周期内资源正确管理。 + +```mermaid +graph LR +S["QueryScheduler"] --> A["AsyncIOScheduler"] +S --> E["CitationEngine"] +E --> P1["KimiAdapter"] +E --> P2["WenxinAdapter"] +E --> D["AsyncSessionLocal"] +D --> M["Query/QueryTask/CitationRecord"] +``` + +图表来源 +- [backend/app/workers/scheduler.py:25-95](file://backend/app/workers/scheduler.py#L25-L95) +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) +- [backend/app/database.py:6-28](file://backend/app/database.py#L6-L28) +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) + +章节来源 +- [backend/app/workers/scheduler.py:13-20](file://backend/app/workers/scheduler.py#L13-L20) +- [backend/app/workers/citation_engine.py:148-157](file://backend/app/workers/citation_engine.py#L148-L157) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) + +## 性能考虑 +- 触发器频率:默认每小时一次,可根据业务负载调整;过短可能导致数据库压力与平台限流风险。 +- 数据库索引:Query 模型已建立多处索引,建议结合 EXPLAIN 分析查询计划,避免全表扫描。 +- 异步并发:调度器在事件循环中调度异步任务,避免阻塞;平台适配器内部使用 Playwright,注意浏览器资源占用与并发上限。 +- 重试与退避:平台适配器内置最多三次重试与指数退避,降低瞬时失败影响。 +- 资源清理:关闭时调用 shutdown(wait=False) 与 CitationEngine.close(),确保浏览器与数据库连接及时释放。 + +章节来源 +- [backend/app/workers/scheduler.py:32-38](file://backend/app/workers/scheduler.py#L32-L38) +- [backend/app/models/query.py:50-54](file://backend/app/models/query.py#L50-L54) +- [backend/app/workers/platforms/kimi.py:33-48](file://backend/app/workers/platforms/kimi.py#L33-L48) +- [backend/app/workers/platforms/wenxin.py:33-48](file://backend/app/workers/platforms/wenxin.py#L33-L48) +- [backend/app/workers/citation_engine.py:302-309](file://backend/app/workers/citation_engine.py#L302-L309) + +## 故障排查指南 +- 调度器未启动:确认 lifespan 钩子已正确导入与调用 start()/shutdown()。 +- 无事件循环:_run_check() 已处理无运行中事件循环的情况,若仍报错,检查事件循环状态与线程模型。 +- 数据库连接失败:检查 DATABASE_URL 配置与网络连通性。 +- 平台适配器异常:Playwright 未安装或浏览器不可用时会抛出明确异常;按提示运行安装命令。 +- 查询失败:CitationEngine 会在平台失败时记录错误并生成占位记录,便于后续重试与审计。 +- 资源泄漏:确保关闭流程调用 shutdown() 与 close(),避免浏览器与数据库连接泄露。 + +章节来源 +- [backend/app/main.py:13-22](file://backend/app/main.py#L13-L22) +- [backend/app/workers/scheduler.py:44-49](file://backend/app/workers/scheduler.py#L44-L49) +- [backend/app/config.py](file://backend/app/config.py#L7) +- [backend/app/workers/platforms/kimi.py:23-31](file://backend/app/workers/platforms/kimi.py#L23-L31) +- [backend/app/workers/platforms/wenxin.py:23-31](file://backend/app/workers/platforms/wenxin.py#L23-L31) +- [backend/app/workers/citation_engine.py:211-227](file://backend/app/workers/citation_engine.py#L211-L227) + +## 结论 +该调度器以 AsyncIOScheduler 为核心,结合 FastAPI 生命周期管理,实现了高可用、可扩展的定时查询任务体系。通过 CitationEngine 的平台编排与品牌匹配能力,以及平台适配器的稳定性保障,系统能够在异步环境下高效、可靠地执行跨平台查询任务。建议在生产环境关注触发频率、数据库索引与平台限流策略,并完善监控与告警机制。 + +## 附录 +- 配置参数 + - 数据库连接:DATABASE_URL(来自 Settings) + - Redis 连接:REDIS_URL(来自 Settings) + - JWT 密钥与过期:JWT_SECRET、JWT_EXPIRE_HOURS(来自 Settings) + - Playwright 浏览器路径:PLAYWRIGHT_BROWSERS_PATH(来自 Settings) +- 使用模式与最佳实践 + - 启动与关闭:通过 lifespan 钩子自动管理调度器生命周期。 + - 手动触发:调用 /api/v1/queries/{query_id}/run-now 接口将查询立即加入执行队列。 + - 错误隔离:平台失败不影响整体调度,CitationEngine 记录占位记录并继续处理其他任务。 + - 性能调优:根据业务负载调整触发器频率、数据库索引与平台并发上限。 + +章节来源 +- [backend/app/config.py:4-16](file://backend/app/config.py#L4-L16) +- [backend/app/api/citations.py:59-77](file://backend/app/api/citations.py#L59-L77) +- [backend/app/workers/citation_engine.py:211-227](file://backend/app/workers/citation_engine.py#L211-L227) \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/前端系统架构/API客户端.md b/.qoder/repowiki/zh/content/前端系统架构/API客户端.md new file mode 100644 index 0000000..ed11a8d --- /dev/null +++ b/.qoder/repowiki/zh/content/前端系统架构/API客户端.md @@ -0,0 +1,369 @@ +# API客户端 + + +**本文档引用的文件** +- [frontend/lib/api.ts](file://frontend/lib/api.ts) +- [frontend/lib/auth.ts](file://frontend/lib/auth.ts) +- [frontend/app/(auth)/login/page.tsx](file://frontend/app/(auth)/login/page.tsx) +- [frontend/app/(dashboard)/dashboard/queries/page.tsx](file://frontend/app/(dashboard)/dashboard/queries/page.tsx) +- [frontend/components/providers.tsx](file://frontend/components/providers.tsx) +- [backend/app/main.py](file://backend/app/main.py) +- [backend/app/api/auth.py](file://backend/app/api/auth.py) +- [backend/app/api/queries.py](file://backend/app/api/queries.py) +- [backend/app/api/deps.py](file://backend/app/api/deps.py) +- [backend/app/services/auth.py](file://backend/app/services/auth.py) +- [backend/app/schemas/auth.py](file://backend/app/schemas/auth.py) +- [backend/app/config.py](file://backend/app/config.py) +- [backend/requirements.txt](file://backend/requirements.txt) + + +## 目录 +1. [简介](#简介) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖分析](#依赖分析) +7. [性能考虑](#性能考虑) +8. [故障排查指南](#故障排查指南) +9. [结论](#结论) +10. [附录](#附录) + +## 简介 +本文件面向前端工程师与全栈开发者,系统性阐述本项目的API客户端实现与REST封装设计。内容涵盖: +- 请求配置与响应处理机制 +- 错误处理、重试机制与超时控制 +- 请求/响应拦截器与中间件模式 +- API版本管理、URL构建与参数序列化 +- 认证头自动添加、状态码处理与错误消息格式化 +- 最佳实践、性能优化与调试技巧 + +## 项目结构 +前端通过统一的API模块封装所有后端接口,采用按功能域分层组织: +- 前端API模块:集中定义基础URL、通用请求函数与各业务域方法 +- 认证模块:集成NextAuth,负责登录态与JWT令牌流转 +- 页面组件:在需要鉴权的页面中调用API模块进行数据读写 +- 后端FastAPI:提供/v1版本的REST接口,统一CORS与路由前缀 + +```mermaid +graph TB +subgraph "前端" +UI["页面组件
登录页/查询页"] +API["API模块
api.ts"] +AUTH["认证模块
auth.ts"] +end +subgraph "后端" +MAIN["应用入口
main.py"] +CORS["CORS中间件"] +ROUTER_AUTH["路由: /api/v1/auth/*"] +ROUTER_QUERIES["路由: /api/v1/queries/*"] +ROUTER_CITATIONS["路由: /api/v1/citations/*"] +ROUTER_REPORTS["路由: /api/v1/reports/*"] +end +UI --> API +AUTH --> API +API --> MAIN +MAIN --> CORS +MAIN --> ROUTER_AUTH +MAIN --> ROUTER_QUERIES +MAIN --> ROUTER_CITATIONS +MAIN --> ROUTER_REPORTS +``` + +图表来源 +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) +- [frontend/lib/auth.ts:1-56](file://frontend/lib/auth.ts#L1-L56) +- [backend/app/main.py:24-42](file://backend/app/main.py#L24-L42) + +章节来源 +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) +- [backend/app/main.py:24-42](file://backend/app/main.py#L24-L42) + +## 核心组件 +- 基础URL与请求封装 + - 前端通过NEXT_PUBLIC_API_URL环境变量配置后端基地址,默认本地8000端口 + - 统一的fetchWithAuth函数负责: + - 自动注入Content-Type与Authorization头(若提供token) + - 检查res.ok并解析JSON;非2xx时抛出带错误详情的异常 + - 返回解析后的JSON数据 +- 业务域API对象 + - 提供auth、queries、citations、reports四个命名空间的方法 + - 所有方法均以/v1为版本前缀,遵循REST风格 + - 支持查询参数拼接与JSON体序列化 + +章节来源 +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) + +## 架构总览 +下图展示从前端调用到后端路由的完整链路,包括认证中间件与CORS策略。 + +```mermaid +sequenceDiagram +participant C as "客户端浏览器" +participant F as "前端API模块
api.ts" +participant S as "NextAuth会话
auth.ts" +participant A as "后端应用
main.py" +participant D as "依赖注入
deps.py" +participant H as "认证路由
auth.py" +participant Q as "查询路由
queries.py" +C->>S : "触发登录/获取会话" +S->>F : "调用api.auth.login(...)" +F->>A : "POST /api/v1/auth/login" +A->>D : "OAuth2密码流校验" +D->>H : "调用服务层验证用户" +H-->>A : "返回JWT令牌" +A-->>F : "返回TokenResponse(JSON)" +F-->>S : "返回access_token" +S-->>C : "更新会话状态" +C->>F : "调用api.queries.list(access_token)" +F->>A : "GET /api/v1/queries" +A->>D : "校验JWT并解析当前用户" +D-->>A : "返回当前用户" +A->>Q : "调用查询服务" +Q-->>A : "返回查询列表" +A-->>F : "返回查询列表(JSON)" +F-->>C : "返回数据" +``` + +图表来源 +- [frontend/lib/api.ts:23-57](file://frontend/lib/api.ts#L23-L57) +- [frontend/lib/auth.ts:5-55](file://frontend/lib/auth.ts#L5-L55) +- [backend/app/main.py:38-42](file://backend/app/main.py#L38-L42) +- [backend/app/api/deps.py:16-42](file://backend/app/api/deps.py#L16-L42) +- [backend/app/api/auth.py:22-37](file://backend/app/api/auth.py#L22-L37) +- [backend/app/api/queries.py:15-23](file://backend/app/api/queries.py#L15-L23) + +## 详细组件分析 + +### 前端API模块(请求封装与拦截器) +- 版本与URL构建 + - 基于NEXT_PUBLIC_API_URL拼接/v1路径,确保版本一致性 + - 查询参数通过字符串拼接传入,如citations/list(params) +- 请求拦截器 + - 在fetchWithAuth内统一设置Content-Type与Authorization头 + - 支持外部传入自定义headers并合并 +- 响应拦截器 + - res.ok检查与JSON解析 + - 非2xx时从响应体提取detail或回退为HTTP状态描述,并抛出错误 +- 中间件模式 + - 通过API对象的命名空间体现:auth/queries/citations/reports + - 每个方法内部复用fetchWithAuth,形成“中间件式”的统一处理链 + +```mermaid +flowchart TD +Start(["进入 fetchWithAuth"]) --> MergeHeaders["合并默认与自定义头部"] +MergeHeaders --> AddAuth{"是否提供token?"} +AddAuth --> |是| SetBearer["设置Authorization: Bearer ..."] +AddAuth --> |否| SkipAuth["跳过认证头"] +SetBearer --> Fetch["执行fetch(拼接API_BASE + url)"] +SkipAuth --> Fetch +Fetch --> Ok{"res.ok?"} +Ok --> |否| ParseErr["尝试解析JSON错误体"] +ParseErr --> Throw["抛出错误(detail或HTTP状态)"] +Ok --> |是| ParseJSON["res.json()"] +ParseJSON --> Return["返回解析结果"] +``` + +图表来源 +- [frontend/lib/api.ts:3-21](file://frontend/lib/api.ts#L3-L21) + +章节来源 +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) + +### 认证流程与会话管理 +- 登录页通过NextAuth的credentials提供者发起登录 +- 授权回调中,若返回access_token则写入JWT与会话 +- 后续页面通过useSession获取accessToken并传递给API模块 + +```mermaid +sequenceDiagram +participant L as "登录页
login/page.tsx" +participant N as "NextAuth
auth.ts" +participant A as "后端认证
auth.py" +participant S as "会话存储" +L->>N : "signIn('credentials', {email,password})" +N->>A : "POST /api/v1/auth/login" +A-->>N : "TokenResponse(JSON)" +N->>S : "写入JWT与用户信息" +N-->>L : "登录成功/失败" +``` + +图表来源 +- [frontend/app/(auth)/login/page.tsx:26-42](file://frontend/app/(auth)/login/page.tsx#L26-L42) +- [frontend/lib/auth.ts:5-55](file://frontend/lib/auth.ts#L5-L55) +- [backend/app/api/auth.py:22-37](file://backend/app/api/auth.py#L22-L37) + +章节来源 +- [frontend/app/(auth)/login/page.tsx:1-93](file://frontend/app/(auth)/login/page.tsx#L1-L93) +- [frontend/lib/auth.ts:1-56](file://frontend/lib/auth.ts#L1-L56) +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) + +### 查询管理页面(鉴权与数据加载) +- 仅当session.accessToken存在时才加载数据,避免未授权请求 +- 调用api.queries.list获取列表,错误时显示友好提示 +- 新增/编辑/删除均通过对应API方法完成,最终刷新列表 + +```mermaid +sequenceDiagram +participant P as "查询页
queries/page.tsx" +participant S as "会话
useSession" +participant A as "API模块
api.ts" +participant R as "后端路由
queries.py" +P->>S : "读取accessToken" +S-->>P : "返回token" +P->>A : "api.queries.list(token)" +A->>R : "GET /api/v1/queries" +R-->>A : "返回查询列表(JSON)" +A-->>P : "返回数据" +P-->>P : "渲染表格/错误处理" +``` + +图表来源 +- [frontend/app/(dashboard)/dashboard/queries/page.tsx:96-113](file://frontend/app/(dashboard)/dashboard/queries/page.tsx#L96-L113) +- [frontend/lib/api.ts:37-45](file://frontend/lib/api.ts#L37-L45) +- [backend/app/api/queries.py:15-23](file://backend/app/api/queries.py#L15-L23) + +章节来源 +- [frontend/app/(dashboard)/dashboard/queries/page.tsx:1-461](file://frontend/app/(dashboard)/dashboard/queries/page.tsx#L1-L461) +- [frontend/lib/api.ts:23-57](file://frontend/lib/api.ts#L23-L57) +- [backend/app/api/queries.py:1-86](file://backend/app/api/queries.py#L1-L86) + +### 后端路由与中间件 +- 应用入口统一注册CORS中间件与路由前缀 +- 认证与查询等路由分别定义,使用Depends注入数据库与当前用户 +- 依赖模块OAuth2PasswordBearer配合JWT校验,确保受保护资源访问 + +```mermaid +graph TB +M["应用入口
main.py"] --> C["CORS中间件"] +M --> R1["/api/v1/auth/*"] +M --> R2["/api/v1/queries/*"] +M --> R3["/api/v1/citations/*"] +M --> R4["/api/v1/reports/*"] +R2 --> D["依赖注入
deps.py"] +D --> U["当前用户校验"] +``` + +图表来源 +- [backend/app/main.py:30-42](file://backend/app/main.py#L30-L42) +- [backend/app/api/deps.py:13-42](file://backend/app/api/deps.py#L13-L42) + +章节来源 +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/api/deps.py:1-43](file://backend/app/api/deps.py#L1-L43) + +## 依赖分析 +- 前端依赖 + - Next.js、NextAuth用于SSR/CSR会话管理 + - TypeScript类型约束保证API调用参数安全 +- 后端依赖 + - FastAPI提供路由与中间件能力 + - SQLAlchemy异步ORM、Redis、APScheduler用于数据与任务 + - Pydantic/JWT用于数据校验与令牌签发 + +```mermaid +graph TB +subgraph "前端" +N["Next.js"] +NA["NextAuth"] +TS["TypeScript"] +API["API模块"] +end +subgraph "后端" +FA["FastAPI"] +SA["SQLAlchemy"] +RD["Redis"] +AP["APScheduler"] +PJ["Pydantic/JWT"] +end +API --> FA +NA --> FA +FA --> SA +FA --> RD +FA --> AP +FA --> PJ +``` + +图表来源 +- [frontend/package.json:11-27](file://frontend/package.json#L11-L27) +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) + +章节来源 +- [frontend/package.json:1-40](file://frontend/package.json#L1-L40) +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) + +## 性能考虑 +- 请求合并与去抖 + - 对频繁触发的列表加载,可在组件侧引入防抖/节流,减少不必要的网络请求 +- 缓存策略 + - 对只读列表数据可采用内存缓存或React Query等缓存库,设置合理的TTL与失效策略 +- 并发控制 + - 使用信号量限制并发请求数,避免雪崩效应 +- 传输优化 + - 尽量使用JSON序列化,避免大对象重复传输;必要时启用gzip压缩(由后端CORS与服务器配置决定) +- 会话与令牌 + - 利用JWT短小特性,避免携带冗余信息;结合后端配置合理设置过期时间 + +## 故障排查指南 +- 常见错误与定位 + - 401未授权:检查Authorization头是否正确设置;确认JWT未过期 + - 403禁止访问:核对当前用户权限与资源所有权 + - 404未找到:核对URL路径与UUID参数 + - 500服务器错误:查看后端日志与数据库连接状态 +- 错误处理机制 + - 前端:fetchWithAuth在非2xx时解析响应体中的detail字段,作为用户可见错误信息 + - 后端:路由层使用HTTPException返回标准错误体,包含状态码与详细信息 +- 调试技巧 + - 开启浏览器网络面板,观察请求头与响应体 + - 在NextAuth回调中打印token与用户信息,确认会话状态 + - 在API模块增加日志开关,输出URL、方法与关键参数 + - 使用curl或Postman直接调用后端接口,排除前端逻辑干扰 + +章节来源 +- [frontend/lib/api.ts:16-20](file://frontend/lib/api.ts#L16-L20) +- [backend/app/api/auth.py:25-30](file://backend/app/api/auth.py#L25-L30) +- [backend/app/api/queries.py:49-53](file://backend/app/api/queries.py#L49-L53) + +## 结论 +本项目的API客户端采用简洁而一致的设计:统一的请求封装、明确的版本前缀、基于JWT的认证与严格的错误处理。通过NextAuth与FastAPI的协作,实现了前后端一致的会话与路由体验。建议在生产环境中进一步增强超时控制、重试机制与缓存策略,以提升稳定性与用户体验。 + +## 附录 + +### API版本管理与URL构建 +- 版本前缀:/api/v1 +- 路由前缀:/api/v1/{domain} +- 参数序列化: + - 查询参数:字符串拼接 + - 请求体:JSON.stringify + +章节来源 +- [backend/app/main.py:38-42](file://backend/app/main.py#L38-L42) +- [frontend/lib/api.ts:47-56](file://frontend/lib/api.ts#L47-L56) + +### 认证头自动添加与状态码处理 +- 自动添加:Authorization: Bearer {token} +- 状态码处理:res.ok检查;非2xx抛错 +- 错误消息格式化:优先使用响应体中的detail字段 + +章节来源 +- [frontend/lib/api.ts:8-20](file://frontend/lib/api.ts#L8-L20) +- [backend/app/api/auth.py:22-37](file://backend/app/api/auth.py#L22-L37) + +### 请求拦截器与中间件模式 +- 请求拦截器:在fetchWithAuth内统一设置头部与合并自定义头 +- 响应拦截器:统一解析JSON与错误处理 +- 中间件模式:API对象按领域划分,每个方法复用统一处理链 + +章节来源 +- [frontend/lib/api.ts:3-21](file://frontend/lib/api.ts#L3-L21) +- [frontend/lib/api.ts:23-57](file://frontend/lib/api.ts#L23-L57) + +### 最佳实践与性能优化 +- 会话驱动:仅在accessToken存在时发起受保护请求 +- 错误显式:在UI层展示用户可理解的错误信息 +- 并发控制:限制同时进行的请求数量 +- 缓存策略:对只读数据进行本地缓存与失效管理 + +章节来源 +- [frontend/app/(dashboard)/dashboard/queries/page.tsx:96-113](file://frontend/app/(dashboard)/dashboard/queries/page.tsx#L96-L113) +- [backend/app/config.py:7-10](file://backend/app/config.py#L7-L10) \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/前端系统架构/Next.js应用配置.md b/.qoder/repowiki/zh/content/前端系统架构/Next.js应用配置.md new file mode 100644 index 0000000..ffcca71 --- /dev/null +++ b/.qoder/repowiki/zh/content/前端系统架构/Next.js应用配置.md @@ -0,0 +1,416 @@ +# Next.js应用配置 + + +**本文档引用的文件** +- [package.json](file://frontend/package.json) +- [next.config.mjs](file://frontend/next.config.mjs) +- [tailwind.config.ts](file://frontend/tailwind.config.ts) +- [tsconfig.json](file://frontend/tsconfig.json) +- [postcss.config.mjs](file://frontend/postcss.config.mjs) +- [app/layout.tsx](file://frontend/app/layout.tsx) +- [app/globals.css](file://frontend/app/globals.css) +- [.eslintrc.json](file://frontend/.eslintrc.json) +- [components/providers.tsx](file://frontend/components/providers.tsx) +- [app/(auth)/layout.tsx](file://frontend/app/(auth)/layout.tsx) +- [app/(dashboard)/layout.tsx](file://frontend/app/(dashboard)/layout.tsx) +- [lib/auth.ts](file://frontend/lib/auth.ts) +- [types/next-auth.d.ts](file://frontend/types/next-auth.d.ts) +- [components/ui/button.tsx](file://frontend/components/ui/button.tsx) +- [components/charts/platform-chart.tsx](file://frontend/components/charts/platform-chart.tsx) +- [components/layout/header.tsx](file://frontend/components/layout/header.tsx) +- [components/layout/sidebar.tsx](file://frontend/components/layout/sidebar.tsx) +- [next-env.d.ts](file://frontend/next-env.d.ts) + + +## 目录 +1. [简介](#简介) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构概览](#架构概览) +5. [详细组件分析](#详细组件分析) +6. [依赖关系分析](#依赖关系分析) +7. [性能考虑](#性能考虑) +8. [故障排除指南](#故障排除指南) +9. [结论](#结论) +10. [附录](#附录) + +## 简介 +本文件为基于Next.js 14的应用配置详细文档,涵盖App Router页面组织结构、路由规则、嵌套路由与页面布局设计;全局样式配置、字体系统与主题定制;Tailwind CSS配置选项、自定义样式与响应式设计原则;TypeScript配置、类型定义与开发工具设置;性能优化配置、构建优化与生产环境部署设置;以及配置最佳实践与常见问题解决方案。 + +## 项目结构 +前端代码位于`frontend/`目录,采用Next.js 14 App Router结构,使用TypeScript与Tailwind CSS进行样式管理,并通过Next-Auth实现认证功能。项目采用分层组织:页面路由在`app/`目录下,UI组件在`components/`目录,业务逻辑在`lib/`目录,类型定义在`types/`目录。 + +```mermaid +graph TB +subgraph "前端应用" +A["app/ 根布局
app/layout.tsx"] +B["全局样式
app/globals.css"] +C["认证布局
app/(auth)/layout.tsx"] +D["仪表盘布局
app/(dashboard)/layout.tsx"] +E["UI组件
components/ui/*"] +F["布局组件
components/layout/*"] +G["认证配置
lib/auth.ts"] +H["类型定义
types/next-auth.d.ts"] +I["样式配置
tailwind.config.ts"] +J["构建配置
next.config.mjs"] +K["TypeScript配置
tsconfig.json"] +L["PostCSS配置
postcss.config.mjs"] +M["ESLint配置
.eslintrc.json"] +end +A --> B +A --> C +A --> D +D --> F +D --> G +E --> B +F --> B +I --> B +J --> A +K --> A +L --> I +M --> A +``` + +**图表来源** +- [app/layout.tsx:1-37](file://frontend/app/layout.tsx#L1-L37) +- [app/globals.css:1-64](file://frontend/app/globals.css#L1-L64) +- [app/(auth)/layout.tsx](file://frontend/app/(auth)/layout.tsx#L1-L12) +- [app/(dashboard)/layout.tsx](file://frontend/app/(dashboard)/layout.tsx#L1-L27) +- [tailwind.config.ts:1-57](file://frontend/tailwind.config.ts#L1-L57) +- [next.config.mjs:1-5](file://frontend/next.config.mjs#L1-L5) +- [tsconfig.json:1-27](file://frontend/tsconfig.json#L1-L27) +- [postcss.config.mjs:1-9](file://frontend/postcss.config.mjs#L1-L9) +- [.eslintrc.json:1-4](file://frontend/.eslintrc.json#L1-L4) + +**章节来源** +- [package.json:1-40](file://frontend/package.json#L1-L40) +- [next.config.mjs:1-5](file://frontend/next.config.mjs#L1-L5) +- [tailwind.config.ts:1-57](file://frontend/tailwind.config.ts#L1-L57) +- [tsconfig.json:1-27](file://frontend/tsconfig.json#L1-L27) +- [postcss.config.mjs:1-9](file://frontend/postcss.config.mjs#L1-L9) +- [.eslintrc.json:1-4](file://frontend/.eslintrc.json#L1-L4) + +## 核心组件 +- App Router页面组织:采用`(group)`命名分组实现嵌套路由,如`(auth)`用于认证相关页面,`(dashboard)`用于受保护的仪表盘页面。 +- 全局布局:根布局负责注入字体变量、全局样式与会话提供者。 +- 认证系统:基于Next-Auth的Credentials Provider,支持JWT回调与会话管理。 +- UI组件:基于Radix UI与class-variance-authority的可变性组件库,统一按钮等基础UI元素。 +- 图表组件:基于Recharts的响应式图表组件,支持主题色与交互提示。 + +**章节来源** +- [app/layout.tsx:1-37](file://frontend/app/layout.tsx#L1-L37) +- [components/providers.tsx:1-9](file://frontend/components/providers.tsx#L1-L9) +- [lib/auth.ts:1-56](file://frontend/lib/auth.ts#L1-L56) +- [components/ui/button.tsx:1-57](file://frontend/components/ui/button.tsx#L1-L57) +- [components/charts/platform-chart.tsx:1-68](file://frontend/components/charts/platform-chart.tsx#L1-L68) + +## 架构概览 +应用采用分层架构:UI层(App Router页面与组件)、业务层(认证与API封装)、样式层(Tailwind CSS与CSS变量)。认证流程通过Next-Auth在服务端验证后,客户端使用SessionProvider共享会话状态。 + +```mermaid +graph TB +subgraph "客户端" +A["App Router 页面
app/(dashboard)/*"] +B["布局组件
components/layout/*"] +C["UI 组件
components/ui/*"] +D["会话提供者
components/providers.tsx"] +end +subgraph "认证层" +E["Next-Auth 配置
lib/auth.ts"] +F["类型扩展
types/next-auth.d.ts"] +end +subgraph "样式层" +G["全局样式
app/globals.css"] +H["Tailwind 配置
tailwind.config.ts"] +I["PostCSS 配置
postcss.config.mjs"] +end +subgraph "服务端" +J["Next.js 服务端渲染
next.config.mjs"] +K["TypeScript 编译
tsconfig.json"] +end +A --> B +A --> C +A --> D +D --> E +E --> F +C --> G +G --> H +H --> I +J --> K +``` + +**图表来源** +- [app/(dashboard)/layout.tsx](file://frontend/app/(dashboard)/layout.tsx#L1-L27) +- [components/layout/header.tsx:1-30](file://frontend/components/layout/header.tsx#L1-L30) +- [components/layout/sidebar.tsx:1-54](file://frontend/components/layout/sidebar.tsx#L1-L54) +- [components/providers.tsx:1-9](file://frontend/components/providers.tsx#L1-L9) +- [lib/auth.ts:1-56](file://frontend/lib/auth.ts#L1-L56) +- [types/next-auth.d.ts:1-26](file://frontend/types/next-auth.d.ts#L1-L26) +- [app/globals.css:1-64](file://frontend/app/globals.css#L1-L64) +- [tailwind.config.ts:1-57](file://frontend/tailwind.config.ts#L1-L57) +- [postcss.config.mjs:1-9](file://frontend/postcss.config.mjs#L1-L9) +- [next.config.mjs:1-5](file://frontend/next.config.mjs#L1-L5) +- [tsconfig.json:1-27](file://frontend/tsconfig.json#L1-L27) + +## 详细组件分析 + +### App Router页面组织与路由规则 +- 分组路由:`(auth)`与`(dashboard)`作为路由分组,实现嵌套路由与共享布局。 +- 布局继承:各分组拥有独立布局,根布局负责全局样式与字体注入。 +- 路由守卫:仪表盘布局在服务端检查会话,未登录自动重定向至登录页。 + +```mermaid +sequenceDiagram +participant U as "用户浏览器" +participant R as "路由系统" +participant DL as "仪表盘布局" +participant S as "会话服务" +participant A as "认证配置" +U->>R : 请求 /dashboard +R->>DL : 加载仪表盘布局 +DL->>S : 获取服务器会话 +S-->>DL : 返回会话状态 +alt 无会话 +DL->>U : 重定向到 /login +else 有会话 +DL->>U : 渲染仪表盘页面 +end +``` + +**图表来源** +- [app/(dashboard)/layout.tsx](file://frontend/app/(dashboard)/layout.tsx#L1-L27) +- [lib/auth.ts:1-56](file://frontend/lib/auth.ts#L1-L56) + +**章节来源** +- [app/(auth)/layout.tsx](file://frontend/app/(auth)/layout.tsx#L1-L12) +- [app/(dashboard)/layout.tsx](file://frontend/app/(dashboard)/layout.tsx#L1-L27) + +### 全局样式配置与主题定制 +- CSS变量:通过`:root`与`.dark`类定义主题变量,支持明暗主题切换。 +- Tailwind扩展:在Tailwind配置中扩展颜色与圆角变量,与CSS变量保持一致。 +- 字体系统:使用Next.js本地字体加载器注入变量字体,提升性能与SEO。 +- 基础层与工具层:通过`@layer base`与`@layer utilities`统一基础样式与实用工具类。 + +```mermaid +flowchart TD +Start(["样式初始化"]) --> CSSVars["定义CSS变量
:root 与 .dark"] +CSSVars --> TailwindExt["Tailwind主题扩展
颜色与圆角变量"] +TailwindExt --> Fonts["本地字体变量注入
Geist Sans/Mono"] +Fonts --> BaseLayer["@layer base
统一边框与背景"] +BaseLayer --> UtilsLayer["@layer utilities
文本均衡等工具类"] +UtilsLayer --> End(["完成"]) +``` + +**图表来源** +- [app/globals.css:1-64](file://frontend/app/globals.css#L1-L64) +- [tailwind.config.ts:1-57](file://frontend/tailwind.config.ts#L1-L57) +- [app/layout.tsx:1-37](file://frontend/app/layout.tsx#L1-L37) + +**章节来源** +- [app/globals.css:1-64](file://frontend/app/globals.css#L1-L64) +- [tailwind.config.ts:1-57](file://frontend/tailwind.config.ts#L1-L57) +- [app/layout.tsx:1-37](file://frontend/app/layout.tsx#L1-L37) + +### Tailwind CSS配置与自定义样式 +- 内容扫描:配置内容路径覆盖`pages/`、`components/`与`app/`,确保按需生成样式。 +- 暗色模式:启用类选择器暗色模式,与CSS变量配合实现主题切换。 +- 插件:集成`tailwindcss-animate`插件,提供动画相关的工具类。 +- 自定义圆角:通过CSS变量控制圆角大小,适配不同组件尺寸。 + +```mermaid +classDiagram +class TailwindConfig { ++string darkMode ++string[] content ++Theme theme ++Plugin[] plugins +} +class Theme { ++extend Extend +} +class Extend { ++Record~string,string~ colors ++Record~string,string~ borderRadius +} +TailwindConfig --> Theme +Theme --> Extend +``` + +**图表来源** +- [tailwind.config.ts:1-57](file://frontend/tailwind.config.ts#L1-L57) + +**章节来源** +- [tailwind.config.ts:1-57](file://frontend/tailwind.config.ts#L1-L57) + +### TypeScript配置与类型定义 +- 编译选项:启用严格模式、模块解析为bundler、路径映射`@/*`指向项目根目录。 +- 插件:集成Next.js内置TypeScript插件以支持App Router类型推断。 +- 类型扩展:扩展Next-Auth的Session与JWT类型,确保认证数据的类型安全。 + +```mermaid +classDiagram +class TSConfig { ++string[] lib ++boolean strict ++boolean esModuleInterop ++string module ++string moduleResolution ++Record~string,string[]~ paths ++Plugin[] plugins +} +class NextAuthTypes { ++Session ++User ++JWT +} +TSConfig --> NextAuthTypes : "类型扩展" +``` + +**图表来源** +- [tsconfig.json:1-27](file://frontend/tsconfig.json#L1-L27) +- [types/next-auth.d.ts:1-26](file://frontend/types/next-auth.d.ts#L1-L26) + +**章节来源** +- [tsconfig.json:1-27](file://frontend/tsconfig.json#L1-L27) +- [types/next-auth.d.ts:1-26](file://frontend/types/next-auth.d.ts#L1-L26) +- [next-env.d.ts:1-6](file://frontend/next-env.d.ts#L1-L6) + +### 开发工具与代码质量 +- ESLint:继承Next.js核心Web Vitals与TypeScript规则,保证代码质量与性能指标。 +- PostCSS:仅启用Tailwind CSS处理器,简化构建流程。 +- 包管理:使用npm脚本启动开发服务器、构建与启动生产服务。 + +**章节来源** +- [.eslintrc.json:1-4](file://frontend/.eslintrc.json#L1-L4) +- [postcss.config.mjs:1-9](file://frontend/postcss.config.mjs#L1-L9) +- [package.json:1-40](file://frontend/package.json#L1-L40) + +### 认证与会话管理 +- 提供者:使用Credentials Provider,从后端API获取访问令牌并注入会话。 +- 回调:JWT与Session回调同步accessToken与用户ID,确保客户端可用。 +- 客户端:通过SessionProvider在客户端共享认证状态,Header组件展示用户信息并支持登出。 + +```mermaid +sequenceDiagram +participant C as "客户端" +participant P as "SessionProvider" +participant N as "Next-Auth" +participant B as "后端API" +C->>P : 渲染应用 +P->>N : 初始化会话 +N->>B : 验证访问令牌 +B-->>N : 返回用户信息 +N-->>P : 注入会话数据 +P-->>C : 提供认证上下文 +``` + +**图表来源** +- [components/providers.tsx:1-9](file://frontend/components/providers.tsx#L1-L9) +- [lib/auth.ts:1-56](file://frontend/lib/auth.ts#L1-L56) +- [types/next-auth.d.ts:1-26](file://frontend/types/next-auth.d.ts#L1-L26) + +**章节来源** +- [components/providers.tsx:1-9](file://frontend/components/providers.tsx#L1-L9) +- [lib/auth.ts:1-56](file://frontend/lib/auth.ts#L1-L56) +- [types/next-auth.d.ts:1-26](file://frontend/types/next-auth.d.ts#L1-L26) +- [components/layout/header.tsx:1-30](file://frontend/components/layout/header.tsx#L1-L30) + +### UI组件与响应式设计 +- 可变性组件:Button组件通过class-variance-authority定义变体与尺寸,结合clsx与cn组合类名。 +- 响应式图表:PlatformChart使用Recharts与ResponsiveContainer实现自适应宽度与高度,支持主题色与工具提示。 +- 布局组件:Header与Sidebar分别处理头部信息与导航栏,结合Tailwind工具类实现响应式布局。 + +```mermaid +classDiagram +class Button { ++variant : "default|destructive|outline|secondary|ghost|link" ++size : "default|sm|lg|icon" ++asChild : boolean ++className : string +} +class PlatformChart { ++data : Record~string, Stats~ ++render() : JSX.Element +} +class Header { ++render() : JSX.Element +} +class Sidebar { ++render() : JSX.Element +} +Button --> "使用" cn +PlatformChart --> "使用" Recharts +Header --> Button +Sidebar --> Link +``` + +**图表来源** +- [components/ui/button.tsx:1-57](file://frontend/components/ui/button.tsx#L1-L57) +- [components/charts/platform-chart.tsx:1-68](file://frontend/components/charts/platform-chart.tsx#L1-L68) +- [components/layout/header.tsx:1-30](file://frontend/components/layout/header.tsx#L1-L30) +- [components/layout/sidebar.tsx:1-54](file://frontend/components/layout/sidebar.tsx#L1-L54) + +**章节来源** +- [components/ui/button.tsx:1-57](file://frontend/components/ui/button.tsx#L1-L57) +- [components/charts/platform-chart.tsx:1-68](file://frontend/components/charts/platform-chart.tsx#L1-L68) +- [components/layout/header.tsx:1-30](file://frontend/components/layout/header.tsx#L1-L30) +- [components/layout/sidebar.tsx:1-54](file://frontend/components/layout/sidebar.tsx#L1-L54) + +## 依赖关系分析 +- 运行时依赖:Next.js 14、React 18、Next-Auth、Radix UI组件库、Recharts、Tailwind CSS等。 +- 开发依赖:TypeScript、ESLint、Tailwind CSS、PostCSS等。 +- 构建链路:TypeScript编译 → Next.js构建 → PostCSS处理 → Tailwind CSS生成样式 → 浏览器运行。 + +```mermaid +graph LR +A["package.json 依赖"] --> B["Next.js 运行时"] +A --> C["React 生态"] +A --> D["样式与UI库"] +E["tsconfig.json"] --> F["TypeScript 编译"] +G["postcss.config.mjs"] --> H["PostCSS 处理"] +I["tailwind.config.ts"] --> J["Tailwind CSS"] +F --> B +H --> J +J --> B +``` + +**图表来源** +- [package.json:1-40](file://frontend/package.json#L1-L40) +- [tsconfig.json:1-27](file://frontend/tsconfig.json#L1-L27) +- [postcss.config.mjs:1-9](file://frontend/postcss.config.mjs#L1-L9) +- [tailwind.config.ts:1-57](file://frontend/tailwind.config.ts#L1-L57) + +**章节来源** +- [package.json:1-40](file://frontend/package.json#L1-L40) +- [tsconfig.json:1-27](file://frontend/tsconfig.json#L1-L27) +- [postcss.config.mjs:1-9](file://frontend/postcss.config.mjs#L1-L9) +- [tailwind.config.ts:1-57](file://frontend/tailwind.config.ts#L1-L57) + +## 性能考虑 +- 按需样式:Tailwind内容扫描仅覆盖实际使用的目录,减少未使用样式的生成。 +- 字体优化:使用本地字体变量注入,避免网络字体阻塞,提升首屏渲染性能。 +- 组件懒加载:利用Next.js的路由与组件特性,结合客户端组件按需加载。 +- 构建优化:启用严格模式与增量编译,缩短TypeScript编译时间。 +- 暗色模式:通过CSS变量与类选择器实现,避免运行时样式计算开销。 + +## 故障排除指南 +- 登录后无法进入仪表盘:检查服务端会话获取逻辑与重定向配置,确认认证回调正确注入用户ID与访问令牌。 +- 样式不生效:确认Tailwind内容扫描路径包含当前页面与组件目录,清理构建缓存后重新构建。 +- 字体加载异常:检查本地字体文件路径与变量名,确保字体文件存在于指定位置。 +- TypeScript类型错误:根据扩展的Next-Auth类型定义修正Session与JWT接口,确保与后端返回结构一致。 +- ESLint报错:遵循Next.js核心Web Vitals与TypeScript规则,修复不合规代码或调整规则配置。 + +**章节来源** +- [lib/auth.ts:1-56](file://frontend/lib/auth.ts#L1-L56) +- [tailwind.config.ts:1-57](file://frontend/tailwind.config.ts#L1-L57) +- [app/layout.tsx:1-37](file://frontend/app/layout.tsx#L1-L37) +- [types/next-auth.d.ts:1-26](file://frontend/types/next-auth.d.ts#L1-L26) +- [.eslintrc.json:1-4](file://frontend/.eslintrc.json#L1-L4) + +## 结论 +本项目基于Next.js 14实现了清晰的App Router页面组织、完善的认证体系与现代化的样式架构。通过Tailwind CSS与TypeScript的结合,提供了良好的开发体验与可维护性。建议在生产环境中进一步完善性能监控、缓存策略与安全配置,以获得更佳的用户体验。 + +## 附录 +- 部署建议:使用Next.js默认构建与静态导出能力,结合CDN加速与HTTPS配置。 +- 版本兼容:确保Node.js版本与Next.js 14兼容,定期更新依赖以获得最新安全补丁。 +- 最佳实践:保持组件单一职责、合理使用CSS变量与Tailwind工具类、严格类型约束与代码规范。 \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/前端系统架构/UI组件库.md b/.qoder/repowiki/zh/content/前端系统架构/UI组件库.md new file mode 100644 index 0000000..b5effe8 --- /dev/null +++ b/.qoder/repowiki/zh/content/前端系统架构/UI组件库.md @@ -0,0 +1,547 @@ +# UI组件库 + + +**本文档引用的文件** +- [package.json](file://frontend/package.json) +- [tailwind.config.ts](file://frontend/tailwind.config.ts) +- [lib/utils.ts](file://frontend/lib/utils.ts) +- [components/ui/button.tsx](file://frontend/components/ui/button.tsx) +- [components/ui/dialog.tsx](file://frontend/components/ui/dialog.tsx) +- [components/ui/dropdown-menu.tsx](file://frontend/components/ui/dropdown-menu.tsx) +- [components/ui/input.tsx](file://frontend/components/ui/input.tsx) +- [components/ui/select.tsx](file://frontend/components/ui/select.tsx) +- [components/ui/card.tsx](file://frontend/components/ui/card.tsx) +- [components/ui/badge.tsx](file://frontend/components/ui/badge.tsx) +- [components/ui/table.tsx](file://frontend/components/ui/table.tsx) +- [components/ui/tabs.tsx](file://frontend/components/ui/tabs.tsx) +- [components/ui/label.tsx](file://frontend/components/ui/label.tsx) +- [components/providers.tsx](file://frontend/components/providers.tsx) +- [app/layout.tsx](file://frontend/app/layout.tsx) +- [app/(dashboard)/dashboard/page.tsx](file://frontend/app/(dashboard)/dashboard/page.tsx) +- [app/(dashboard)/dashboard/citations/page.tsx](file://frontend/app/(dashboard)/dashboard/citations/page.tsx) + + +## 目录 +1. [简介](#简介) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构概览](#架构概览) +5. [详细组件分析](#详细组件分析) +6. [依赖关系分析](#依赖关系分析) +7. [性能考虑](#性能考虑) +8. [故障排除指南](#故障排除指南) +9. [结论](#结论) +10. [附录](#附录) + +## 简介 +本UI组件库以Radix UI为核心,结合Tailwind CSS实现一致、可访问且可定制的基础组件。组件遵循以下设计原则: +- 可访问性优先:基于Radix UI的语义化和键盘交互能力 +- 一致性:统一的视觉语言与交互模式 +- 可定制性:通过变体系统与CSS变量实现主题与尺寸扩展 +- 响应式:在移动端与桌面端保持良好体验 +- 组合性:以组合模式构建复杂界面,避免过度封装 + +## 项目结构 +前端采用Next.js应用结构,UI组件集中于components/ui目录,通过cn工具函数统一类名合并,Tailwind配置提供主题变量与动画插件。 + +```mermaid +graph TB +subgraph "前端应用" +LAYOUT["app/layout.tsx
根布局与字体"] +PROVIDERS["components/providers.tsx
会话提供者"] +DASHBOARD["app/(dashboard)/dashboard/page.tsx
仪表盘页面"] +CITATIONS["app/(dashboard)/dashboard/citations/page.tsx
引用记录页面"] +end +subgraph "UI组件库" +BUTTON["components/ui/button.tsx"] +INPUT["components/ui/input.tsx"] +SELECT["components/ui/select.tsx"] +DIALOG["components/ui/dialog.tsx"] +DROPDOWN["components/ui/dropdown-menu.tsx"] +CARD["components/ui/card.tsx"] +TABLE["components/ui/table.tsx"] +TABS["components/ui/tabs.tsx"] +LABEL["components/ui/label.tsx"] +BADGE["components/ui/badge.tsx"] +end +UTILS["lib/utils.ts
类名合并工具"] +LAYOUT --> PROVIDERS +LAYOUT --> DASHBOARD +LAYOUT --> CITATIONS +DASHBOARD --> CARD +DASHBOARD --> TABLE +CITATIONS --> INPUT +CITATIONS --> SELECT +CITATIONS --> TABLE +CITATIONS --> BUTTON +CITATIONS --> LABEL +CITATIONS --> BADGE +BUTTON --> UTILS +INPUT --> UTILS +SELECT --> UTILS +DIALOG --> UTILS +DROPDOWN --> UTILS +CARD --> UTILS +TABLE --> UTILS +TABS --> UTILS +LABEL --> UTILS +BADGE --> UTILS +``` + +**图表来源** +- [app/layout.tsx:1-37](file://frontend/app/layout.tsx#L1-L37) +- [components/providers.tsx:1-9](file://frontend/components/providers.tsx#L1-L9) +- [app/(dashboard)/dashboard/page.tsx](file://frontend/app/(dashboard)/dashboard/page.tsx#L1-L156) +- [app/(dashboard)/dashboard/citations/page.tsx](file://frontend/app/(dashboard)/dashboard/citations/page.tsx#L1-L282) +- [lib/utils.ts:1-7](file://frontend/lib/utils.ts#L1-L7) + +**章节来源** +- [package.json:1-40](file://frontend/package.json#L1-L40) +- [tailwind.config.ts:1-57](file://frontend/tailwind.config.ts#L1-L57) + +## 核心组件 +本节概述所有基础UI组件的功能、属性与使用场景,并说明其可访问性与一致性保障。 + +- 按钮 Button + - 功能:承载点击动作,支持多种外观与尺寸 + - 关键属性:variant(外观)、size(尺寸)、asChild(语义化渲染) + - 可访问性:继承原生button语义,支持聚焦与键盘激活 + - 使用示例路径:[按钮使用示例](file://frontend/app/(dashboard)/dashboard/citations/page.tsx#L187-L192) + +- 输入框 Input + - 功能:文本输入,支持禁用与聚焦态样式 + - 关键属性:type、className等原生属性透传 + - 可访问性:原生语义,配合Label使用提升可访问性 + - 使用示例路径:[输入框使用示例](file://frontend/app/(dashboard)/dashboard/citations/page.tsx#L170-L184) + +- 选择器 Select + - 功能:下拉选择,支持滚动按钮与多级选项 + - 关键属性:触发器、内容区、项、分隔符、滚动按钮 + - 可访问性:基于Radix UI的键盘导航与焦点管理 + - 使用示例路径:[选择器使用示例](file://frontend/app/(dashboard)/dashboard/citations/page.tsx#L138-L166) + +- 对话框 Dialog + - 功能:模态对话,包含覆盖层、内容区、标题与描述 + - 关键属性:Portal、Overlay、Content、Close等 + - 可访问性:自动聚焦到内容区,支持Esc关闭 + - 使用示例路径:[对话框使用示例:1-123](file://frontend/components/ui/dialog.tsx#L1-L123) + +- 下拉菜单 Dropdown Menu + - 功能:上下文菜单,支持子菜单、复选/单选项、快捷键提示 + - 关键属性:Root、Trigger、Content、Item、CheckboxItem、RadioItem、Label、Separator、Shortcut等 + - 可访问性:基于Radix UI的键盘导航与焦点管理 + - 使用示例路径:[下拉菜单使用示例:1-201](file://frontend/components/ui/dropdown-menu.tsx#L1-L201) + +- 卡片 Card + - 功能:容器组件,支持头部、标题、描述、内容与底部 + - 关键属性:通用HTML属性透传 + - 使用示例路径:[卡片使用示例](file://frontend/app/(dashboard)/dashboard/page.tsx#L106-L120) + +- 表格 Table + - 功能:数据表格,支持表头、表体、表尾、行、单元格与标题 + - 关键属性:通用HTML属性透传 + - 使用示例路径:[表格使用示例](file://frontend/app/(dashboard)/dashboard/citations/page.tsx#L217-L274) + +- 标签 Tabs + - 功能:标签页切换,包含列表、触发器与内容区 + - 关键属性:基于Radix UI的状态同步 + - 使用示例路径:[标签页使用示例:1-56](file://frontend/components/ui/tabs.tsx#L1-L56) + +- 标签 Label + - 功能:表单控件标签,与输入控件建立关联 + - 关键属性:基于Radix UI的peer-disabled语义 + - 使用示例路径:[标签使用示例](file://frontend/app/(dashboard)/dashboard/citations/page.tsx#L137-L150) + +- 徽章 Badge + - 功能:状态或分类标记 + - 关键属性:variant(外观) + - 使用示例路径:[徽章使用示例](file://frontend/app/(dashboard)/dashboard/citations/page.tsx#L256-L266) + +**章节来源** +- [components/ui/button.tsx:1-57](file://frontend/components/ui/button.tsx#L1-L57) +- [components/ui/input.tsx:1-23](file://frontend/components/ui/input.tsx#L1-L23) +- [components/ui/select.tsx:1-161](file://frontend/components/ui/select.tsx#L1-L161) +- [components/ui/dialog.tsx:1-123](file://frontend/components/ui/dialog.tsx#L1-L123) +- [components/ui/dropdown-menu.tsx:1-201](file://frontend/components/ui/dropdown-menu.tsx#L1-L201) +- [components/ui/card.tsx:1-80](file://frontend/components/ui/card.tsx#L1-L80) +- [components/ui/table.tsx:1-118](file://frontend/components/ui/table.tsx#L1-L118) +- [components/ui/tabs.tsx:1-56](file://frontend/components/ui/tabs.tsx#L1-L56) +- [components/ui/label.tsx:1-27](file://frontend/components/ui/label.tsx#L1-L27) +- [components/ui/badge.tsx:1-37](file://frontend/components/ui/badge.tsx#L1-L37) + +## 架构概览 +组件库围绕以下架构要素工作: +- Radix UI:提供可访问性与状态管理的底层抽象 +- Tailwind CSS:提供原子化样式与主题变量 +- class-variance-authority:提供变体系统,统一外观与尺寸 +- clsx/tailwind-merge:安全合并类名,避免冲突 +- Next.js App Router:页面级布局与路由组织 + +```mermaid +graph TB +RADIX["@radix-ui/react-*
可访问性与状态"] +CVA["class-variance-authority
变体系统"] +CLX["clsx + tailwind-merge
类名合并"] +TW["Tailwind CSS
主题与动画"] +UTILS["lib/utils.ts
cn工具函数"] +COMPONENTS["components/ui/*
具体组件实现"] +COMPONENTS --> RADIX +COMPONENTS --> CVA +COMPONENTS --> CLX +CLX --> TW +UTILS --> CLX +``` + +**图表来源** +- [package.json:11-27](file://frontend/package.json#L11-L27) +- [lib/utils.ts:1-7](file://frontend/lib/utils.ts#L1-L7) +- [tailwind.config.ts:1-57](file://frontend/tailwind.config.ts#L1-L57) + +## 详细组件分析 + +### 按钮 Button +- 设计要点 + - 使用Slot实现asChild,允许将按钮渲染为链接或其他元素 + - 通过变体系统控制外观与尺寸,保持一致的交互反馈 + - 聚焦态与禁用态通过CSS类明确表达 +- 可访问性 + - 默认button语义,支持键盘激活 + - 聚焦可见性与环形边框确保键盘用户可见 +- 复杂度与性能 + - O(1) 渲染开销,变体计算在编译期完成 +- 使用示例 + - [按钮使用示例](file://frontend/app/(dashboard)/dashboard/citations/page.tsx#L187-L192) + +```mermaid +classDiagram +class Button { ++variant : "default|destructive|outline|secondary|ghost|link" ++size : "default|sm|lg|icon" ++asChild : boolean ++forwardRef +} +class Slot { ++asChild : boolean +} +Button --> Slot : "可选语义化渲染" +``` + +**图表来源** +- [components/ui/button.tsx:36-54](file://frontend/components/ui/button.tsx#L36-L54) + +**章节来源** +- [components/ui/button.tsx:1-57](file://frontend/components/ui/button.tsx#L1-L57) + +### 对话框 Dialog +- 设计要点 + - Portal确保覆盖层与内容在DOM层级上正确分离 + - 基于data-state的动画类实现进入/退出过渡 + - Close按钮包含不可见文本,提升屏幕阅读器可用性 +- 可访问性 + - 自动聚焦到内容区,Esc键关闭 + - Overlay点击可关闭,支持键盘导航 +- 使用示例 + - [对话框使用示例:1-123](file://frontend/components/ui/dialog.tsx#L1-L123) + +```mermaid +sequenceDiagram +participant U as "用户" +participant T as "触发器" +participant P as "Portal" +participant O as "Overlay" +participant C as "Content" +U->>T : 点击/按键激活 +T->>P : 打开对话框 +P->>O : 渲染覆盖层 +P->>C : 渲染内容区 +U->>O : 点击背景 +O->>P : 关闭对话框 +U->>C : Esc键 +C->>P : 关闭对话框 +``` + +**图表来源** +- [components/ui/dialog.tsx:9-54](file://frontend/components/ui/dialog.tsx#L9-L54) + +**章节来源** +- [components/ui/dialog.tsx:1-123](file://frontend/components/ui/dialog.tsx#L1-L123) + +### 下拉菜单 Dropdown Menu +- 设计要点 + - 支持子菜单、复选/单选项、快捷键提示与分隔符 + - 基于Portal与data-state的动画类实现流畅过渡 +- 可访问性 + - 键盘导航:上下左右移动、Enter确认、Esc返回 + - 焦点管理:打开时聚焦首个项,关闭时返回触发器 +- 使用示例 + - [下拉菜单使用示例](file://frontend/app/(dashboard)/dashboard/citations/page.tsx#L138-L166) + +```mermaid +flowchart TD +Start(["打开菜单"]) --> FocusFirst["聚焦首个可选项"] +FocusFirst --> Nav{"键盘方向键"} +Nav --> |上/下| MoveFocus["移动焦点"] +Nav --> |左| SubOpen["打开子菜单"] +Nav --> |右| Select["选择项"] +Nav --> |Enter| Confirm["确认选择"] +Nav --> |Esc| Close["关闭菜单"] +MoveFocus --> Nav +SubOpen --> Nav +Select --> Close +Confirm --> Close +``` + +**图表来源** +- [components/ui/dropdown-menu.tsx:21-75](file://frontend/components/ui/dropdown-menu.tsx#L21-L75) + +**章节来源** +- [components/ui/dropdown-menu.tsx:1-201](file://frontend/components/ui/dropdown-menu.tsx#L1-L201) + +### 选择器 Select +- 设计要点 + - 触发器包含图标与占位符,内容区支持滚动按钮 + - Viewport根据触发器尺寸自适应 +- 可访问性 + - 键盘导航:Tab进入、方向键选择、Enter确认 + - 屏幕阅读器:通过SelectValue与ItemText传达当前值与选项 +- 使用示例 + - [选择器使用示例](file://frontend/app/(dashboard)/dashboard/citations/page.tsx#L138-L166) + +```mermaid +sequenceDiagram +participant U as "用户" +participant T as "触发器" +participant P as "Portal" +participant V as "Viewport" +participant I as "选项项" +U->>T : 点击/按键激活 +T->>P : 打开内容区 +P->>V : 渲染选项列表 +U->>I : 键盘/鼠标选择 +I->>T : 更新值并关闭 +``` + +**图表来源** +- [components/ui/select.tsx:15-100](file://frontend/components/ui/select.tsx#L15-L100) + +**章节来源** +- [components/ui/select.tsx:1-161](file://frontend/components/ui/select.tsx#L1-L161) + +### 表格 Table +- 设计要点 + - 外层容器提供横向滚动,适配窄屏设备 + - 行与单元格支持hover与选中态 +- 可访问性 + - 表格语义清晰,适合屏幕阅读器解析 +- 使用示例 + - [表格使用示例](file://frontend/app/(dashboard)/dashboard/citations/page.tsx#L217-L274) + +```mermaid +flowchart TD +Container["表格容器
overflow-auto"] --> Table["table 元素"] +Table --> Header["thead
边框样式"] +Table --> Body["tbody
悬停与选中态"] +Body --> Row["tr
边框与状态"] +Row --> Cell["td/th
对齐与内边距"] +``` + +**图表来源** +- [components/ui/table.tsx:5-106](file://frontend/components/ui/table.tsx#L5-L106) + +**章节来源** +- [components/ui/table.tsx:1-118](file://frontend/components/ui/table.tsx#L1-L118) + +### 标签 Tabs +- 设计要点 + - 列表容器与触发器基于Radix UI状态同步 + - 激活态提供背景与阴影强调 +- 可访问性 + - 键盘导航:左右移动、Tab切换内容 +- 使用示例 + - [标签页使用示例:1-56](file://frontend/components/ui/tabs.tsx#L1-L56) + +```mermaid +sequenceDiagram +participant U as "用户" +participant L as "TabsList" +participant T as "TabsTrigger" +participant C as "TabsContent" +U->>T : 点击/按键激活 +T->>L : 同步状态 +L->>C : 显示对应内容 +``` + +**图表来源** +- [components/ui/tabs.tsx:8-53](file://frontend/components/ui/tabs.tsx#L8-L53) + +**章节来源** +- [components/ui/tabs.tsx:1-56](file://frontend/components/ui/tabs.tsx#L1-L56) + +### 卡片 Card +- 设计要点 + - 分离头部、标题、描述、内容与底部区域,便于组合 +- 使用示例 + - [卡片使用示例](file://frontend/app/(dashboard)/dashboard/page.tsx#L106-L120) + +```mermaid +classDiagram +class Card { ++HTMLDivElement +} +class CardHeader +class CardTitle +class CardDescription +class CardContent +class CardFooter +Card --> CardHeader +Card --> CardTitle +Card --> CardDescription +Card --> CardContent +Card --> CardFooter +``` + +**图表来源** +- [components/ui/card.tsx:5-77](file://frontend/components/ui/card.tsx#L5-L77) + +**章节来源** +- [components/ui/card.tsx:1-80](file://frontend/components/ui/card.tsx#L1-L80) + +### 标签 Label +- 设计要点 + - 基于peer-disabled语义,与受控输入联动 +- 使用示例 + - [标签使用示例](file://frontend/app/(dashboard)/dashboard/citations/page.tsx#L137-L150) + +```mermaid +flowchart TD +Input["受控输入"] --> Peer["peer 伪类"] +Peer --> Disabled{"禁用状态"} +Disabled --> |是| LabelDisabled["标签禁用样式"] +Disabled --> |否| LabelEnabled["标签启用样式"] +``` + +**图表来源** +- [components/ui/label.tsx:9-23](file://frontend/components/ui/label.tsx#L9-L23) + +**章节来源** +- [components/ui/label.tsx:1-27](file://frontend/components/ui/label.tsx#L1-L27) + +### 徽章 Badge +- 设计要点 + - 通过变体系统提供默认/次要/破坏/描边等外观 +- 使用示例 + - [徽章使用示例](file://frontend/app/(dashboard)/dashboard/citations/page.tsx#L256-L266) + +```mermaid +classDiagram +class Badge { ++variant : "default|secondary|destructive|outline" +} +``` + +**图表来源** +- [components/ui/badge.tsx:6-34](file://frontend/components/ui/badge.tsx#L6-L34) + +**章节来源** +- [components/ui/badge.tsx:1-37](file://frontend/components/ui/badge.tsx#L1-L37) + +## 依赖关系分析 +- 组件依赖Radix UI实现可访问性与状态管理 +- 类名合并依赖clsx与tailwind-merge,确保样式不冲突 +- 主题变量与圆角半径由Tailwind配置提供 +- 页面通过引入UI组件实现功能组合 + +```mermaid +graph LR +P["package.json
依赖声明"] --> R["@radix-ui/react-*"] +P --> CVA["class-variance-authority"] +P --> CLSX["clsx"] +P --> TWM["tailwind-merge"] +P --> TW["tailwindcss"] +BTN["button.tsx"] --> R +BTN --> CVA +BTN --> UTILS["lib/utils.ts"] +SEL["select.tsx"] --> R +SEL --> UTILS +DDL["dropdown-menu.tsx"] --> R +DDL --> UTILS +DLG["dialog.tsx"] --> R +DLG --> UTILS +TBL["table.tsx"] --> UTILS +CARD["card.tsx"] --> UTILS +TABS["tabs.tsx"] --> R +TABS --> UTILS +LABEL["label.tsx"] --> R +LABEL --> CVA +LABEL --> UTILS +BADGE["badge.tsx"] --> CVA +BADGE --> UTILS +``` + +**图表来源** +- [package.json:11-27](file://frontend/package.json#L11-L27) +- [lib/utils.ts:1-7](file://frontend/lib/utils.ts#L1-L7) + +**章节来源** +- [package.json:1-40](file://frontend/package.json#L1-L40) +- [lib/utils.ts:1-7](file://frontend/lib/utils.ts#L1-L7) + +## 性能考虑 +- 组件均采用forwardRef与透传属性,减少额外包装开销 +- 变体系统在编译期确定,运行时仅进行类名拼接 +- Portal仅在需要时渲染,避免不必要的DOM节点 +- 表格容器提供横向滚动,避免布局抖动 +- 建议在大量数据场景下使用虚拟化或分页 + +## 故障排除指南 +- 焦点问题 + - 确保对话框与下拉菜单在打开时自动聚焦到可交互元素 + - 检查Close按钮的不可见文本是否正确设置 +- 动画异常 + - 确认data-state类与动画类匹配 + - 检查Tailwind动画插件是否正确启用 +- 类名冲突 + - 使用cn工具函数合并类名,避免重复覆盖 +- 主题不生效 + - 检查Tailwind配置中的颜色与圆角变量 + - 确认content路径包含组件目录 + +**章节来源** +- [components/ui/dialog.tsx:47-50](file://frontend/components/ui/dialog.tsx#L47-L50) +- [tailwind.config.ts:10-54](file://frontend/tailwind.config.ts#L10-L54) +- [lib/utils.ts:4-6](file://frontend/lib/utils.ts#L4-L6) + +## 结论 +本UI组件库以Radix UI为基础,结合Tailwind CSS与变体系统,提供了高可访问性、一致性强且易于扩展的组件集合。通过清晰的组合模式与严格的样式约定,能够支撑从简单表单到复杂数据面板的各类界面需求。 + +## 附录 + +### 使用规范与最佳实践 +- 组合模式 + - 使用Card组合多个小部件,如统计卡片与图表 + - 使用Table与Badge组合展示数据与状态 +- 状态管理 + - 将外部状态(如查询参数)与组件状态解耦 + - 在页面级组件中集中处理加载、错误与空状态 +- 无障碍支持 + - 为所有交互元素提供键盘可达性 + - 为图标与装饰性元素提供替代文本 +- 主题与响应式 + - 通过CSS变量与Tailwind变体实现主题切换 + - 使用Grid与Flex在不同断点下调整布局 + +### 自定义扩展指南 +- 新增组件 + - 基于现有组件结构,使用forwardRef与透传属性 + - 引入变体系统以支持外观与尺寸扩展 +- 主题定制 + - 在Tailwind配置中扩展colors与borderRadius + - 通过CSS变量统一主题色板 +- 动画与过渡 + - 使用data-state类与Tailwind动画插件 + - 保持过渡时长与缓动曲线一致 + +**章节来源** +- [app/(dashboard)/dashboard/page.tsx](file://frontend/app/(dashboard)/dashboard/page.tsx#L106-L152) +- [app/(dashboard)/dashboard/citations/page.tsx](file://frontend/app/(dashboard)/dashboard/citations/page.tsx#L135-L194) +- [tailwind.config.ts:10-54](file://frontend/tailwind.config.ts#L10-L54) \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/前端系统架构/前端系统架构.md b/.qoder/repowiki/zh/content/前端系统架构/前端系统架构.md new file mode 100644 index 0000000..f715387 --- /dev/null +++ b/.qoder/repowiki/zh/content/前端系统架构/前端系统架构.md @@ -0,0 +1,349 @@ +# 前端系统架构 + + +**本文档引用的文件** +- [package.json](file://frontend/package.json) +- [next.config.mjs](file://frontend/next.config.mjs) +- [tailwind.config.ts](file://frontend/tailwind.config.ts) +- [app/layout.tsx](file://frontend/app/layout.tsx) +- [components/providers.tsx](file://frontend/components/providers.tsx) +- [app/(auth)/layout.tsx](file://frontend/app/(auth)/layout.tsx) +- [app/(dashboard)/layout.tsx](file://frontend/app/(dashboard)/layout.tsx) +- [lib/auth.ts](file://frontend/lib/auth.ts) +- [types/next-auth.d.ts](file://frontend/types/next-auth.d.ts) +- [lib/api.ts](file://frontend/lib/api.ts) +- [app/(auth)/login/page.tsx](file://frontend/app/(auth)/login/page.tsx) +- [app/(dashboard)/dashboard/page.tsx](file://frontend/app/(dashboard)/dashboard/page.tsx) +- [components/ui/button.tsx](file://frontend/components/ui/button.tsx) +- [components/layout/header.tsx](file://frontend/components/layout/header.tsx) +- [app/api/auth/[...nextauth]/route.ts](file://frontend/app/api/auth/[...nextauth]/route.ts) + + +## 目录 +1. [引言](#引言) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖分析](#依赖分析) +7. [性能考虑](#性能考虑) +8. [故障排除指南](#故障排除指南) +9. [结论](#结论) +10. [附录](#附录) + +## 引言 +本文件为 GEO 前端系统的架构文档,聚焦于基于 Next.js 14 的应用架构设计,涵盖 App Router 页面组织、服务器组件与客户端组件的混合使用模式;认证系统(NextAuth.js 集成、会话管理与路由保护);UI 组件库设计理念与复用策略;数据获取与状态管理;错误处理机制;以及响应式设计、可访问性与性能优化等最佳实践。 + +## 项目结构 +前端采用 Next.js 14 App Router 结构,页面按功能域分组(通过路由组 `(auth)` 和 `(dashboard)` 实现),根布局统一注入全局样式与 Provider,认证相关 API 路由集中于 `/api/auth/[...nextauth]`。 + +```mermaid +graph TB +A["app/layout.tsx
根布局与全局样式"] --> B["components/providers.tsx
SessionProvider"] +A --> C["app/(auth)/layout.tsx
认证页容器"] +A --> D["app/(dashboard)/layout.tsx
仪表盘容器"] +D --> E["components/layout/header.tsx
头部与登出"] +C --> F["app/(auth)/login/page.tsx
登录页"] +D --> G["app/(dashboard)/dashboard/page.tsx
仪表盘页"] +B --> H["app/api/auth/[...nextauth]/route.ts
NextAuth 路由处理器"] +A --> I["tailwind.config.ts
Tailwind 配置"] +A --> J["next.config.mjs
Next 配置"] +``` + +**图表来源** +- [app/layout.tsx:1-37](file://frontend/app/layout.tsx#L1-L37) +- [components/providers.tsx:1-9](file://frontend/components/providers.tsx#L1-L9) +- [app/(auth)/layout.tsx](file://frontend/app/(auth)/layout.tsx#L1-L12) +- [app/(dashboard)/layout.tsx](file://frontend/app/(dashboard)/layout.tsx#L1-L27) +- [components/layout/header.tsx:1-30](file://frontend/components/layout/header.tsx#L1-L30) +- [app/(auth)/login/page.tsx](file://frontend/app/(auth)/login/page.tsx#L1-L93) +- [app/(dashboard)/dashboard/page.tsx](file://frontend/app/(dashboard)/dashboard/page.tsx#L1-L156) +- [app/api/auth/[...nextauth]/route.ts](file://frontend/app/api/auth/[...nextauth]/route.ts#L1-L7) +- [tailwind.config.ts:1-57](file://frontend/tailwind.config.ts#L1-L57) +- [next.config.mjs:1-5](file://frontend/next.config.mjs#L1-L5) + +**章节来源** +- [app/layout.tsx:1-37](file://frontend/app/layout.tsx#L1-L37) +- [components/providers.tsx:1-9](file://frontend/components/providers.tsx#L1-L9) +- [app/(auth)/layout.tsx](file://frontend/app/(auth)/layout.tsx#L1-L12) +- [app/(dashboard)/layout.tsx](file://frontend/app/(dashboard)/layout.tsx#L1-L27) +- [tailwind.config.ts:1-57](file://frontend/tailwind.config.ts#L1-L57) +- [next.config.mjs:1-5](file://frontend/next.config.mjs#L1-L5) + +## 核心组件 +- 根布局与全局样式:定义站点元数据、字体变量与全局样式入口,并包裹应用上下文 Provider。 +- 会话提供者:在客户端注入 SessionProvider,使整个应用可访问 NextAuth 会话状态。 +- 认证路由组:提供登录/注册等认证页面的统一容器样式。 +- 仪表盘路由组:提供侧边栏与头部导航,同时在服务器端校验会话,未登录则重定向至登录页。 +- 认证配置:NextAuth 选项,使用凭据提供者对接后端认证接口,JWT 会话策略,回调处理 token 与 session 映射。 +- 类型扩展:为 NextAuth 的 Session 与 JWT 扩展自定义字段,确保类型安全。 +- API 客户端:封装带鉴权头的通用请求方法,统一错误处理与响应解析。 +- UI 组件库:基于 Radix UI 与 Tailwind,使用 class-variance-authority 提供变体与尺寸控制。 +- 头部组件:展示当前用户信息与登出按钮,触发 NextAuth 的 signOut 流程。 + +**章节来源** +- [app/layout.tsx:1-37](file://frontend/app/layout.tsx#L1-L37) +- [components/providers.tsx:1-9](file://frontend/components/providers.tsx#L1-L9) +- [app/(auth)/layout.tsx](file://frontend/app/(auth)/layout.tsx#L1-L12) +- [app/(dashboard)/layout.tsx](file://frontend/app/(dashboard)/layout.tsx#L1-L27) +- [lib/auth.ts:1-56](file://frontend/lib/auth.ts#L1-L56) +- [types/next-auth.d.ts:1-26](file://frontend/types/next-auth.d.ts#L1-L26) +- [lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) +- [components/ui/button.tsx:1-57](file://frontend/components/ui/button.tsx#L1-L57) +- [components/layout/header.tsx:1-30](file://frontend/components/layout/header.tsx#L1-L30) + +## 架构总览 +下图展示了从浏览器到认证服务与业务 API 的整体调用链路,以及客户端组件与服务器组件的职责边界。 + +```mermaid +graph TB +subgraph "浏览器" +U["用户界面
客户端组件"] +end +subgraph "Next.js 应用" +RL["根布局
app/layout.tsx"] +PR["会话提供者
components/providers.tsx"] +AL["认证布局
app/(auth)/layout.tsx"] +DL["仪表盘布局
app/(dashboard)/layout.tsx"] +LG["登录页
app/(auth)/login/page.tsx"] +DB["仪表盘页
app/(dashboard)/dashboard/page.tsx"] +AH["NextAuth 路由
app/api/auth/[...nextauth]/route.ts"] +UI["UI 组件库
components/ui/*"] +LH["头部组件
components/layout/header.tsx"] +end +subgraph "认证服务" +NA["NextAuth 服务
lib/auth.ts"] +end +subgraph "后端 API" +API["业务 API 客户端
lib/api.ts"] +BE["后端服务
backend/app/*"] +end +U --> RL +RL --> PR +PR --> AL +PR --> DL +AL --> LG +DL --> LH +DL --> DB +LG --> AH +AH --> NA +DB --> API +NA --> BE +API --> BE +``` + +**图表来源** +- [app/layout.tsx:1-37](file://frontend/app/layout.tsx#L1-L37) +- [components/providers.tsx:1-9](file://frontend/components/providers.tsx#L1-L9) +- [app/(auth)/layout.tsx](file://frontend/app/(auth)/layout.tsx#L1-L12) +- [app/(dashboard)/layout.tsx](file://frontend/app/(dashboard)/layout.tsx#L1-L27) +- [app/(auth)/login/page.tsx](file://frontend/app/(auth)/login/page.tsx#L1-L93) +- [app/(dashboard)/dashboard/page.tsx](file://frontend/app/(dashboard)/dashboard/page.tsx#L1-L156) +- [components/layout/header.tsx:1-30](file://frontend/components/layout/header.tsx#L1-L30) +- [app/api/auth/[...nextauth]/route.ts](file://frontend/app/api/auth/[...nextauth]/route.ts#L1-L7) +- [lib/auth.ts:1-56](file://frontend/lib/auth.ts#L1-L56) +- [lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) + +## 详细组件分析 + +### 认证系统(NextAuth.js 集成) +- 凭据提供者:使用邮箱/密码进行认证,调用后端登录接口,成功后返回包含用户信息与访问令牌的对象。 +- JWT 会话策略:在回调中将访问令牌与用户 ID 写入 JWT,并在 session 回调中回填到 session 对象。 +- 登录流程:客户端登录页通过 next-auth/react 的 signIn 触发凭据认证,成功后跳转至仪表盘。 +- 路由保护:仪表盘布局在服务器端通过 getServerSession 获取会话,若无会话则重定向至登录页。 +- NextAuth 路由:统一暴露 GET/POST,交由 NextAuth 处理认证生命周期。 + +```mermaid +sequenceDiagram +participant C as "客户端浏览器" +participant LP as "登录页
app/(auth)/login/page.tsx" +participant RP as "NextAuth 路由
app/api/auth/[...nextauth]/route.ts" +participant NA as "NextAuth 配置
lib/auth.ts" +participant BE as "后端服务
backend/app/api/auth.py" +C->>LP : "提交邮箱/密码" +LP->>RP : "signIn('credentials', {email,password})" +RP->>NA : "调用 NextAuth 处理" +NA->>BE : "调用后端登录接口" +BE-->>NA : "返回访问令牌与用户信息" +NA-->>RP : "生成 JWT 与 session" +RP-->>LP : "认证结果" +LP-->>C : "跳转到 /dashboard 或显示错误" +``` + +**图表来源** +- [app/(auth)/login/page.tsx](file://frontend/app/(auth)/login/page.tsx#L1-L93) +- [app/api/auth/[...nextauth]/route.ts](file://frontend/app/api/auth/[...nextauth]/route.ts#L1-L7) +- [lib/auth.ts:1-56](file://frontend/lib/auth.ts#L1-L56) +- [backend/app/api/auth.py](file://backend/app/api/auth.py) + +**章节来源** +- [lib/auth.ts:1-56](file://frontend/lib/auth.ts#L1-L56) +- [types/next-auth.d.ts:1-26](file://frontend/types/next-auth.d.ts#L1-L26) +- [app/api/auth/[...nextauth]/route.ts](file://frontend/app/api/auth/[...nextauth]/route.ts#L1-L7) +- [app/(auth)/login/page.tsx](file://frontend/app/(auth)/login/page.tsx#L1-L93) +- [app/(dashboard)/layout.tsx](file://frontend/app/(dashboard)/layout.tsx#L1-L27) + +### 数据获取与状态管理 +- 会话状态:仪表盘页通过 next-auth/react 的 useSession 获取当前会话,包含访问令牌。 +- API 客户端:封装 fetchWithAuth,自动添加 Authorization 头,统一处理非 2xx 错误并抛出异常。 +- 仪表盘数据:在依赖会话令牌时加载统计数据,包含加载态与错误态处理,失败时提供刷新操作。 +- 查询与引用:API 客户端提供查询列表、创建、更新、删除与引用统计、导出等方法,便于页面按需调用。 + +```mermaid +flowchart TD +Start(["进入仪表盘"]) --> CheckToken["检查会话令牌"] +CheckToken --> HasToken{"存在令牌?"} +HasToken --> |否| EndNoop["不执行数据加载"] +HasToken --> |是| Fetch["调用 API 客户端获取统计数据"] +Fetch --> Ok{"请求成功?"} +Ok --> |是| SetData["设置统计数据并渲染图表"] +Ok --> |否| ShowErr["显示错误并提供刷新按钮"] +SetData --> EndDone(["完成"]) +ShowErr --> EndDone +EndNoop --> EndDone +``` + +**图表来源** +- [app/(dashboard)/dashboard/page.tsx](file://frontend/app/(dashboard)/dashboard/page.tsx#L1-L156) +- [lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) + +**章节来源** +- [app/(dashboard)/dashboard/page.tsx](file://frontend/app/(dashboard)/dashboard/page.tsx#L1-L156) +- [lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) + +### UI 组件库与样式系统 +- 设计理念:以 Radix UI 为基础构建语义化与可访问性友好的基础控件;使用 Tailwind 实现原子化样式与主题变量。 +- 变体与尺寸:通过 class-variance-authority 为组件提供多种变体(如 default、destructive、outline 等)与尺寸(default、sm、lg、icon)。 +- 主题与暗色模式:Tailwind 配置启用基于 class 的深色模式,颜色与圆角通过 CSS 变量统一管理。 +- 复用策略:将通用 UI 抽象为可复用组件(如 Button、Input、Card、Dialog 等),在页面中按需组合使用。 + +```mermaid +classDiagram +class Button { ++variant : "default|destructive|outline|secondary|ghost|link" ++size : "default|sm|lg|icon" ++asChild : boolean ++ref : HTMLButtonElement +} +class UIComponents { ++Card ++Input ++Label ++Select ++Dialog ++DropdownMenu ++Tabs ++Table +} +Button --> UIComponents : "作为基础控件被复用" +``` + +**图表来源** +- [components/ui/button.tsx:1-57](file://frontend/components/ui/button.tsx#L1-L57) +- [tailwind.config.ts:1-57](file://frontend/tailwind.config.ts#L1-L57) + +**章节来源** +- [components/ui/button.tsx:1-57](file://frontend/components/ui/button.tsx#L1-L57) +- [tailwind.config.ts:1-57](file://frontend/tailwind.config.ts#L1-L57) + +### 服务器组件与客户端组件的混合使用 +- 服务器组件:仪表盘布局在服务器端通过 getServerSession 校验会话并进行重定向,避免客户端渲染无意义内容。 +- 客户端组件:登录页、仪表盘页、头部组件均标记为客户端组件,以便使用 hooks(如 useSession、useRouter)与交互逻辑。 +- Provider 注入:根布局注入 Providers,使子树中的客户端组件可共享会话状态。 + +```mermaid +sequenceDiagram +participant S as "服务器组件
app/(dashboard)/layout.tsx" +participant C as "客户端组件
app/(dashboard)/dashboard/page.tsx" +participant P as "Providers
components/providers.tsx" +S->>S : "getServerSession() 校验会话" +S-->>S : "无会话 -> 重定向到 /login" +S-->>P : "渲染根 Provider" +P-->>C : "向客户端组件提供会话状态" +C->>C : "useSession/useEffect 加载数据" +``` + +**图表来源** +- [app/(dashboard)/layout.tsx](file://frontend/app/(dashboard)/layout.tsx#L1-L27) +- [app/(dashboard)/dashboard/page.tsx](file://frontend/app/(dashboard)/dashboard/page.tsx#L1-L156) +- [components/providers.tsx:1-9](file://frontend/components/providers.tsx#L1-L9) + +**章节来源** +- [app/(dashboard)/layout.tsx](file://frontend/app/(dashboard)/layout.tsx#L1-L27) +- [app/(dashboard)/dashboard/page.tsx](file://frontend/app/(dashboard)/dashboard/page.tsx#L1-L156) +- [components/providers.tsx:1-9](file://frontend/components/providers.tsx#L1-L9) + +### 错误处理机制 +- API 层:fetchWithAuth 在非 2xx 时解析错误消息并抛出异常,保证上层统一处理。 +- 页面层:仪表盘页捕获错误并提供“重新加载”操作;登录页在认证失败时显示错误提示。 +- 路由保护:服务器端无会话时直接重定向,避免进入受保护页面。 + +**章节来源** +- [lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) +- [app/(dashboard)/dashboard/page.tsx](file://frontend/app/(dashboard)/dashboard/page.tsx#L1-L156) +- [app/(auth)/login/page.tsx](file://frontend/app/(auth)/login/page.tsx#L1-L93) +- [app/(dashboard)/layout.tsx](file://frontend/app/(dashboard)/layout.tsx#L1-L27) + +## 依赖分析 +- 核心框架:Next.js 14(App Router)、React 18。 +- 认证:NextAuth.js(凭据提供者、JWT 会话、回调映射)。 +- UI:Radix UI(对话框、下拉菜单、标签页、选择器等)、Lucide React 图标。 +- 样式:Tailwind CSS、Tailwind 插件(动画)、class-variance-authority、clsx、tailwind-merge。 +- 图表:Recharts 用于可视化展示。 +- 开发工具:ESLint、TypeScript、PostCSS、Tailwind。 + +```mermaid +graph LR +N["Next.js 14"] --> R["React 18"] +N --> NA["NextAuth.js"] +NA --> AC["凭据提供者"] +UI["Radix UI + Lucide React"] --> TW["Tailwind CSS"] +TW --> CN["class-variance-authority"] +CN --> CL["clsx"] +CL --> TM["tailwind-merge"] +VIZ["Recharts"] --> UI +TS["TypeScript"] --> N +ESL["ESLint"] --> N +PC["PostCSS"] --> TW +``` + +**图表来源** +- [package.json:1-40](file://frontend/package.json#L1-L40) +- [tailwind.config.ts:1-57](file://frontend/tailwind.config.ts#L1-L57) + +**章节来源** +- [package.json:1-40](file://frontend/package.json#L1-L40) +- [tailwind.config.ts:1-57](file://frontend/tailwind.config.ts#L1-L57) + +## 性能考虑 +- App Router 与服务器组件:利用服务器端渲染与路由组隔离,减少不必要的客户端渲染与网络请求。 +- 客户端水合:仅在必要页面标记为客户端组件,避免过度水合。 +- 缓存与重试:可在 API 客户端增加缓存策略与重试逻辑(建议项)。 +- 图表渲染:对大数据集使用虚拟化或采样策略(建议项)。 +- 资源优化:开启图片与静态资源优化(Next.js 默认支持),按需加载第三方库。 +- 构建优化:使用生产构建与代码分割,避免打包体积过大。 + +## 故障排除指南 +- 登录失败:检查凭据是否正确,确认后端认证接口可用;查看登录页错误提示与 NextAuth 回调日志。 +- 会话丢失:确认 Cookie 设置、SameSite 与跨域配置;检查 NextAuth 回调是否正确写入 token。 +- 仪表盘空白:确认服务器端 getServerSession 返回有效会话;检查客户端 useSession 是否拿到访问令牌。 +- API 请求失败:查看 fetchWithAuth 抛出的错误信息,确认后端接口路径与鉴权头是否正确。 +- 样式异常:检查 Tailwind 配置 content 路径与 CSS 变量是否生效;确认暗色模式 class 是否正确切换。 + +**章节来源** +- [app/(auth)/login/page.tsx](file://frontend/app/(auth)/login/page.tsx#L1-L93) +- [app/(dashboard)/layout.tsx](file://frontend/app/(dashboard)/layout.tsx#L1-L27) +- [lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) +- [tailwind.config.ts:1-57](file://frontend/tailwind.config.ts#L1-L57) + +## 结论 +本架构以 Next.js 14 App Router 为核心,结合服务器组件与客户端组件的混合模式,实现了清晰的页面组织与路由保护;通过 NextAuth.js 的凭据提供者与 JWT 会话策略,完成了前后端认证协作;UI 组件库以 Radix UI 与 Tailwind 为基础,具备良好的可维护性与一致性;API 客户端统一处理鉴权与错误,配合页面层的状态与错误处理,形成完整的前端数据流。建议在后续迭代中进一步完善缓存与重试、图表渲染优化与构建体积治理,持续提升用户体验与可维护性。 + +## 附录 +- 最佳实践清单 + - 使用路由组隔离功能域,保持页面组织清晰。 + - 将路由保护放在服务器端,优先保障安全性。 + - 仅在需要时标记客户端组件,减少水合成本。 + - 统一错误处理与用户反馈,提供明确的重试与刷新能力。 + - 严格类型约束,结合 TypeScript 与自定义类型扩展,降低运行时风险。 + - 持续优化构建产物与运行时性能,关注首屏与交互延迟。 \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/前端系统架构/数据可视化.md b/.qoder/repowiki/zh/content/前端系统架构/数据可视化.md new file mode 100644 index 0000000..9a57e7c --- /dev/null +++ b/.qoder/repowiki/zh/content/前端系统架构/数据可视化.md @@ -0,0 +1,323 @@ +# 数据可视化 + + +**本文档引用的文件** +- [frontend/components/charts/trend-chart.tsx](file://frontend/components/charts/trend-chart.tsx) +- [frontend/components/charts/platform-chart.tsx](file://frontend/components/charts/platform-chart.tsx) +- [frontend/lib/platforms.ts](file://frontend/lib/platforms.ts) +- [frontend/lib/api.ts](file://frontend/lib/api.ts) +- [frontend/app/(dashboard)/dashboard/page.tsx](file://frontend/app/(dashboard)/dashboard/page.tsx) +- [frontend/app/(dashboard)/dashboard/citations/page.tsx](file://frontend/app/(dashboard)/dashboard/citations/page.tsx) +- [frontend/app/(dashboard)/dashboard/queries/page.tsx](file://frontend/app/(dashboard)/dashboard/queries/page.tsx) +- [frontend/app/(dashboard)/dashboard/reports/page.tsx](file://frontend/app/(dashboard)/dashboard/reports/page.tsx) +- [frontend/tailwind.config.ts](file://frontend/tailwind.config.ts) +- [frontend/components/providers.tsx](file://frontend/components/providers.tsx) +- [frontend/app/layout.tsx](file://frontend/app/layout.tsx) +- [frontend/package.json](file://frontend/package.json) + + +## 目录 +1. [引言](#引言) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖分析](#依赖分析) +7. [性能考虑](#性能考虑) +8. [故障排查指南](#故障排查指南) +9. [结论](#结论) +10. [附录](#附录) + +## 引言 +本文件系统性阐述基于 Recharts 的数据可视化实现,聚焦两类图表组件:趋势图表与平台对比图表。内容涵盖数据绑定机制、数据格式与配置选项、交互与动画、响应式适配、主题定制与样式、可访问性支持、使用示例、性能优化与调试方法。读者无需深入前端技术背景即可理解图表的设计思路与使用方式。 + +## 项目结构 +前端采用 Next.js 应用程序目录结构,图表组件位于 components/charts 下,分别负责趋势与平台对比展示;统计数据由页面通过 API 获取并传递给图表组件;Tailwind CSS 提供主题变量与样式扩展;Radix UI 组件与 Lucide 图标增强交互体验。 + +```mermaid +graph TB +subgraph "应用层" +Dashboard["仪表板页面
dashboard/page.tsx"] +Citations["引用记录页面
citations/page.tsx"] +Queries["查询词页面
queries/page.tsx"] +Reports["报告导出页面
reports/page.tsx"] +end +subgraph "图表组件" +Trend["趋势图表
trend-chart.tsx"] +Platform["平台对比图表
platform-chart.tsx"] +end +subgraph "数据与工具" +API["API 客户端
lib/api.ts"] +Platforms["平台映射
lib/platforms.ts"] +end +subgraph "样式与主题" +Tailwind["Tailwind 配置
tailwind.config.ts"] +Providers["会话提供者
components/providers.tsx"] +Layout["根布局
app/layout.tsx"] +end +Dashboard --> Trend +Dashboard --> Platform +Trend --> API +Platform --> API +Platform --> Platforms +API --> Layout +Providers --> Layout +Tailwind --> Layout +``` + +**图示来源** +- [frontend/app/(dashboard)/dashboard/page.tsx](file://frontend/app/(dashboard)/dashboard/page.tsx) +- [frontend/components/charts/trend-chart.tsx](file://frontend/components/charts/trend-chart.tsx) +- [frontend/components/charts/platform-chart.tsx](file://frontend/components/charts/platform-chart.tsx) +- [frontend/lib/api.ts](file://frontend/lib/api.ts) +- [frontend/lib/platforms.ts](file://frontend/lib/platforms.ts) +- [frontend/tailwind.config.ts](file://frontend/tailwind.config.ts) +- [frontend/components/providers.tsx](file://frontend/components/providers.tsx) +- [frontend/app/layout.tsx](file://frontend/app/layout.tsx) + +**章节来源** +- [frontend/package.json:25](file://frontend/package.json#L25) +- [frontend/tailwind.config.ts:10-52](file://frontend/tailwind.config.ts#L10-L52) +- [frontend/components/providers.tsx:6-8](file://frontend/components/providers.tsx#L6-L8) +- [frontend/app/layout.tsx:22-36](file://frontend/app/layout.tsx#L22-L36) + +## 核心组件 +- 趋势图表(TrendChart) + - 功能:以折线图展示随时间变化的引用次数趋势。 + - 数据格式:数组对象,包含日期与引用次数字段。 + - 关键特性:网格、坐标轴、提示框、响应式容器、单调曲线、点标记与高亮点。 +- 平台对比图表(PlatformChart) + - 功能:以柱状图展示不同平台的引用率(百分比)对比。 + - 数据格式:对象映射,键为平台标识,值包含查询数、引用数、引用率等统计。 + - 关键特性:网格、坐标轴、提示框、响应式容器、颜色映射、柱状圆角、单元格填充。 + +**章节来源** +- [frontend/components/charts/trend-chart.tsx:13-20](file://frontend/components/charts/trend-chart.tsx#L13-L20) +- [frontend/components/charts/trend-chart.tsx:22-59](file://frontend/components/charts/trend-chart.tsx#L22-L59) +- [frontend/components/charts/platform-chart.tsx:15-23](file://frontend/components/charts/platform-chart.tsx#L15-L23) +- [frontend/components/charts/platform-chart.tsx:34-67](file://frontend/components/charts/platform-chart.tsx#L34-L67) + +## 架构总览 +图表组件通过页面从 API 获取统计数据,再以 props 形式注入图表。图表内部使用 Recharts 组合子组件完成渲染与交互。主题与样式通过 Tailwind CSS 的 CSS 变量与类名实现深浅色模式适配。 + +```mermaid +sequenceDiagram +participant Page as "仪表板页面" +participant API as "API 客户端" +participant Chart as "图表组件" +participant Recharts as "Recharts 组件" +Page->>API : 请求统计数据 +API-->>Page : 返回统计数据 +Page->>Chart : 传入数据 props +Chart->>Recharts : 渲染图表组合子 +Recharts-->>Chart : 呈现图形与交互 +Chart-->>Page : 展示结果 +``` + +**图示来源** +- [frontend/app/(dashboard)/dashboard/page.tsx:128](file://frontend/app/(dashboard)/dashboard/page.tsx#L128) +- [frontend/app/(dashboard)/dashboard/page.tsx:143](file://frontend/app/(dashboard)/dashboard/page.tsx#L143) +- [frontend/lib/api.ts:47-49](file://frontend/lib/api.ts#L47-L49) +- [frontend/components/charts/trend-chart.tsx:22-59](file://frontend/components/charts/trend-chart.tsx#L22-L59) +- [frontend/components/charts/platform-chart.tsx:34-67](file://frontend/components/charts/platform-chart.tsx#L34-L67) + +## 详细组件分析 + +### 趋势图表(TrendChart) +- 数据绑定 + - 接收数据数组,每个元素包含日期与引用次数字段。 + - X 轴使用日期字符串作为数据键,并在渲染时格式化为“月/日”。 + - Y 轴禁止小数,确保引用次数为整数。 +- 交互与提示 + - 提示框使用卡片主题色与边框,标签格式化为“年-月-日”,数值格式化为“引用次数: 数值”。 +- 动画与视觉 + - 折线类型为单调曲线,线条宽度与点标记半径较小,高亮点更大以便聚焦。 + - 网格为虚线,颜色来自语义化类名。 +- 响应式适配 + - 使用响应式容器,宽度 100%,高度固定像素,自动适配父容器尺寸。 + +```mermaid +flowchart TD +Start(["组件接收数据"]) --> Render["渲染 LineChart"] +Render --> Grid["绘制网格"] +Render --> XAxis["配置 X 轴日期"] +Render --> YAxis["配置 Y 轴整数"] +Render --> Tooltip["配置提示框卡片样式"] +Render --> Line["绘制折线单调曲线"] +Line --> End(["完成渲染"]) +``` + +**图示来源** +- [frontend/components/charts/trend-chart.tsx:22-59](file://frontend/components/charts/trend-chart.tsx#L22-L59) + +**章节来源** +- [frontend/components/charts/trend-chart.tsx:13-20](file://frontend/components/charts/trend-chart.tsx#L13-L20) +- [frontend/components/charts/trend-chart.tsx:22-59](file://frontend/components/charts/trend-chart.tsx#L22-L59) + +### 平台对比图表(PlatformChart) +- 数据绑定 + - 输入为平台统计对象映射,组件内部将其转换为包含平台标签与引用率的数组。 + - 平台标签通过平台映射表进行本地化。 +- 交互与提示 + - 提示框格式化为“引用率: 数值%”。 +- 视觉与样式 + - Y 轴范围固定为 0–100,刻度格式化为百分比。 + - 柱状图圆角,颜色按预设调色板循环填充。 +- 响应式适配 + - 同样使用响应式容器,保证在不同屏幕尺寸下保持一致的视觉比例。 + +```mermaid +flowchart TD +Start(["接收平台统计对象"]) --> Map["遍历对象生成数组"] +Map --> Localize["平台标签本地化"] +Localize --> Render["渲染 BarChart"] +Render --> Grid["绘制网格"] +Render --> XAxis["配置 X 轴平台标签"] +Render --> YAxis["配置 Y 轴0-100%"] +Render --> Tooltip["配置提示框百分比"] +Render --> Bars["绘制柱状条带颜色"] +Bars --> End(["完成渲染"]) +``` + +**图示来源** +- [frontend/components/charts/platform-chart.tsx:34-67](file://frontend/components/charts/platform-chart.tsx#L34-L67) +- [frontend/lib/platforms.ts:1-18](file://frontend/lib/platforms.ts#L1-L18) + +**章节来源** +- [frontend/components/charts/platform-chart.tsx:15-23](file://frontend/components/charts/platform-chart.tsx#L15-L23) +- [frontend/components/charts/platform-chart.tsx:34-67](file://frontend/components/charts/platform-chart.tsx#L34-L67) +- [frontend/lib/platforms.ts:1-18](file://frontend/lib/platforms.ts#L1-L18) + +### 数据预处理与格式转换 +- 平台对比图表的数据转换 + - 将输入对象映射转换为数组,计算引用率并四舍五入为整数百分比。 + - 使用平台映射表将平台键转换为中文标签。 +- 日期格式化 + - 趋势图表在 X 轴渲染时将日期字符串格式化为“月/日”,在提示框标签中格式化为“年-月-日”。 + +**章节来源** +- [frontend/components/charts/platform-chart.tsx:35-39](file://frontend/components/charts/platform-chart.tsx#L35-L39) +- [frontend/lib/platforms.ts:1-8](file://frontend/lib/platforms.ts#L1-L8) +- [frontend/components/charts/trend-chart.tsx:30-46](file://frontend/components/charts/trend-chart.tsx#L30-L46) + +### 实时更新机制 +- 页面通过 API 客户端拉取最新统计数据,图表组件接收新数据后自动重新渲染。 +- 仪表板页面在挂载时发起数据请求,图表组件在接收到 props 更新后即时反映。 + +**章节来源** +- [frontend/app/(dashboard)/dashboard/page.tsx:128](file://frontend/app/(dashboard)/dashboard/page.tsx#L128) +- [frontend/app/(dashboard)/dashboard/page.tsx:143](file://frontend/app/(dashboard)/dashboard/page.tsx#L143) +- [frontend/lib/api.ts:47-49](file://frontend/lib/api.ts#L47-L49) + +### 主题定制与样式配置 +- Tailwind CSS 扩展 + - 定义了语义化颜色变量与圆角变量,图表中的网格与提示框颜色直接引用这些变量,实现深浅色模式一致性。 +- 类名与内联样式结合 + - 网格与提示框使用类名与内联样式混合,确保在不同主题下具备合适的对比度与可读性。 +- 响应式容器 + - 图表统一使用响应式容器,保证在移动端与桌面端均能良好显示。 + +**章节来源** +- [frontend/tailwind.config.ts:10-52](file://frontend/tailwind.config.ts#L10-L52) +- [frontend/components/charts/trend-chart.tsx:26-41](file://frontend/components/charts/trend-chart.tsx#L26-L41) +- [frontend/components/charts/platform-chart.tsx:44-56](file://frontend/components/charts/platform-chart.tsx#L44-L56) + +### 可访问性支持 +- 文本标签与格式化 + - 提示框标签与坐标轴刻度均采用人类可读的日期与百分比格式,提升信息可理解性。 +- 颜色与对比度 + - 使用 Tailwind 语义化颜色变量,确保在深浅主题下具备足够对比度。 +- 交互反馈 + - 图表点标记与高亮点提供视觉反馈,便于用户定位数据点。 + +**章节来源** +- [frontend/components/charts/trend-chart.tsx:30-46](file://frontend/components/charts/trend-chart.tsx#L30-L46) +- [frontend/components/charts/platform-chart.tsx:48-57](file://frontend/components/charts/platform-chart.tsx#L48-L57) +- [frontend/tailwind.config.ts:12-46](file://frontend/tailwind.config.ts#L12-L46) + +### 使用示例 +- 在仪表板页面中引入并使用两个图表组件,分别传入趋势数据与平台统计数据。 +- 页面通过 API 客户端获取数据,然后将数据作为 props 传递给图表组件。 + +**章节来源** +- [frontend/app/(dashboard)/dashboard/page.tsx:5](file://frontend/app/(dashboard)/dashboard/page.tsx#L5) +- [frontend/app/(dashboard)/dashboard/page.tsx:128](file://frontend/app/(dashboard)/dashboard/page.tsx#L128) +- [frontend/app/(dashboard)/dashboard/page.tsx:143](file://frontend/app/(dashboard)/dashboard/page.tsx#L143) + +## 依赖分析 +- Recharts 版本 + - 项目使用较新版本的 Recharts,具备良好的性能与丰富的交互能力。 +- 样式与主题 + - Tailwind CSS 提供主题变量,图表组件通过类名与 CSS 变量实现主题一致性。 +- 会话与认证 + - 通过会话提供者与 API 客户端配合,确保图表数据请求携带认证头。 + +```mermaid +graph LR +Package["package.json 依赖"] --> Recharts["recharts"] +Package --> Tailwind["tailwindcss"] +Tailwind --> Charts["图表组件样式"] +Providers["providers.tsx"] --> API["api.ts"] +API --> Charts +``` + +**图示来源** +- [frontend/package.json:25](file://frontend/package.json#L25) +- [frontend/tailwind.config.ts:10-52](file://frontend/tailwind.config.ts#L10-L52) +- [frontend/components/providers.tsx:6-8](file://frontend/components/providers.tsx#L6-L8) +- [frontend/lib/api.ts:1-21](file://frontend/lib/api.ts#L1-L21) + +**章节来源** +- [frontend/package.json:11-27](file://frontend/package.json#L11-L27) +- [frontend/tailwind.config.ts:10-52](file://frontend/tailwind.config.ts#L10-L52) +- [frontend/components/providers.tsx:6-8](file://frontend/components/providers.tsx#L6-L8) +- [frontend/lib/api.ts:1-21](file://frontend/lib/api.ts#L1-L21) + +## 性能考虑 +- 数据规模控制 + - 趋势图表建议限制历史天数,避免过多点导致渲染卡顿。 +- 渲染优化 + - 使用响应式容器与固定高度,减少重排与重绘。 + - 折线与点标记半径较小,降低 SVG 渲染负担。 +- 网络与缓存 + - API 请求应合理设置缓存策略,避免频繁重复请求。 +- 主题切换 + - Tailwind CSS 变量在主题切换时无需重绘图表,提升切换流畅度。 + +[本节为通用指导,不直接分析具体文件] + +## 故障排查指南 +- 图表不显示或空白 + - 检查传入数据是否为空或格式不符;确认页面已正确获取并传递数据。 +- 日期显示异常 + - 确认日期字符串格式与解析逻辑一致;检查提示框标签格式化函数。 +- 百分比显示异常 + - 确认数据转换逻辑是否将引用率乘以 100 并四舍五入为整数。 +- 主题颜色不生效 + - 检查 Tailwind CSS 变量定义与类名引用是否正确。 +- 认证失败导致数据为空 + - 确认会话提供者与 API 客户端的认证头设置。 + +**章节来源** +- [frontend/components/charts/trend-chart.tsx:30-46](file://frontend/components/charts/trend-chart.tsx#L30-L46) +- [frontend/components/charts/platform-chart.tsx:37-39](file://frontend/components/charts/platform-chart.tsx#L37-L39) +- [frontend/tailwind.config.ts:12-46](file://frontend/tailwind.config.ts#L12-L46) +- [frontend/lib/api.ts:12-14](file://frontend/lib/api.ts#L12-L14) + +## 结论 +本项目通过简洁的图表组件与清晰的数据绑定机制,实现了趋势与平台对比两类核心可视化需求。借助 Recharts 的强大能力与 Tailwind CSS 的主题体系,图表在交互、动画、响应式与可访问性方面均达到良好水平。后续可在数据规模控制、缓存策略与主题扩展方面进一步优化。 + +[本节为总结性内容,不直接分析具体文件] + +## 附录 +- API 客户端方法 + - 查询词列表、引用记录列表、引用统计、报告导出等接口。 +- 页面职责 + - 仪表板页面负责聚合趋势与平台图表;引用记录与查询词页面负责数据筛选与管理;报告导出页面负责报表下载。 + +**章节来源** +- [frontend/lib/api.ts:23-57](file://frontend/lib/api.ts#L23-L57) +- [frontend/app/(dashboard)/dashboard/page.tsx:128-143](file://frontend/app/(dashboard)/dashboard/page.tsx#L128-L143) +- [frontend/app/(dashboard)/dashboard/citations/page.tsx:73-94](file://frontend/app/(dashboard)/dashboard/citations/page.tsx#L73-L94) +- [frontend/app/(dashboard)/dashboard/queries/page.tsx:102-113](file://frontend/app/(dashboard)/dashboard/queries/page.tsx#L102-L113) +- [frontend/app/(dashboard)/dashboard/reports/page.tsx:49-93](file://frontend/app/(dashboard)/dashboard/reports/page.tsx#L49-L93) \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/前端系统架构/认证系统前端实现.md b/.qoder/repowiki/zh/content/前端系统架构/认证系统前端实现.md new file mode 100644 index 0000000..705e87b --- /dev/null +++ b/.qoder/repowiki/zh/content/前端系统架构/认证系统前端实现.md @@ -0,0 +1,409 @@ +# 认证系统前端实现 + + +**本文档引用的文件** +- [frontend/app/api/auth/[...nextauth]/route.ts](file://frontend/app/api/auth/[...nextauth]/route.ts) +- [frontend/lib/auth.ts](file://frontend/lib/auth.ts) +- [frontend/types/next-auth.d.ts](file://frontend/types/next-auth.d.ts) +- [frontend/components/providers.tsx](file://frontend/components/providers.tsx) +- [frontend/lib/api.ts](file://frontend/lib/api.ts) +- [frontend/app/(auth)/layout.tsx](file://frontend/app/(auth)/layout.tsx) +- [frontend/app/(auth)/login/page.tsx](file://frontend/app/(auth)/login/page.tsx) +- [frontend/app/(dashboard)/layout.tsx](file://frontend/app/(dashboard)/layout.tsx) +- [frontend/app/layout.tsx](file://frontend/app/layout.tsx) +- [frontend/components/layout/header.tsx](file://frontend/components/layout/header.tsx) +- [frontend/app/(dashboard)/dashboard/page.tsx](file://frontend/app/(dashboard)/dashboard/page.tsx) +- [frontend/package.json](file://frontend/package.json) + + +## 目录 +1. [简介](#简介) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构概览](#架构概览) +5. [详细组件分析](#详细组件分析) +6. [依赖关系分析](#依赖关系分析) +7. [性能考虑](#性能考虑) +8. [故障排除指南](#故障排除指南) +9. [结论](#结论) + +## 简介 + +本文件档详细说明了基于 NextAuth.js 的前端认证系统实现,包括 NextAuth.js 集成配置、会话管理、用户状态同步机制、认证提供者配置、OAuth 流程和 JWT 令牌处理。文档还涵盖了路由保护机制、权限验证、用户状态持久化、API 客户端认证头设置、请求拦截和错误处理,以及登录状态管理、会话过期处理和安全最佳实践。 + +## 项目结构 + +前端认证系统主要分布在以下目录和文件中: + +- NextAuth 路由处理器:`frontend/app/api/auth/[...nextauth]/route.ts` +- NextAuth 配置:`frontend/lib/auth.ts` +- 类型声明扩展:`frontend/types/next-auth.d.ts` +- 全局 Provider 包装:`frontend/components/providers.tsx` +- API 客户端封装:`frontend/lib/api.ts` +- 登录页面:`frontend/app/(auth)/login/page.tsx` +- 仪表盘布局保护:`frontend/app/(dashboard)/layout.tsx` +- 根布局与 Provider 注入:`frontend/app/layout.tsx` +- 头部组件与登出:`frontend/components/layout/header.tsx` +- 仪表盘首页与会话使用:`frontend/app/(dashboard)/dashboard/page.tsx` +- 依赖包配置:`frontend/package.json` + +```mermaid +graph TB +subgraph "前端应用" +A["根布局
app/layout.tsx"] +B["全局Provider
components/providers.tsx"] +C["NextAuth路由
app/api/auth/[...nextauth]/route.ts"] +D["NextAuth配置
lib/auth.ts"] +E["类型声明
types/next-auth.d.ts"] +F["API客户端
lib/api.ts"] +G["登录页面
app/(auth)/login/page.tsx"] +H["仪表盘布局
app/(dashboard)/layout.tsx"] +I["头部组件
components/layout/header.tsx"] +J["仪表盘首页
app/(dashboard)/dashboard/page.tsx"] +end +A --> B +B --> C +C --> D +D --> E +D --> F +G --> D +H --> D +I --> D +J --> F +``` + +**图表来源** +- [frontend/app/layout.tsx:22-36](file://frontend/app/layout.tsx#L22-L36) +- [frontend/components/providers.tsx:6-8](file://frontend/components/providers.tsx#L6-L8) +- [frontend/app/api/auth/[...nextauth]/route.ts:1-7](file://frontend/app/api/auth/[...nextauth]/route.ts#L1-L7) +- [frontend/lib/auth.ts:5-55](file://frontend/lib/auth.ts#L5-L55) +- [frontend/types/next-auth.d.ts:1-26](file://frontend/types/next-auth.d.ts#L1-L26) +- [frontend/lib/api.ts:23-57](file://frontend/lib/api.ts#L23-L57) +- [frontend/app/(auth)/login/page.tsx:19-42](file://frontend/app/(auth)/login/page.tsx#L19-L42) +- [frontend/app/(dashboard)/layout.tsx:7-26](file://frontend/app/(dashboard)/layout.tsx#L7-L26) +- [frontend/components/layout/header.tsx:7-29](file://frontend/components/layout/header.tsx#L7-L29) +- [frontend/app/(dashboard)/dashboard/page.tsx:20-44](file://frontend/app/(dashboard)/dashboard/page.tsx#L20-L44) + +**章节来源** +- [frontend/app/layout.tsx:22-36](file://frontend/app/layout.tsx#L22-L36) +- [frontend/components/providers.tsx:6-8](file://frontend/components/providers.tsx#L6-L8) +- [frontend/app/api/auth/[...nextauth]/route.ts:1-7](file://frontend/app/api/auth/[...nextauth]/route.ts#L1-L7) +- [frontend/lib/auth.ts:5-55](file://frontend/lib/auth.ts#L5-L55) +- [frontend/types/next-auth.d.ts:1-26](file://frontend/types/next-auth.d.ts#L1-L26) +- [frontend/lib/api.ts:23-57](file://frontend/lib/api.ts#L23-L57) +- [frontend/app/(auth)/login/page.tsx:19-42](file://frontend/app/(auth)/login/page.tsx#L19-L42) +- [frontend/app/(dashboard)/layout.tsx:7-26](file://frontend/app/(dashboard)/layout.tsx#L7-L26) +- [frontend/components/layout/header.tsx:7-29](file://frontend/components/layout/header.tsx#L7-L29) +- [frontend/app/(dashboard)/dashboard/page.tsx:20-44](file://frontend/app/(dashboard)/dashboard/page.tsx#L20-L44) + +## 核心组件 + +### NextAuth.js 集成配置 + +NextAuth.js 在本项目中的核心配置位于 `lib/auth.ts`,采用凭据式认证提供者,并通过回调函数将后端返回的访问令牌和用户标识写入 JWT 和会话对象中。配置要点如下: + +- 凭据式提供者:接收邮箱和密码,调用后端 `/api/v1/auth/login` 接口进行认证。 +- 会话策略:使用 JWT 策略,便于在客户端存储和传递访问令牌。 +- 回调函数: + - `jwt`:当用户存在时,将 `accessToken` 和 `id` 写入 JWT。 + - `session`:将 `accessToken` 和 `id` 同步到会话对象,供前端组件使用。 +- 登录页重定向:登录失败时自动跳转至 `/login`。 + +```mermaid +flowchart TD +Start(["开始"]) --> Validate["校验凭据
邮箱/密码"] +Validate --> CallAPI["调用后端登录接口
/api/v1/auth/login"] +CallAPI --> HasToken{"返回包含访问令牌?"} +HasToken --> |是| BuildUser["构建用户对象
包含id/name/email/accessToken"] +HasToken --> |否| ReturnNull["返回空"] +BuildUser --> JWT["写入JWT回调
保存accessToken/id"] +JWT --> Session["写入会话回调
同步accessToken/id"] +Session --> Done(["完成"]) +ReturnNull --> Done +``` + +**图表来源** +- [frontend/lib/auth.ts:13-32](file://frontend/lib/auth.ts#L13-L32) +- [frontend/lib/auth.ts:39-50](file://frontend/lib/auth.ts#L39-L50) + +**章节来源** +- [frontend/lib/auth.ts:5-55](file://frontend/lib/auth.ts#L5-L55) + +### NextAuth 路由处理器 + +NextAuth 路由处理器位于 `app/api/auth/[...nextauth]/route.ts`,负责将 NextAuth 实例导出为 GET 和 POST 处理器,以支持 NextAuth 的所有认证流程。 + +**章节来源** +- [frontend/app/api/auth/[...nextauth]/route.ts:1-7](file://frontend/app/api/auth/[...nextauth]/route.ts#L1-L7) + +### 类型声明扩展 + +为了在 TypeScript 中正确识别自定义的 `accessToken` 和 `id` 字段,需要扩展 NextAuth 的类型声明。类型声明位于 `types/next-auth.d.ts`,确保在使用 `useSession` 时能获得完整的类型提示。 + +**章节来源** +- [frontend/types/next-auth.d.ts:1-26](file://frontend/types/next-auth.d.ts#L1-L26) + +### 全局 Provider 包装 + +根布局通过 `components/providers.tsx` 将 `SessionProvider` 注入到整个应用中,使所有子组件能够使用 `useSession`、`signIn`、`signOut` 等 NextAuth 提供的 Hook 和方法。 + +**章节来源** +- [frontend/app/layout.tsx:22-36](file://frontend/app/layout.tsx#L22-L36) +- [frontend/components/providers.tsx:6-8](file://frontend/components/providers.tsx#L6-L8) + +### API 客户端封装 + +API 客户端位于 `lib/api.ts`,统一处理认证头设置、请求拦截和错误处理。其特点包括: + +- 基础 URL:从环境变量 `NEXT_PUBLIC_API_URL` 获取后端地址。 +- 认证头设置:当传入 token 时,在请求头添加 `Authorization: Bearer ${token}`。 +- 错误处理:对非 2xx 响应抛出错误,错误消息来自后端 JSON 或默认文本。 +- 功能模块: + - 认证:注册、登录、获取当前用户信息。 + - 查询:列表、创建、更新、删除。 + - 引用:列表、统计。 + - 报告:导出 CSV。 + +```mermaid +sequenceDiagram +participant UI as "UI组件" +participant API as "API客户端" +participant BE as "后端服务" +UI->>API : 调用带token的方法 +API->>API : 设置Authorization头 +API->>BE : 发送HTTP请求 +BE-->>API : 返回JSON响应 +API->>API : 检查响应状态 +API-->>UI : 成功返回数据或抛出错误 +``` + +**图表来源** +- [frontend/lib/api.ts:3-21](file://frontend/lib/api.ts#L3-L21) +- [frontend/lib/api.ts:23-57](file://frontend/lib/api.ts#L23-L57) + +**章节来源** +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) + +### 登录页面与认证流程 + +登录页面位于 `app/(auth)/login/page.tsx`,使用 `signIn('credentials', {...})` 触发 NextAuth 的凭据式认证流程。表单提交后,若认证成功则跳转到 `/dashboard` 并刷新页面;若失败则显示错误信息。 + +**章节来源** +- [frontend/app/(auth)/login/page.tsx:19-42](file://frontend/app/(auth)/login/page.tsx#L19-L42) + +### 仪表盘布局保护 + +仪表盘布局 `app/(dashboard)/layout.tsx` 使用 `getServerSession(authOptions)` 在服务端检查会话有效性,未登录则重定向到 `/login`。这确保了前端路由的服务器端保护。 + +**章节来源** +- [frontend/app/(dashboard)/layout.tsx:7-26](file://frontend/app/(dashboard)/layout.tsx#L7-L26) + +### 头部组件与登出 + +头部组件 `components/layout/header.tsx` 使用 `useSession` 获取当前会话信息,并通过 `signOut({ callbackUrl: '/login' })` 实现登出功能,登出后自动跳转到登录页。 + +**章节来源** +- [frontend/components/layout/header.tsx:7-29](file://frontend/components/layout/header.tsx#L7-L29) + +### 仪表盘首页与会话使用 + +仪表盘首页 `app/(dashboard)/dashboard/page.tsx` 使用 `useSession` 获取 `accessToken`,并在依赖变化时调用 API 客户端获取统计数据。该组件展示了如何在客户端读取 NextAuth 会话并将其用于 API 请求。 + +**章节来源** +- [frontend/app/(dashboard)/dashboard/page.tsx:20-44](file://frontend/app/(dashboard)/dashboard/page.tsx#L20-L44) + +## 架构概览 + +整体认证架构由前端 NextAuth.js、后端认证接口和 API 客户端组成。前端通过 NextAuth 管理会话和令牌,API 客户端负责携带令牌与后端交互。 + +```mermaid +graph TB +subgraph "前端" +U["用户界面"] +NA["NextAuth.js
凭据式认证"] +SP["SessionProvider
全局会话注入"] +AC["API客户端
fetchWithAuth"] +end +subgraph "后端" +AUTH["认证接口
/api/v1/auth/*"] +DATA["业务接口
/api/v1/*"] +end +U --> SP +SP --> NA +NA --> AC +AC --> AUTH +AC --> DATA +``` + +**图表来源** +- [frontend/lib/auth.ts:5-55](file://frontend/lib/auth.ts#L5-L55) +- [frontend/lib/api.ts:23-57](file://frontend/lib/api.ts#L23-L57) +- [frontend/app/api/auth/[...nextauth]/route.ts:1-7](file://frontend/app/api/auth/[...nextauth]/route.ts#L1-L7) + +## 详细组件分析 + +### NextAuth.js 配置类图 + +```mermaid +classDiagram +class AuthOptions { ++providers ++session ++callbacks ++pages +} +class CredentialsProvider { ++name ++credentials ++authorize(credentials) +} +class JWTCallbacks { ++jwt({token, user}) +} +class SessionCallbacks { ++session({session, token}) +} +AuthOptions --> CredentialsProvider : "包含" +AuthOptions --> JWTCallbacks : "回调" +AuthOptions --> SessionCallbacks : "回调" +``` + +**图表来源** +- [frontend/lib/auth.ts:5-55](file://frontend/lib/auth.ts#L5-L55) + +**章节来源** +- [frontend/lib/auth.ts:5-55](file://frontend/lib/auth.ts#L5-L55) + +### 登录流程序列图 + +```mermaid +sequenceDiagram +participant 用户 as "用户" +participant 登录页 as "登录页面" +participant NextAuth as "NextAuth" +participant 后端 as "后端认证接口" +用户->>登录页 : 输入邮箱/密码并提交 +登录页->>NextAuth : signIn('credentials', {email,password,redirect : false}) +NextAuth->>后端 : POST /api/v1/auth/login +后端-->>NextAuth : 返回access_token和用户信息 +NextAuth-->>登录页 : 返回认证结果 +登录页->>登录页 : 成功则跳转并刷新页面 +``` + +**图表来源** +- [frontend/app/(auth)/login/page.tsx:26-42](file://frontend/app/(auth)/login/page.tsx#L26-L42) +- [frontend/lib/auth.ts:13-32](file://frontend/lib/auth.ts#L13-L32) +- [frontend/lib/api.ts:30-35](file://frontend/lib/api.ts#L30-L35) + +**章节来源** +- [frontend/app/(auth)/login/page.tsx:19-42](file://frontend/app/(auth)/login/page.tsx#L19-L42) +- [frontend/lib/auth.ts:13-32](file://frontend/lib/auth.ts#L13-L32) +- [frontend/lib/api.ts:23-35](file://frontend/lib/api.ts#L23-L35) + +### 会话管理与状态同步 + +会话管理通过 NextAuth 的回调函数实现 JWT 与会话对象之间的双向同步,确保前端组件可以稳定地读取 `accessToken` 和用户标识。 + +```mermaid +flowchart TD +A["用户登录成功"] --> B["JWT回调
写入accessToken/id"] +B --> C["会话回调
同步accessToken/id"] +C --> D["useSession读取会话"] +D --> E["API客户端携带令牌请求后端"] +``` + +**图表来源** +- [frontend/lib/auth.ts:39-50](file://frontend/lib/auth.ts#L39-L50) +- [frontend/lib/api.ts:12-14](file://frontend/lib/api.ts#L12-L14) + +**章节来源** +- [frontend/lib/auth.ts:39-50](file://frontend/lib/auth.ts#L39-L50) +- [frontend/lib/api.ts:12-14](file://frontend/lib/api.ts#L12-L14) + +### 路由保护机制 + +- 服务器端保护:仪表盘布局在服务端检查会话,未登录直接重定向到登录页。 +- 客户端保护:通过 `useSession` 在客户端读取会话状态,控制页面渲染和导航。 + +**章节来源** +- [frontend/app/(dashboard)/layout.tsx:7-26](file://frontend/app/(dashboard)/layout.tsx#L7-L26) +- [frontend/app/(dashboard)/dashboard/page.tsx:20-44](file://frontend/app/(dashboard)/dashboard/page.tsx#L20-L44) + +### 权限验证与用户状态持久化 + +- 权限验证:通过 `useSession` 获取的 `accessToken` 控制 API 请求权限。 +- 用户状态持久化:NextAuth 的 JWT 策略将用户信息持久化在客户端,避免每次刷新丢失。 + +**章节来源** +- [frontend/lib/auth.ts:35-50](file://frontend/lib/auth.ts#L35-L50) +- [frontend/app/(dashboard)/dashboard/page.tsx:20-44](file://frontend/app/(dashboard)/dashboard/page.tsx#L20-L44) + +### API 客户端认证头设置与错误处理 + +- 认证头设置:在 `fetchWithAuth` 中根据是否传入 token 自动添加 `Authorization` 头。 +- 请求拦截:统一处理 Content-Type 和自定义请求头。 +- 错误处理:对非 2xx 响应抛出错误,错误消息来自后端 JSON 或默认文本。 + +**章节来源** +- [frontend/lib/api.ts:3-21](file://frontend/lib/api.ts#L3-L21) + +## 依赖关系分析 + +前端认证系统的关键依赖关系如下: + +- NextAuth.js 版本:`package.json` 中声明为 `^4.24.14`。 +- 全局 Provider:根布局通过 `components/providers.tsx` 注入 `SessionProvider`。 +- NextAuth 路由:`app/api/auth/[...nextauth]/route.ts` 导出 NextAuth 实例。 +- 类型扩展:`types/next-auth.d.ts` 扩展 NextAuth 类型,确保类型安全。 +- API 客户端:`lib/api.ts` 统一处理认证头和错误处理。 + +```mermaid +graph TB +P["package.json
依赖声明"] --> NA["next-auth@^4.24.14"] +L["lib/auth.ts
NextAuth配置"] --> NA +R["app/api/auth/[...nextauth]/route.ts
路由处理器"] --> L +T["types/next-auth.d.ts
类型扩展"] --> NA +A["lib/api.ts
API客户端"] --> L +S["components/providers.tsx
SessionProvider"] --> NA +``` + +**图表来源** +- [frontend/package.json:21-22](file://frontend/package.json#L21-L22) +- [frontend/lib/auth.ts:1-3](file://frontend/lib/auth.ts#L1-L3) +- [frontend/app/api/auth/[...nextauth]/route.ts:1-2](file://frontend/app/api/auth/[...nextauth]/route.ts#L1-L2) +- [frontend/types/next-auth.d.ts:1](file://frontend/types/next-auth.d.ts#L1) +- [frontend/lib/api.ts:3](file://frontend/lib/api.ts#L3) +- [frontend/components/providers.tsx:3](file://frontend/components/providers.tsx#L3) + +**章节来源** +- [frontend/package.json:11-27](file://frontend/package.json#L11-L27) +- [frontend/lib/auth.ts:1-3](file://frontend/lib/auth.ts#L1-L3) +- [frontend/app/api/auth/[...nextauth]/route.ts:1-2](file://frontend/app/api/auth/[...nextauth]/route.ts#L1-L2) +- [frontend/types/next-auth.d.ts:1](file://frontend/types/next-auth.d.ts#L1) +- [frontend/lib/api.ts:3](file://frontend/lib/api.ts#L3) +- [frontend/components/providers.tsx:3](file://frontend/components/providers.tsx#L3) + +## 性能考虑 + +- 会话策略:使用 JWT 策略减少频繁的服务端会话查询,提升客户端响应速度。 +- 请求复用:API 客户端统一处理认证头和错误,避免重复代码和网络开销。 +- 依赖更新:在 `useEffect` 中仅在 `accessToken` 变化时触发数据加载,避免不必要的请求。 + +[本节为通用性能建议,不涉及具体文件分析] + +## 故障排除指南 + +- 登录失败:检查登录页面的错误提示逻辑,确认 `signIn('credentials')` 返回的错误信息。 +- 会话缺失:确认根布局已注入 `SessionProvider`,且 `useSession` 在受保护组件中正确使用。 +- API 请求失败:检查 `fetchWithAuth` 的错误处理逻辑,确认后端返回的错误消息格式。 +- 服务器端重定向:确认仪表盘布局的 `getServerSession` 检查逻辑,确保未登录时正确重定向。 + +**章节来源** +- [frontend/app/(auth)/login/page.tsx:36-42](file://frontend/app/(auth)/login/page.tsx#L36-L42) +- [frontend/app/layout.tsx:32](file://frontend/app/layout.tsx#L32) +- [frontend/lib/api.ts:16-21](file://frontend/lib/api.ts#L16-L21) +- [frontend/app/(dashboard)/layout.tsx:12-15](file://frontend/app/(dashboard)/layout.tsx#L12-L15) + +## 结论 + +本认证系统通过 NextAuth.js 的凭据式认证、JWT 会话策略和统一的 API 客户端封装,实现了完整的前端认证流程。系统在服务器端和客户端分别提供了路由保护,结合类型扩展确保了开发体验和运行时的类型安全。通过合理的错误处理和会话管理,系统具备良好的可维护性和扩展性。 \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/前端系统架构/页面组件设计.md b/.qoder/repowiki/zh/content/前端系统架构/页面组件设计.md new file mode 100644 index 0000000..b465481 --- /dev/null +++ b/.qoder/repowiki/zh/content/前端系统架构/页面组件设计.md @@ -0,0 +1,411 @@ +# 页面组件设计 + + +**本文档引用的文件** +- [frontend/app/layout.tsx](file://frontend/app/layout.tsx) +- [frontend/app/(dashboard)/layout.tsx](file://frontend/app/(dashboard)/layout.tsx) +- [frontend/components/layout/header.tsx](file://frontend/components/layout/header.tsx) +- [frontend/components/layout/sidebar.tsx](file://frontend/components/layout/sidebar.tsx) +- [frontend/lib/api.ts](file://frontend/lib/api.ts) +- [frontend/app/(dashboard)/dashboard/page.tsx](file://frontend/app/(dashboard)/dashboard/page.tsx) +- [frontend/app/(dashboard)/dashboard/queries/page.tsx](file://frontend/app/(dashboard)/dashboard/queries/page.tsx) +- [frontend/app/(dashboard)/dashboard/citations/page.tsx](file://frontend/app/(dashboard)/dashboard/citations/page.tsx) +- [frontend/app/(dashboard)/dashboard/reports/page.tsx](file://frontend/app/(dashboard)/dashboard/reports/page.tsx) +- [frontend/app/(dashboard)/dashboard/settings/page.tsx](file://frontend/app/(dashboard)/dashboard/settings/page.tsx) +- [frontend/components/ui/table.tsx](file://frontend/components/ui/table.tsx) +- [frontend/components/ui/dialog.tsx](file://frontend/components/ui/dialog.tsx) +- [frontend/lib/platforms.ts](file://frontend/lib/platforms.ts) +- [frontend/lib/utils.ts](file://frontend/lib/utils.ts) +- [frontend/components/charts/trend-chart.tsx](file://frontend/components/charts/trend-chart.tsx) + + +## 目录 +1. [引言](#引言) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构概览](#架构概览) +5. [详细组件分析](#详细组件分析) +6. [依赖关系分析](#依赖关系分析) +7. [性能考虑](#性能考虑) +8. [故障排除指南](#故障排除指南) +9. [结论](#结论) +10. [附录](#附录) + +## 引言 +本文件系统性梳理 GEO 平台前端页面组件的设计与实现,覆盖仪表板、查询管理、引用数据、报告导出与设置页面。内容包括页面布局与导航结构、用户体验流程、页面级数据获取策略、状态管理与错误边界处理、页面间导航逻辑与路由参数传递、页面生命周期管理、性能优化与懒加载策略、SEO 配置以及开发规范与最佳实践。 + +## 项目结构 +前端采用 Next.js App Router 的分组路由模式,将认证相关页面置于 `(auth)` 分组,仪表板相关页面置于 `(dashboard)` 分组。根布局负责全局样式与 Provider 包装;仪表板布局负责权限校验、侧边栏与头部导航的统一渲染。 + +```mermaid +graph TB +root_layout["根布局
frontend/app/layout.tsx"] --> providers["Provider 包装
frontend/components/providers.tsx"] +dashboard_layout["仪表板布局
frontend/app/(dashboard)/layout.tsx"] --> sidebar["侧边栏
frontend/components/layout/sidebar.tsx"] +dashboard_layout --> header["头部导航
frontend/components/layout/header.tsx"] +dashboard_layout --> pages["仪表板页面集合
frontend/app/(dashboard)/dashboard/*"] +pages --> dashboard_page["数据总览
page.tsx"] +pages --> queries_page["查询管理
queries/page.tsx"] +pages --> citations_page["引用记录
citations/page.tsx"] +pages --> reports_page["报告导出
reports/page.tsx"] +pages --> settings_page["设置
settings/page.tsx"] +``` + +**图示来源** +- [frontend/app/layout.tsx:1-37](file://frontend/app/layout.tsx#L1-L37) +- [frontend/app/(dashboard)/layout.tsx:1-27](file://frontend/app/(dashboard)/layout.tsx#L1-L27) +- [frontend/components/layout/sidebar.tsx:1-54](file://frontend/components/layout/sidebar.tsx#L1-L54) +- [frontend/components/layout/header.tsx:1-30](file://frontend/components/layout/header.tsx#L1-L30) + +**章节来源** +- [frontend/app/layout.tsx:1-37](file://frontend/app/layout.tsx#L1-L37) +- [frontend/app/(dashboard)/layout.tsx:1-27](file://frontend/app/(dashboard)/layout.tsx#L1-L27) + +## 核心组件 +- 布局与导航 + - 根布局:定义站点元数据、字体变量与全局样式,注入 Provider。 + - 仪表板布局:服务端会话校验,未登录重定向至登录页;渲染侧边栏与头部,承载主内容区域。 + - 头部组件:展示用户名与登出按钮,使用客户端会话状态。 + - 侧边栏组件:基于路径高亮当前菜单项,提供统一导航入口。 +- UI 组件库 + - 表格组件:封装响应式表格容器与表头/体/行/单元格等基础结构。 + - 对话框组件:基于 Radix UI 实现模态对话框,支持触发器、内容、标题与描述。 +- 工具与常量 + - 平台映射:提供平台键值到中文名称的映射与平台选项列表。 + - 工具函数:类名合并工具,用于组合 Tailwind 类。 + - API 客户端:统一封装鉴权头、错误处理与各模块接口(认证、查询、引用、报告)。 + +**章节来源** +- [frontend/components/layout/header.tsx:1-30](file://frontend/components/layout/header.tsx#L1-L30) +- [frontend/components/layout/sidebar.tsx:1-54](file://frontend/components/layout/sidebar.tsx#L1-L54) +- [frontend/components/ui/table.tsx:1-118](file://frontend/components/ui/table.tsx#L1-L118) +- [frontend/components/ui/dialog.tsx:1-123](file://frontend/components/ui/dialog.tsx#L1-L123) +- [frontend/lib/platforms.ts:1-18](file://frontend/lib/platforms.ts#L1-L18) +- [frontend/lib/utils.ts:1-7](file://frontend/lib/utils.ts#L1-L7) +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) + +## 架构概览 +整体采用“布局层 + 页面层 + 组件层 + 工具层”的分层设计。页面层通过客户端会话获取令牌,调用 API 客户端进行数据拉取与写入;UI 组件层提供可复用的基础控件;工具层提供通用能力(类名合并、平台映射、API 封装)。图表组件独立封装,按需渲染。 + +```mermaid +graph TB +subgraph "布局层" +root_layout["根布局"] +dashboard_layout["仪表板布局"] +header["头部"] +sidebar["侧边栏"] +end +subgraph "页面层" +dashboard_page["数据总览页"] +queries_page["查询管理页"] +citations_page["引用记录页"] +reports_page["报告导出页"] +settings_page["设置页"] +end +subgraph "组件层" +table["表格组件"] +dialog["对话框组件"] +trend_chart["趋势图组件"] +end +subgraph "工具层" +utils["工具函数"] +platforms["平台映射"] +api_client["API 客户端"] +end +root_layout --> dashboard_layout +dashboard_layout --> header +dashboard_layout --> sidebar +dashboard_layout --> dashboard_page +dashboard_layout --> queries_page +dashboard_layout --> citations_page +dashboard_layout --> reports_page +dashboard_layout --> settings_page +dashboard_page --> trend_chart +queries_page --> table +queries_page --> dialog +citations_page --> table +reports_page --> api_client +settings_page --> utils +settings_page --> platforms +api_client --> utils +table --> utils +dialog --> utils +trend_chart --> utils +``` + +**图示来源** +- [frontend/app/layout.tsx:1-37](file://frontend/app/layout.tsx#L1-L37) +- [frontend/app/(dashboard)/layout.tsx:1-27](file://frontend/app/(dashboard)/layout.tsx#L1-L27) +- [frontend/components/layout/header.tsx:1-30](file://frontend/components/layout/header.tsx#L1-L30) +- [frontend/components/layout/sidebar.tsx:1-54](file://frontend/components/layout/sidebar.tsx#L1-L54) +- [frontend/app/(dashboard)/dashboard/page.tsx:1-156](file://frontend/app/(dashboard)/dashboard/page.tsx#L1-L156) +- [frontend/app/(dashboard)/dashboard/queries/page.tsx:1-461](file://frontend/app/(dashboard)/dashboard/queries/page.tsx#L1-L461) +- [frontend/app/(dashboard)/dashboard/citations/page.tsx:1-282](file://frontend/app/(dashboard)/dashboard/citations/page.tsx#L1-L282) +- [frontend/app/(dashboard)/dashboard/reports/page.tsx:1-198](file://frontend/app/(dashboard)/dashboard/reports/page.tsx#L1-L198) +- [frontend/app/(dashboard)/dashboard/settings/page.tsx:1-172](file://frontend/app/(dashboard)/dashboard/settings/page.tsx#L1-L172) +- [frontend/components/ui/table.tsx:1-118](file://frontend/components/ui/table.tsx#L1-L118) +- [frontend/components/ui/dialog.tsx:1-123](file://frontend/components/ui/dialog.tsx#L1-L123) +- [frontend/components/charts/trend-chart.tsx:1-60](file://frontend/components/charts/trend-chart.tsx#L1-L60) +- [frontend/lib/utils.ts:1-7](file://frontend/lib/utils.ts#L1-L7) +- [frontend/lib/platforms.ts:1-18](file://frontend/lib/platforms.ts#L1-L18) +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) + +## 详细组件分析 + +### 仪表板页面(数据总览) +- 页面职责 + - 展示用户关键指标卡片(总查询次数、总引用次数、引用率、平均引用位置)。 + - 展示过去30天引用趋势折线图与平台引用率对比柱状图。 +- 数据流 + - 客户端从会话中获取访问令牌,调用引用统计接口获取聚合数据。 + - 加载状态、错误状态与空数据提示分别处理。 +- 用户体验 + - 卡片式布局,响应式网格适配不同屏幕尺寸。 + - 图表组件使用响应式容器,确保在小屏设备上正常显示。 +- 错误边界 + - 请求失败时显示错误提示与刷新按钮;加载中显示旋转指示器。 + +```mermaid +sequenceDiagram +participant U as "用户" +participant P as "仪表板页面" +participant S as "会话状态" +participant A as "API 客户端" +participant B as "后端" +U->>P : 进入页面 +P->>S : 读取 accessToken +alt 有令牌 +P->>A : 调用引用统计接口 +A->>B : 发起带授权头的请求 +B-->>A : 返回统计数据 +A-->>P : 解析并返回数据 +P-->>U : 渲染指标卡片与图表 +else 无令牌 +P-->>U : 显示加载占位 +end +``` + +**图示来源** +- [frontend/app/(dashboard)/dashboard/page.tsx:20-44](file://frontend/app/(dashboard)/dashboard/page.tsx#L20-L44) +- [frontend/lib/api.ts:46-49](file://frontend/lib/api.ts#L46-L49) + +**章节来源** +- [frontend/app/(dashboard)/dashboard/page.tsx:1-156](file://frontend/app/(dashboard)/dashboard/page.tsx#L1-L156) +- [frontend/components/charts/trend-chart.tsx:1-60](file://frontend/components/charts/trend-chart.tsx#L1-L60) + +### 查询管理页面 +- 页面职责 + - 列表展示查询词,支持新增、编辑、删除与“立即查询”操作。 + - 提供平台多选、频率选择、品牌别名输入等配置项。 +- 数据流 + - 客户端加载查询词列表;新增/编辑通过 PUT/POST 写入;删除通过 DELETE。 + - 表单校验:关键词、目标品牌必填,至少选择一个平台。 +- 用户体验 + - 使用对话框承载表单,支持取消与保存;删除操作二次确认。 + - 列表支持平台徽标展示与状态徽章。 +- 错误边界 + - 表单校验失败与网络错误分别提示;保存与删除过程禁用按钮并显示加载状态。 + +```mermaid +flowchart TD +Start(["进入查询管理页"]) --> Load["加载查询词列表"] +Load --> View{"列表为空?"} +View --> |是| Empty["显示空状态与添加按钮"] +View --> |否| List["渲染查询词表格"] +AddEdit["打开新增/编辑对话框"] --> Form["填写表单关键词/品牌/平台/频率"] +Form --> Validate{"校验通过?"} +Validate --> |否| ShowError["显示错误提示"] +Validate --> |是| Save["调用保存接口"] +Save --> Refresh["刷新列表并关闭对话框"] +Delete["打开删除确认"] --> Confirm{"确认删除?"} +Confirm --> |否| Cancel["取消"] +Confirm --> |是| Remove["调用删除接口"] --> Refresh +Empty --> AddEdit +``` + +**图示来源** +- [frontend/app/(dashboard)/dashboard/queries/page.tsx:79-170](file://frontend/app/(dashboard)/dashboard/queries/page.tsx#L79-L170) +- [frontend/lib/api.ts:37-45](file://frontend/lib/api.ts#L37-L45) + +**章节来源** +- [frontend/app/(dashboard)/dashboard/queries/page.tsx:1-461](file://frontend/app/(dashboard)/dashboard/queries/page.tsx#L1-L461) +- [frontend/components/ui/dialog.tsx:1-123](file://frontend/components/ui/dialog.tsx#L1-L123) +- [frontend/lib/platforms.ts:1-18](file://frontend/lib/platforms.ts#L1-L18) + +### 引用记录页面 +- 页面职责 + - 展示跨平台的引用检测结果,支持按查询词、平台、日期范围筛选。 +- 数据流 + - 客户端加载查询词下拉选项与引用记录列表;筛选条件通过查询字符串拼接。 + - 支持重置筛选条件并重新加载。 +- 用户体验 + - 筛选区采用栅格布局,适配移动端;表格支持横向滚动。 + - 引用状态以图标与颜色区分,引用位置与文本片段清晰展示。 +- 错误边界 + - 加载失败时显示错误提示;空列表时提供引导文案。 + +```mermaid +sequenceDiagram +participant U as "用户" +participant P as "引用记录页" +participant A as "API 客户端" +participant B as "后端" +U->>P : 打开页面 +P->>A : 获取查询词列表 +A->>B : 发起请求 +B-->>A : 返回查询词数组 +A-->>P : 设置查询词选项 +U->>P : 设置筛选条件 +P->>A : 拼接查询参数并请求引用记录 +A->>B : 发起带参数的请求 +B-->>A : 返回引用记录数组 +A-->>P : 渲染表格 +``` + +**图示来源** +- [frontend/app/(dashboard)/dashboard/citations/page.tsx:45-98](file://frontend/app/(dashboard)/dashboard/citations/page.tsx#L45-L98) +- [frontend/lib/api.ts:46-49](file://frontend/lib/api.ts#L46-L49) + +**章节来源** +- [frontend/app/(dashboard)/dashboard/citations/page.tsx:1-282](file://frontend/app/(dashboard)/dashboard/citations/page.tsx#L1-L282) +- [frontend/components/ui/table.tsx:1-118](file://frontend/components/ui/table.tsx#L1-L118) + +### 报告导出页面 +- 页面职责 + - 选择查询词后导出 CSV 报告,文件包含字段:关键词、平台、是否引用、引用位置、引用文本、竞争品牌、查询时间。 +- 数据流 + - 客户端加载查询词列表;点击导出时使用原生 fetch 下载文件并触发浏览器下载。 + - 成功后显示成功提示,失败时显示错误信息。 +- 用户体验 + - 导出设置卡片与使用说明卡片并列展示,信息清晰。 + - 自动命名文件,便于识别与归档。 +- 错误边界 + - 未选择查询词时阻止导出并提示;网络异常或后端错误统一捕获并展示。 + +```mermaid +sequenceDiagram +participant U as "用户" +participant P as "报告导出页" +participant A as "API 客户端" +participant B as "后端" +U->>P : 选择查询词并点击导出 +alt 未选择查询词 +P-->>U : 显示错误提示 +else 已选择 +P->>A : 调用导出接口携带令牌 +A->>B : 发起带授权头的请求 +B-->>A : 返回二进制文件 +A-->>P : 触发浏览器下载 +P-->>U : 显示成功提示 +end +``` + +**图示来源** +- [frontend/app/(dashboard)/dashboard/reports/page.tsx:25-93](file://frontend/app/(dashboard)/dashboard/reports/page.tsx#L25-L93) +- [frontend/lib/api.ts:51-56](file://frontend/lib/api.ts#L51-L56) + +**章节来源** +- [frontend/app/(dashboard)/dashboard/reports/page.tsx:1-198](file://frontend/app/(dashboard)/dashboard/reports/page.tsx#L1-L198) + +### 设置页面 +- 页面职责 + - 展示用户信息与当前套餐状态;对比不同套餐的功能差异。 +- 数据流 + - 读取会话中的用户信息;根据当前计划键渲染对应套餐卡片。 +- 用户体验 + - 套餐卡片突出当前版本(MVP 阶段免费开放),提供功能清单与价格信息。 + - 提供 MVP 阶段说明与后续升级提示。 +- 错误边界 + - 无网络或会话异常时,显示默认占位信息。 + +**章节来源** +- [frontend/app/(dashboard)/dashboard/settings/page.tsx:1-172](file://frontend/app/(dashboard)/dashboard/settings/page.tsx#L1-L172) + +## 依赖关系分析 +- 组件耦合 + - 页面组件依赖 UI 组件库与工具函数,保持低耦合与高内聚。 + - 仪表板与查询管理页面依赖 API 客户端;引用记录与报告导出页面同样依赖 API 客户端。 +- 外部依赖 + - 图表组件依赖 Recharts;对话框组件依赖 Radix UI;平台映射来自独立模块。 +- 潜在循环依赖 + - 未发现直接循环导入;布局与页面通过路由分组避免循环引用。 + +```mermaid +graph LR +api["API 客户端"] --> dashboard_page["仪表板页面"] +api --> queries_page["查询管理页面"] +api --> citations_page["引用记录页面"] +api --> reports_page["报告导出页面"] +table["表格组件"] --> queries_page +table --> citations_page +dialog["对话框组件"] --> queries_page +utils["工具函数"] --> api +utils --> table +utils --> dialog +platforms["平台映射"] --> queries_page +platforms --> citations_page +trend_chart["趋势图组件"] --> dashboard_page +``` + +**图示来源** +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) +- [frontend/app/(dashboard)/dashboard/page.tsx:1-156](file://frontend/app/(dashboard)/dashboard/page.tsx#L1-L156) +- [frontend/app/(dashboard)/dashboard/queries/page.tsx:1-461](file://frontend/app/(dashboard)/dashboard/queries/page.tsx#L1-L461) +- [frontend/app/(dashboard)/dashboard/citations/page.tsx:1-282](file://frontend/app/(dashboard)/dashboard/citations/page.tsx#L1-L282) +- [frontend/app/(dashboard)/dashboard/reports/page.tsx:1-198](file://frontend/app/(dashboard)/dashboard/reports/page.tsx#L1-L198) +- [frontend/components/ui/table.tsx:1-118](file://frontend/components/ui/table.tsx#L1-L118) +- [frontend/components/ui/dialog.tsx:1-123](file://frontend/components/ui/dialog.tsx#L1-L123) +- [frontend/lib/utils.ts:1-7](file://frontend/lib/utils.ts#L1-L7) +- [frontend/lib/platforms.ts:1-18](file://frontend/lib/platforms.ts#L1-L18) +- [frontend/components/charts/trend-chart.tsx:1-60](file://frontend/components/charts/trend-chart.tsx#L1-L60) + +**章节来源** +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) +- [frontend/lib/utils.ts:1-7](file://frontend/lib/utils.ts#L1-L7) +- [frontend/lib/platforms.ts:1-18](file://frontend/lib/platforms.ts#L1-L18) + +## 性能考虑 +- 懒加载策略 + - 图表组件按需引入,避免首屏额外负担;页面组件在客户端首次渲染时才初始化。 +- 数据获取策略 + - 页面在会话令牌可用时再发起请求,减少无效网络调用;查询管理与引用记录页面在筛选条件变化时才重新请求。 +- 状态管理 + - 使用 React 本地状态管理加载、错误与交互状态,避免引入重型状态库。 +- UI 渲染优化 + - 表格组件提供横向滚动容器,避免大表格导致布局抖动;卡片与网格布局自适应不同屏幕尺寸。 +- SEO 配置 + - 根布局提供站点元数据(标题与描述),可在后续扩展 Open Graph、结构化数据等。 + +**章节来源** +- [frontend/app/layout.tsx:17-20](file://frontend/app/layout.tsx#L17-L20) +- [frontend/app/(dashboard)/dashboard/queries/page.tsx:96-113](file://frontend/app/(dashboard)/dashboard/queries/page.tsx#L96-L113) +- [frontend/app/(dashboard)/dashboard/citations/page.tsx:73-94](file://frontend/app/(dashboard)/dashboard/citations/page.tsx#L73-L94) + +## 故障排除指南 +- 登录态缺失 + - 仪表板布局在服务端检查会话,未登录将重定向至登录页;客户端组件应始终检查会话令牌后再发起请求。 +- 请求失败 + - API 客户端对非 2xx 响应抛出错误,页面层捕获并显示错误提示;建议增加重试机制与更详细的错误码解析。 +- 表单校验失败 + - 查询管理页面对必填字段与平台选择进行校验,失败时显示错误提示;建议在提交前进行前端即时校验并聚焦首个错误项。 +- 导出失败 + - 报告导出页面对未选择查询词进行拦截;网络异常或后端错误统一捕获;建议在下载前预检接口可用性。 + +**章节来源** +- [frontend/app/(dashboard)/layout.tsx:12-15](file://frontend/app/(dashboard)/layout.tsx#L12-L15) +- [frontend/lib/api.ts:3-21](file://frontend/lib/api.ts#L3-L21) +- [frontend/app/(dashboard)/dashboard/queries/page.tsx:133-142](file://frontend/app/(dashboard)/dashboard/queries/page.tsx#L133-L142) +- [frontend/app/(dashboard)/dashboard/reports/page.tsx:49-93](file://frontend/app/(dashboard)/dashboard/reports/page.tsx#L49-L93) + +## 结论 +本设计以清晰的分层与职责划分实现了仪表板、查询管理、引用记录、报告导出与设置页面的组件化构建。通过统一的 API 客户端与 UI 组件库,提升了可维护性与一致性;结合会话驱动的数据获取与完善的错误边界处理,保障了用户体验与稳定性。后续可在 SEO、国际化、缓存策略与状态持久化方面进一步完善。 + +## 附录 +- 开发规范与最佳实践 + - 组件命名:使用语义化文件名与导出名称,如 `queries/page.tsx`。 + - 状态管理:优先使用 React 本地状态;复杂场景再考虑集中式状态库。 + - 错误处理:统一在页面层捕获并展示错误,避免错误冒泡到全局。 + - 表单处理:在提交前进行前端校验,必要时提供实时校验反馈。 + - 导航与路由:利用 Next.js 路由与分组,保持路径与页面职责一致。 + - 性能:按需引入重型组件(如图表),避免首屏阻塞;合理使用加载状态与骨架屏。 + - 可访问性:为按钮、对话框、表格等提供适当的 ARIA 属性与键盘导航支持。 + - 样式:统一使用工具函数合并类名,遵循 Tailwind 命名约定。 \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/后端系统架构/API接口设计.md b/.qoder/repowiki/zh/content/后端系统架构/API接口设计.md new file mode 100644 index 0000000..bc4853b --- /dev/null +++ b/.qoder/repowiki/zh/content/后端系统架构/API接口设计.md @@ -0,0 +1,455 @@ +# API接口设计 + + +**本文档引用的文件** +- [backend/app/main.py](file://backend/app/main.py) +- [backend/app/api/auth.py](file://backend/app/api/auth.py) +- [backend/app/api/queries.py](file://backend/app/api/queries.py) +- [backend/app/api/citations.py](file://backend/app/api/citations.py) +- [backend/app/api/reports.py](file://backend/app/api/reports.py) +- [backend/app/api/deps.py](file://backend/app/api/deps.py) +- [backend/app/schemas/auth.py](file://backend/app/schemas/auth.py) +- [backend/app/schemas/query.py](file://backend/app/schemas/query.py) +- [backend/app/schemas/citation.py](file://backend/app/schemas/citation.py) +- [backend/app/services/auth.py](file://backend/app/services/auth.py) +- [backend/app/services/query.py](file://backend/app/services/query.py) +- [backend/app/services/citation.py](file://backend/app/services/citation.py) +- [backend/app/database.py](file://backend/app/database.py) +- [backend/app/config.py](file://backend/app/config.py) +- [backend/app/models/user.py](file://backend/app/models/user.py) +- [backend/app/models/citation_record.py](file://backend/app/models/citation_record.py) + + +## 目录 +1. [简介](#简介) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖分析](#依赖分析) +7. [性能考虑](#性能考虑) +8. [故障排查指南](#故障排查指南) +9. [结论](#结论) +10. [附录](#附录) + +## 简介 +本文件为 GEO 平台的 API 接口设计规范文档,面向后端与前端开发者,系统性阐述 RESTful API 设计原则、路由组织结构、版本控制策略、请求/响应数据模型(Pydantic)、错误处理机制、认证与授权、API 文档与测试策略,以及使用示例与最佳实践。 + +## 项目结构 +后端采用 FastAPI 框架,按功能模块划分 API 路由与服务层,数据库使用 SQLAlchemy 异步引擎,配置通过 Pydantic Settings 管理。主要模块如下: +- 应用入口与路由注册:backend/app/main.py +- 认证与用户:backend/app/api/auth.py、backend/app/services/auth.py、backend/app/schemas/auth.py +- 查询词管理:backend/app/api/queries.py、backend/app/services/query.py、backend/app/schemas/query.py +- 引用数据与统计:backend/app/api/citations.py、backend/app/services/citation.py、backend/app/schemas/citation.py +- 报告导出:backend/app/api/reports.py +- 依赖注入与鉴权:backend/app/api/deps.py +- 数据库与模型:backend/app/database.py、backend/app/models/*.py +- 配置:backend/app/config.py + +```mermaid +graph TB +A["应用入口
backend/app/main.py"] --> B["认证路由
backend/app/api/auth.py"] +A --> C["查询路由
backend/app/api/queries.py"] +A --> D["引用路由
backend/app/api/citations.py"] +A --> E["报告路由
backend/app/api/reports.py"] +B --> F["认证服务
backend/app/services/auth.py"] +C --> G["查询服务
backend/app/services/query.py"] +D --> H["引用服务
backend/app/services/citation.py"] +F --> I["数据库会话
backend/app/database.py"] +G --> I +H --> I +I --> J["配置
backend/app/config.py"] +I --> K["用户模型
backend/app/models/user.py"] +I --> L["引用记录模型
backend/app/models/citation_record.py"] +``` + +图表来源 +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/app/api/queries.py:1-86](file://backend/app/api/queries.py#L1-L86) +- [backend/app/api/citations.py:1-78](file://backend/app/api/citations.py#L1-L78) +- [backend/app/api/reports.py:1-47](file://backend/app/api/reports.py#L1-L47) +- [backend/app/services/auth.py:1-69](file://backend/app/services/auth.py#L1-L69) +- [backend/app/services/query.py:1-130](file://backend/app/services/query.py#L1-L130) +- [backend/app/services/citation.py:1-269](file://backend/app/services/citation.py#L1-L269) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [backend/app/models/user.py:1-41](file://backend/app/models/user.py#L1-L41) +- [backend/app/models/citation_record.py:1-42](file://backend/app/models/citation_record.py#L1-L42) + +章节来源 +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) + +## 核心组件 +- 版本控制与路由前缀 + - 应用统一使用 /api/v1 前缀,并按资源划分子路由: + - /api/v1/auth:认证相关 + - /api/v1/queries:查询词 CRUD + - /api/v1/citations:引用数据列表、统计、立即执行 + - /api/v1/reports:报告导出 +- CORS 配置 + - 允许本地前端 localhost:3000 进行跨域访问,支持所有方法与头 +- 健康检查 + - GET /health 返回 {"status":"ok"} + +章节来源 +- [backend/app/main.py:24-48](file://backend/app/main.py#L24-L48) + +## 架构总览 +下图展示从客户端到 API、服务层与数据库的调用链路,以及认证中间件如何注入当前用户上下文。 + +```mermaid +sequenceDiagram +participant Client as "客户端" +participant API as "FastAPI 应用" +participant Router as "API 路由" +participant Deps as "依赖注入/鉴权" +participant Service as "业务服务" +participant DB as "数据库" +Client->>API : "HTTP 请求" +API->>Router : "路由分发" +Router->>Deps : "获取当前用户(鉴权)" +Deps-->>Router : "User 对象" +Router->>Service : "调用业务逻辑" +Service->>DB : "SQLAlchemy 异步查询/写入" +DB-->>Service : "结果集" +Service-->>Router : "领域对象/聚合" +Router-->>Client : "JSON 响应/状态码" +``` + +图表来源 +- [backend/app/main.py:38-42](file://backend/app/main.py#L38-L42) +- [backend/app/api/deps.py:16-43](file://backend/app/api/deps.py#L16-L43) +- [backend/app/api/auth.py:13-42](file://backend/app/api/auth.py#L13-L42) +- [backend/app/api/queries.py:15-85](file://backend/app/api/queries.py#L15-L85) +- [backend/app/api/citations.py:25-77](file://backend/app/api/citations.py#L25-L77) +- [backend/app/api/reports.py:16-46](file://backend/app/api/reports.py#L16-L46) +- [backend/app/services/auth.py:37-69](file://backend/app/services/auth.py#L37-L69) +- [backend/app/services/query.py:12-130](file://backend/app/services/query.py#L12-L130) +- [backend/app/services/citation.py:24-269](file://backend/app/services/citation.py#L24-L269) +- [backend/app/database.py:23-29](file://backend/app/database.py#L23-L29) + +## 详细组件分析 + +### 认证与用户管理 +- 路由与方法 + - POST /api/v1/auth/register:注册,返回用户信息,状态码 201 + - POST /api/v1/auth/login:登录,返回访问令牌与用户信息,状态码 200;失败 401 + - GET /api/v1/auth/me:获取当前用户信息,需携带 Bearer Token +- 鉴权流程 + - 使用 OAuth2PasswordBearer 指向 /api/v1/auth/login + - 依赖 get_current_user 校验 JWT 并加载用户 +- 数据模型 + - 注册输入:邮箱、密码、姓名(最小长度约束) + - 登录输入:邮箱、密码 + - 用户输出:id、email、name、plan、max_queries、is_active、created_at + - 令牌输出:access_token、token_type、user +- 错误处理 + - 注册重复邮箱:400 + - 登录失败:401(带 WWW-Authenticate 头) + - 鉴权失败:401(凭据无效) + +```mermaid +sequenceDiagram +participant Client as "客户端" +participant Auth as "认证路由" +participant Deps as "get_current_user" +participant Svc as "认证服务" +participant DB as "数据库" +Client->>Auth : "POST /api/v1/auth/login" +Auth->>Svc : "authenticate_user(email,password)" +Svc->>DB : "查询用户并校验密码" +DB-->>Svc : "用户对象" +Svc-->>Auth : "用户或None" +Auth-->>Client : "200 OK + {access_token,user}" +Note over Client,Auth : "后续请求携带 Authorization : Bearer " +``` + +图表来源 +- [backend/app/api/auth.py:13-42](file://backend/app/api/auth.py#L13-L42) +- [backend/app/api/deps.py:16-43](file://backend/app/api/deps.py#L16-L43) +- [backend/app/services/auth.py:55-69](file://backend/app/services/auth.py#L55-L69) + +章节来源 +- [backend/app/api/auth.py:13-42](file://backend/app/api/auth.py#L13-L42) +- [backend/app/api/deps.py:16-43](file://backend/app/api/deps.py#L16-L43) +- [backend/app/schemas/auth.py:7-34](file://backend/app/schemas/auth.py#L7-L34) +- [backend/app/services/auth.py:37-69](file://backend/app/services/auth.py#L37-L69) + +### 查询词管理 +- 路由与方法 + - GET /api/v1/queries:分页列出查询词,支持 skip/limit + - POST /api/v1/queries:创建查询词,状态码 201 + - GET /api/v1/queries/{query_id}:获取单个查询词 + - PUT /api/v1/queries/{query_id}:更新查询词 + - DELETE /api/v1/queries/{query_id}:删除查询词,状态码 204 +- 输入/输出模型 + - 创建输入:keyword、target_brand、brand_aliases、platforms、frequency(默认 weekly) + - 更新输入:可选字段 keyword、target_brand、brand_aliases、platforms、frequency、status + - 输出:完整查询词详情(含频率、状态、时间戳等) +- 权限与限制 + - 用户最大查询数受用户计划限制,超过抛出 403 + - 所有操作均进行所有权校验(user_id 匹配) +- 错误处理 + - 未找到:404 + - 超出配额:403 + - 参数校验失败:422(Pydantic 自动) + +```mermaid +flowchart TD +Start(["创建查询"]) --> CheckCount["检查用户当前查询数"] +CheckCount --> CountOK{"是否小于等于 max_queries?"} +CountOK -- 否 --> Forbidden["返回 403: 超出配额"] +CountOK -- 是 --> CalcNext["根据频率计算 next_query_at"] +CalcNext --> Create["持久化 Query 记录"] +Create --> Return201["返回 201 + QueryResponse"] +``` + +图表来源 +- [backend/app/api/queries.py:26-39](file://backend/app/api/queries.py#L26-L39) +- [backend/app/services/query.py:45-81](file://backend/app/services/query.py#L45-L81) + +章节来源 +- [backend/app/api/queries.py:15-85](file://backend/app/api/queries.py#L15-L85) +- [backend/app/schemas/query.py:11-94](file://backend/app/schemas/query.py#L11-L94) +- [backend/app/services/query.py:12-130](file://backend/app/services/query.py#L12-L130) + +### 引用数据与统计 +- 路由与方法 + - GET /api/v1/citations:分页列出引用记录,支持 query_id、platform、日期范围过滤 + - GET /api/v1/citations/stats:统计摘要(总查询、总引用、引用率、平台分布、趋势) + - POST /api/v1/queries/{query_id}/run-now:立即触发查询任务,状态码 202 +- 数据模型 + - 引用记录输出:id、query_id、platform、cited、citation_position、citation_text、competitor_brands、queried_at + - 统计输出:总览指标、平台维度统计、周粒度趋势 + - 立即执行输出:task_id、status、message +- 安全与权限 + - 列表与统计均进行所有权校验;当提供 query_id 时额外校验查询归属 + - 导出 CSV 亦进行所有权校验 +- 错误处理 + - 未找到查询:404 + - 查询非激活或无平台配置:400/404 + - 未授权/无效凭据:401 + +```mermaid +sequenceDiagram +participant Client as "客户端" +participant Cit as "引用路由" +participant Svc as "引用服务" +participant DB as "数据库" +Client->>Cit : "GET /api/v1/citations/stats?query_id={id}" +Cit->>Svc : "get_citation_stats(user_id, query_id?)" +Svc->>DB : "聚合统计(条件含 user_id 与可选 query_id)" +DB-->>Svc : "统计结果" +Svc-->>Cit : "CitationStatsResponse" +Cit-->>Client : "200 OK" +``` + +图表来源 +- [backend/app/api/citations.py:49-56](file://backend/app/api/citations.py#L49-L56) +- [backend/app/services/citation.py:76-201](file://backend/app/services/citation.py#L76-L201) + +章节来源 +- [backend/app/api/citations.py:25-77](file://backend/app/api/citations.py#L25-L77) +- [backend/app/schemas/citation.py:7-50](file://backend/app/schemas/citation.py#L7-L50) +- [backend/app/services/citation.py:24-269](file://backend/app/services/citation.py#L24-L269) + +### 报告导出 +- 路由与方法 + - GET /api/v1/reports/export/csv:导出 CSV 文件流,支持文件名与 Content-Disposition 头 +- 错误处理 + - 不支持的格式:400 + - 查询不存在:404 + - 流式响应:StreamingResponse,媒体类型 text/csv + +章节来源 +- [backend/app/api/reports.py:16-46](file://backend/app/api/reports.py#L16-L46) +- [backend/app/services/citation.py:237-269](file://backend/app/services/citation.py#L237-L269) + +### 立即执行查询(独立路由) +- 路由与方法 + - POST /api/v1/queries/{query_id}/run-now:加入查询任务队列,状态码 202 +- 行为 + - 校验查询归属与状态,为每个平台创建一个 QueryTask +- 错误处理 + - 查询不存在或非激活:404 + - 无平台配置:400 + +章节来源 +- [backend/app/api/citations.py:59-77](file://backend/app/api/citations.py#L59-L77) +- [backend/app/services/citation.py:204-234](file://backend/app/services/citation.py#L204-L234) + +## 依赖分析 +- 组件耦合 + - API 层仅依赖依赖注入与服务层,不直接操作数据库 + - 服务层通过异步 Session 访问数据库,避免阻塞 + - 配置集中于 Settings,便于环境隔离 +- 关键依赖关系 + - OAuth2PasswordBearer 指向登录端点,确保令牌一致 + - 所有受保护路由均通过 get_current_user 注入 User 上下文 + - 业务服务内部进行所有权校验,保证数据隔离 + +```mermaid +graph LR +API_Auth["API 认证"] --> Deps["依赖注入"] +API_Queries["API 查询"] --> Deps +API_Citations["API 引用"] --> Deps +API_Reports["API 报告"] --> Deps +Deps --> Svc_Auth["认证服务"] +Deps --> Svc_Query["查询服务"] +Deps --> Svc_Citation["引用服务"] +Svc_Auth --> DB["数据库"] +Svc_Query --> DB +Svc_Citation --> DB +``` + +图表来源 +- [backend/app/api/deps.py:13-43](file://backend/app/api/deps.py#L13-L43) +- [backend/app/services/auth.py:37-69](file://backend/app/services/auth.py#L37-L69) +- [backend/app/services/query.py:12-130](file://backend/app/services/query.py#L12-L130) +- [backend/app/services/citation.py:24-269](file://backend/app/services/citation.py#L24-L269) +- [backend/app/database.py:23-29](file://backend/app/database.py#L23-L29) + +## 性能考虑 +- 分页与过滤 + - 列表接口支持 skip/limit,建议前端按需请求,避免一次性拉取大量数据 + - 引用列表支持多维过滤(query_id、platform、日期),建议在高频查询场景下配合索引 +- 数据库索引 + - 引用记录表对 query_id、queried_at、platform 建有索引,有利于统计与分页 +- 异步 I/O + - 使用 SQLAlchemy 异步引擎与异步 Session,减少阻塞 +- 缓存与队列 + - 立即执行查询通过任务队列异步处理,避免长耗时请求阻塞 API + +## 故障排查指南 +- 401 未授权 + - 检查 Authorization 头是否为 Bearer Token + - 检查 token 是否过期或签名错误 +- 403 禁止访问 + - 用户配额不足或权限不足(如创建查询超限) +- 404 未找到 + - 资源 ID 不存在或不属于当前用户 +- 400 参数错误 + - 平台列表为空或包含非法值;导出格式不支持 +- 500 服务器错误 + - 检查数据库连接与服务日志 + +章节来源 +- [backend/app/api/auth.py:26-30](file://backend/app/api/auth.py#L26-L30) +- [backend/app/api/queries.py:34-38](file://backend/app/api/queries.py#L34-L38) +- [backend/app/api/citations.py:67-71](file://backend/app/api/citations.py#L67-L71) +- [backend/app/api/reports.py:23-27](file://backend/app/api/reports.py#L23-L27) +- [backend/app/api/deps.py:20-41](file://backend/app/api/deps.py#L20-L41) + +## 结论 +本规范以 RESTful 设计为核心,结合 FastAPI 的类型安全与自动文档能力,构建了清晰的路由分层、严格的认证授权与完善的错误处理机制。通过 Pydantic 模型与 SQLAlchemy 异步 ORM,实现了高一致性与高性能的数据访问。建议在生产环境中进一步完善速率限制、审计日志与监控告警体系。 + +## 附录 + +### RESTful 设计原则与路由组织 +- URL 命名规范 + - 使用名词复数形式表示资源集合,如 /queries、/citations + - 资源标识符使用路径参数,如 /queries/{query_id} +- HTTP 方法使用 + - GET:读取资源列表或单个资源 + - POST:创建资源 + - PUT:更新资源 + - DELETE:删除资源 +- 状态码标准 + - 200:成功获取或更新 + - 201:创建成功 + - 202:异步任务已接受 + - 204:删除成功且无内容 + - 400:参数或业务错误 + - 401:未授权 + - 403:禁止访问 + - 404:资源不存在 + - 422:数据校验失败(Pydantic) + - 500:服务器内部错误 + +### API 版本控制与路由前缀 +- 版本控制策略 + - 采用 URL 前缀 /api/v1 进行版本隔离,便于未来演进 +- 路由前缀管理 + - 认证:/api/v1/auth + - 查询词:/api/v1/queries + - 引用数据:/api/v1/citations + - 报告:/api/v1/reports + +章节来源 +- [backend/app/main.py:38-42](file://backend/app/main.py#L38-L42) + +### 请求与响应数据模型(Pydantic) +- 认证 + - 注册输入:邮箱、密码、姓名(最小长度约束) + - 登录输入:邮箱、密码 + - 用户输出:id、email、name、plan、max_queries、is_active、created_at + - 令牌输出:access_token、token_type、user +- 查询词 + - 创建输入:keyword、target_brand、brand_aliases、platforms、frequency(默认 weekly) + - 更新输入:可选字段 keyword、target_brand、brand_aliases、platforms、frequency、status + - 输出:完整查询词详情(含频率、状态、时间戳等) +- 引用数据 + - 列表项:id、query_id、platform、cited、citation_position、citation_text、competitor_brands、queried_at + - 统计:总览指标、平台维度统计、周粒度趋势 + - 立即执行:task_id、status、message + +章节来源 +- [backend/app/schemas/auth.py:7-34](file://backend/app/schemas/auth.py#L7-L34) +- [backend/app/schemas/query.py:11-94](file://backend/app/schemas/query.py#L11-L94) +- [backend/app/schemas/citation.py:7-50](file://backend/app/schemas/citation.py#L7-L50) + +### 错误处理机制 +- 异常类型分类 + - 业务异常:如配额不足、查询不存在、平台非法 + - 凭据异常:JWT 解析失败、用户不存在 +- 错误响应格式 + - 统一为 JSON:{ "detail": "错误描述" } + - 未授权携带 WWW-Authenticate 头 +- HTTP 状态码映射 + - 400:参数/业务错误 + - 401:凭据无效 + - 403:权限不足 + - 404:资源不存在 + - 422:数据校验失败 + - 500:服务器错误 + +章节来源 +- [backend/app/api/auth.py:17-18](file://backend/app/api/auth.py#L17-L18) +- [backend/app/api/queries.py:34-38](file://backend/app/api/queries.py#L34-L38) +- [backend/app/api/citations.py:67-71](file://backend/app/api/citations.py#L67-L71) +- [backend/app/api/reports.py:23-27](file://backend/app/api/reports.py#L23-L27) +- [backend/app/api/deps.py:20-41](file://backend/app/api/deps.py#L20-L41) + +### 认证与授权实现 +- 认证方式 + - JWT Bearer Token,密钥与过期时间由配置管理 +- 授权策略 + - 所有受保护路由通过 get_current_user 注入当前用户 + - 业务层进行所有权校验(user_id 匹配),防止越权访问 +- 最佳实践 + - 生产环境更换默认密钥与设置合理过期时间 + - 前端统一在请求头携带 Authorization: Bearer + +章节来源 +- [backend/app/api/deps.py:13-43](file://backend/app/api/deps.py#L13-L43) +- [backend/app/services/auth.py:24-34](file://backend/app/services/auth.py#L24-L34) +- [backend/app/config.py:9-10](file://backend/app/config.py#L9-L10) + +### API 文档生成与测试策略 +- 文档生成 + - FastAPI 自动生成 OpenAPI 规范与交互式文档,默认启用 +- 测试策略 + - 单元测试:针对服务层函数(如 get_citations、create_query)进行异步测试 + - 集成测试:通过 API 路由层发起请求,验证鉴权、权限与错误码 + - 建议使用 pytest 异步运行器与测试数据库 + +### API 使用示例与最佳实践 +- 示例 + - 登录获取令牌后,在后续请求头中添加 Authorization: Bearer + - 创建查询时指定 platforms 与 frequency,注意平台集合的有效性 + - 导出报告时仅支持 csv 格式 +- 最佳实践 + - 前端分页请求 skip/limit,避免一次性加载过多数据 + - 在高频查询场景下利用过滤参数缩小数据集 + - 对外暴露的路由保持幂等性与明确的状态码语义 \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/后端系统架构/后端系统架构.md b/.qoder/repowiki/zh/content/后端系统架构/后端系统架构.md new file mode 100644 index 0000000..7a0bb73 --- /dev/null +++ b/.qoder/repowiki/zh/content/后端系统架构/后端系统架构.md @@ -0,0 +1,431 @@ +# 后端系统架构 + + +**本文档引用的文件** +- [backend/app/main.py](file://backend/app/main.py) +- [backend/app/config.py](file://backend/app/config.py) +- [backend/app/database.py](file://backend/app/database.py) +- [backend/app/api/auth.py](file://backend/app/api/auth.py) +- [backend/app/api/queries.py](file://backend/app/api/queries.py) +- [backend/app/api/citations.py](file://backend/app/api/citations.py) +- [backend/app/api/deps.py](file://backend/app/api/deps.py) +- [backend/app/schemas/auth.py](file://backend/app/schemas/auth.py) +- [backend/app/models/user.py](file://backend/app/models/user.py) +- [backend/app/models/query.py](file://backend/app/models/query.py) +- [backend/app/models/citation_record.py](file://backend/app/models/citation_record.py) +- [backend/app/services/auth.py](file://backend/app/services/auth.py) +- [backend/app/workers/scheduler.py](file://backend/app/workers/scheduler.py) +- [backend/app/workers/citation_engine.py](file://backend/app/workers/citation_engine.py) + + +## 目录 +1. [引言](#引言) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖关系分析](#依赖关系分析) +7. [性能考量](#性能考量) +8. [故障排查指南](#故障排查指南) +9. [结论](#结论) +10. [附录](#附录) + +## 引言 +本文件为 GEO 平台后端系统的架构文档,基于 FastAPI 构建,采用异步 SQLAlchemy ORM、APScheduler 定时任务与多平台适配器模式,实现查询词管理、引用检测与报告统计等功能。文档覆盖应用配置、中间件、路由组织、生命周期管理、数据库连接与 ORM、异步处理、认证与权限控制、API 设计与错误处理、系统监控与日志、性能优化策略,并给出架构决策的技术背景与权衡。 + +## 项目结构 +后端采用分层与功能域结合的组织方式: +- 应用入口与生命周期:app/main.py +- 配置中心:app/config.py +- 数据库与依赖注入:app/database.py +- API 层:app/api/ 下按功能模块划分(auth、queries、citations、deps) +- 模型层:app/models/(SQLAlchemy ORM 映射) +- 服务层:app/services/(业务逻辑封装) +- 工作器与调度:app/workers/(APScheduler 调度器、引用检测引擎、平台适配器) +- 测试:tests/(pytest) + +```mermaid +graph TB +subgraph "应用入口" +MAIN["app/main.py"] +end +subgraph "配置与数据库" +CFG["app/config.py"] +DB["app/database.py"] +end +subgraph "API 层" +AUTH["app/api/auth.py"] +QUERIES["app/api/queries.py"] +CITATIONS["app/api/citations.py"] +DEPS["app/api/deps.py"] +end +subgraph "模型与服务" +MODEL_USER["app/models/user.py"] +MODEL_QUERY["app/models/query.py"] +MODEL_CIT["app/models/citation_record.py"] +SVC_AUTH["app/services/auth.py"] +end +subgraph "工作器与调度" +SCHED["app/workers/scheduler.py"] +ENGINE["app/workers/citation_engine.py"] +end +MAIN --> AUTH +MAIN --> QUERIES +MAIN --> CITATIONS +MAIN --> SCHED +AUTH --> SVC_AUTH +AUTH --> DB +QUERIES --> DB +CITATIONS --> DB +DEPS --> DB +SVC_AUTH --> CFG +SCHED --> DB +SCHED --> ENGINE +ENGINE --> MODEL_QUERY +ENGINE --> MODEL_CIT +DB --> CFG +``` + +图表来源 +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/app/api/queries.py:1-86](file://backend/app/api/queries.py#L1-L86) +- [backend/app/api/citations.py:1-78](file://backend/app/api/citations.py#L1-L78) +- [backend/app/api/deps.py:1-43](file://backend/app/api/deps.py#L1-L43) +- [backend/app/services/auth.py:1-69](file://backend/app/services/auth.py#L1-L69) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) + +章节来源 +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) + +## 核心组件 +- 应用入口与生命周期:通过 lifespan 钩子在启动时初始化模型与调度器,在关闭时优雅停止。 +- 中间件:启用 CORS,允许前端 localhost:3000 访问。 +- 路由组织:按模块拆分,统一前缀与标签,便于 API 文档生成与维护。 +- 数据库:异步 SQLAlchemy 引擎与会话工厂,依赖注入式获取会话。 +- 认证与权限:OAuth2 密码流 + JWT,依赖注入解析当前用户,未授权时抛出 401。 +- 引擎与调度:APScheduler 定时扫描到期查询,调用 CitationEngine 执行并持久化结果。 + +章节来源 +- [backend/app/main.py:13-48](file://backend/app/main.py#L13-L48) +- [backend/app/database.py:6-29](file://backend/app/database.py#L6-L29) +- [backend/app/api/deps.py:13-43](file://backend/app/api/deps.py#L13-L43) + +## 架构总览 +系统采用“API 层-服务层-模型层-基础设施”的分层架构,配合异步 I/O 与定时任务,实现高并发与可扩展的查询与检测能力。 + +```mermaid +graph TB +CLIENT["客户端/前端"] +FASTAPI["FastAPI 应用
lifespan/CORS"] +ROUTER_AUTH["认证路由"] +ROUTER_QUERIES["查询路由"] +ROUTER_CIT["引用路由"] +DEPS["依赖注入
OAuth2/JWT 解析"] +SVC_AUTH["认证服务
密码哈希/JWT"] +DB["异步数据库
Session 工厂"] +SCHED["查询调度器
APScheduler"] +ENGINE["引用检测引擎
平台适配器"] +CLIENT --> FASTAPI +FASTAPI --> ROUTER_AUTH +FASTAPI --> ROUTER_QUERIES +FASTAPI --> ROUTER_CIT +ROUTER_AUTH --> DEPS +ROUTER_QUERIES --> DEPS +ROUTER_CIT --> DEPS +ROUTER_AUTH --> SVC_AUTH +ROUTER_QUERIES --> DB +ROUTER_CIT --> DB +DEPS --> DB +SVC_AUTH --> DB +SCHED --> DB +SCHED --> ENGINE +ENGINE --> DB +``` + +图表来源 +- [backend/app/main.py:24-48](file://backend/app/main.py#L24-L48) +- [backend/app/api/deps.py:16-43](file://backend/app/api/deps.py#L16-L43) +- [backend/app/services/auth.py:16-69](file://backend/app/services/auth.py#L16-L69) +- [backend/app/workers/scheduler.py:25-95](file://backend/app/workers/scheduler.py#L25-L95) +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) + +## 详细组件分析 + +### 应用入口与生命周期 +- 使用 lifespan 钩子在启动时导入模型并启动查询调度器;在关闭时优雅停止调度器与引擎资源。 +- 注册 CORS 中间件,允许前端跨域访问。 +- 统一注册认证、查询、引用、报告路由,并为“立即执行”路由复用同一前缀。 + +```mermaid +sequenceDiagram +participant Client as "客户端" +participant App as "FastAPI 应用" +participant Life as "lifespan" +participant Sched as "查询调度器" +Client->>App : 启动请求 +App->>Life : 进入 lifespan +Life->>Sched : start() +App-->>Client : 200 OK +Note over App,Sched : 应用运行中 +Client->>App : 关闭请求 +App->>Life : 退出 lifespan +Life->>Sched : shutdown() +App-->>Client : 200 OK +``` + +图表来源 +- [backend/app/main.py:13-22](file://backend/app/main.py#L13-L22) +- [backend/app/workers/scheduler.py:86-90](file://backend/app/workers/scheduler.py#L86-L90) + +章节来源 +- [backend/app/main.py:13-48](file://backend/app/main.py#L13-L48) + +### 配置与数据库 +- 配置项:数据库连接、Redis、JWT 秘钥与过期时间、浏览器路径、平台 API Key 等。 +- 数据库:异步引擎、会话工厂、基础模型类;提供依赖注入函数以获取会话。 +- 会话行为:非自动提交/刷新/回滚,显式管理事务边界。 + +```mermaid +flowchart TD +Start(["应用启动"]) --> LoadCfg["加载配置"] +LoadCfg --> InitEngine["创建异步引擎"] +InitEngine --> InitSession["创建会话工厂"] +InitSession --> RegisterDep["注册 get_db 依赖"] +RegisterDep --> Ready(["就绪"]) +``` + +图表来源 +- [backend/app/config.py:4-17](file://backend/app/config.py#L4-L17) +- [backend/app/database.py:6-29](file://backend/app/database.py#L6-L29) + +章节来源 +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) + +### 认证系统 +- 登录:校验邮箱与密码,成功则签发 JWT。 +- 注册:检查邮箱唯一性,哈希密码后创建用户。 +- 当前用户:通过 OAuth2 密码流获取令牌,解码 JWT 提取用户 ID,查询数据库返回当前用户。 +- 错误处理:未通过凭据验证时返回 401。 + +```mermaid +sequenceDiagram +participant Client as "客户端" +participant Auth as "认证路由" +participant Svc as "认证服务" +participant DB as "数据库" +Client->>Auth : POST /api/v1/auth/login +Auth->>Svc : authenticate_user(email, password) +Svc->>DB : 查询用户 +DB-->>Svc : 用户对象 +Svc-->>Auth : 用户或空 +Auth-->>Client : {access_token, user} 或 401 +Client->>Auth : GET /api/v1/auth/me (携带 Bearer Token) +Auth->>Svc : verify_token(token) +Svc-->>Auth : 载荷 +Auth->>DB : 查询用户 by id +DB-->>Auth : 用户对象 +Auth-->>Client : 用户信息 +``` + +图表来源 +- [backend/app/api/auth.py:13-43](file://backend/app/api/auth.py#L13-L43) +- [backend/app/services/auth.py:37-69](file://backend/app/services/auth.py#L37-L69) +- [backend/app/api/deps.py:16-43](file://backend/app/api/deps.py#L16-L43) + +章节来源 +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/app/schemas/auth.py:1-34](file://backend/app/schemas/auth.py#L1-L34) +- [backend/app/models/user.py:11-41](file://backend/app/models/user.py#L11-L41) +- [backend/app/services/auth.py:1-69](file://backend/app/services/auth.py#L1-L69) +- [backend/app/api/deps.py:1-43](file://backend/app/api/deps.py#L1-L43) + +### 查询与引用 API +- 查询 API:支持分页、创建、读取、更新、删除,均需当前用户权限。 +- 引用 API:支持分页查询、统计、立即执行查询任务(返回任务状态)。 +- 权限控制:所有路由依赖 get_current_user,未通过验证返回 401。 + +```mermaid +sequenceDiagram +participant Client as "客户端" +participant Q as "查询路由" +participant D as "依赖注入" +participant S as "服务层" +participant DB as "数据库" +Client->>Q : POST /api/v1/queries/ +Q->>D : get_current_user() +D-->>Q : 当前用户 +Q->>S : create_query(...) +S->>DB : 写入 +DB-->>S : 成功 +S-->>Q : 查询对象 +Q-->>Client : 201 + 查询对象 +``` + +图表来源 +- [backend/app/api/queries.py:26-39](file://backend/app/api/queries.py#L26-L39) +- [backend/app/api/deps.py:16-43](file://backend/app/api/deps.py#L16-L43) + +章节来源 +- [backend/app/api/queries.py:1-86](file://backend/app/api/queries.py#L1-L86) +- [backend/app/api/citations.py:1-78](file://backend/app/api/citations.py#L1-L78) + +### 引擎与调度 +- 调度器:每小时扫描 queries 表中 status='active' 且 next_query_at <= now() 的记录,逐条执行。 +- 引擎:对每个平台执行查询,进行品牌匹配与竞争品牌检测,写入 citation_records,并更新查询时间字段。 +- 平台适配器:抽象不同平台的查询接口,统一返回原始响应供匹配器处理。 + +```mermaid +flowchart TD +Tick["定时触发(每小时)"] --> Scan["扫描到期查询"] +Scan --> Found{"找到待执行查询?"} +Found -- 否 --> Wait["等待下一轮"] +Found -- 是 --> Exec["遍历查询平台执行"] +Exec --> Record["写入引用记录"] +Record --> Update["更新查询时间字段"] +Update --> Done["完成"] +Wait --> Tick +``` + +图表来源 +- [backend/app/workers/scheduler.py:51-85](file://backend/app/workers/scheduler.py#L51-L85) +- [backend/app/workers/citation_engine.py:159-234](file://backend/app/workers/citation_engine.py#L159-L234) + +章节来源 +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) + +### 数据模型与关系 +- 用户:主键 UUID、邮箱唯一、密码哈希、计划与配额、活跃状态、时间戳。 +- 查询:外键用户、关键词、目标品牌、别名、平台集合、频率、状态与时间字段。 +- 引用记录:外键查询、平台、是否引用、位置、文本、竞争品牌、原始响应、时间戳。 + +```mermaid +erDiagram +USERS { +uuid id PK +string email UK +string password_hash +string name +string plan +int max_queries +bool is_active +timestamp created_at +timestamp updated_at +} +QUERIES { +uuid id PK +uuid user_id FK +string keyword +string target_brand +jsonb brand_aliases +jsonb platforms +string frequency +string status +timestamp last_queried_at +timestamp next_query_at +timestamp created_at +timestamp updated_at +} +CITATION_RECORDS { +uuid id PK +uuid query_id FK +string platform +bool cited +int citation_position +text citation_text +jsonb competitor_brands +text raw_response +timestamp queried_at +} +USERS ||--o{ QUERIES : "拥有" +QUERIES ||--o{ CITATION_RECORDS : "产生" +``` + +图表来源 +- [backend/app/models/user.py:11-41](file://backend/app/models/user.py#L11-L41) +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [backend/app/models/citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) + +章节来源 +- [backend/app/models/user.py:1-41](file://backend/app/models/user.py#L1-L41) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [backend/app/models/citation_record.py:1-42](file://backend/app/models/citation_record.py#L1-L42) + +## 依赖关系分析 +- 组件内聚:API 路由与服务层职责清晰,模型仅负责映射。 +- 组件耦合:API 依赖服务,服务依赖数据库与配置;调度器依赖引擎与数据库;引擎依赖平台适配器。 +- 依赖注入:通过 FastAPI 依赖系统注入数据库会话与当前用户。 +- 循环依赖:未见明显循环依赖。 + +```mermaid +graph LR +API_AUTH["api/auth.py"] --> SVC_AUTH["services/auth.py"] +API_AUTH --> DEPS["api/deps.py"] +API_QUERIES["api/queries.py"] --> DEPS +API_CIT["api/citations.py"] --> DEPS +SVC_AUTH --> DB["database.py"] +DEPS --> DB +SCHED["workers/scheduler.py"] --> DB +SCHED --> ENGINE["workers/citation_engine.py"] +ENGINE --> MODELS["models/*.py"] +``` + +图表来源 +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/app/api/queries.py:1-86](file://backend/app/api/queries.py#L1-L86) +- [backend/app/api/citations.py:1-78](file://backend/app/api/citations.py#L1-L78) +- [backend/app/api/deps.py:1-43](file://backend/app/api/deps.py#L1-L43) +- [backend/app/services/auth.py:1-69](file://backend/app/services/auth.py#L1-L69) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/models/user.py:1-41](file://backend/app/models/user.py#L1-L41) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [backend/app/models/citation_record.py:1-42](file://backend/app/models/citation_record.py#L1-L42) + +章节来源 +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/app/api/queries.py:1-86](file://backend/app/api/queries.py#L1-L86) +- [backend/app/api/citations.py:1-78](file://backend/app/api/citations.py#L1-L78) +- [backend/app/api/deps.py:1-43](file://backend/app/api/deps.py#L1-L43) +- [backend/app/services/auth.py:1-69](file://backend/app/services/auth.py#L1-L69) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) + +## 性能考量 +- 异步 I/O:数据库与平台查询均采用异步,提升并发吞吐。 +- 会话管理:显式事务边界,避免长事务占用连接池。 +- 定时任务:APScheduler 异步调度,事件循环兼容处理,降低阻塞风险。 +- 索引优化:查询与引用表建立复合索引,加速过滤与排序。 +- 缓存建议:可引入 Redis 缓存热点查询结果与用户会话信息(当前配置已准备)。 +- 日志采样:生产环境建议开启采样与结构化日志,避免高频日志影响性能。 + +## 故障排查指南 +- 认证失败:检查 JWT 秘钥、过期时间与前端令牌传递;确认 OAuth2 tokenUrl 与 Bearer 头正确。 +- 数据库连接:核对 DATABASE_URL;确认容器网络可达;查看连接池与超时配置。 +- 定时任务异常:关注调度器日志,检查查询状态与平台适配器可用性;确认 next_query_at 计算逻辑。 +- 引擎执行失败:查看平台适配器错误与原始响应;检查品牌匹配器与竞争品牌检测逻辑。 +- CORS 问题:确认前端域名与请求头是否在允许范围内。 + +章节来源 +- [backend/app/api/deps.py:16-43](file://backend/app/api/deps.py#L16-L43) +- [backend/app/config.py:7-13](file://backend/app/config.py#L7-L13) +- [backend/app/workers/scheduler.py:42-90](file://backend/app/workers/scheduler.py#L42-L90) +- [backend/app/workers/citation_engine.py:211-227](file://backend/app/workers/citation_engine.py#L211-L227) + +## 结论 +该架构以 FastAPI 为核心,结合异步数据库、定时任务与多平台适配器,形成高可用、可扩展的查询与引用检测系统。通过明确的分层与依赖注入,系统具备良好的可测试性与可维护性。建议在生产环境中完善日志与监控、接入缓存与告警,并持续优化索引与查询计划。 + +## 附录 +- API 设计原则:统一前缀与标签、明确响应模型、一致的状态码与错误消息。 +- 错误处理:在路由层捕获业务异常并转换为标准 HTTP 状态码;在依赖层统一 401 未授权。 +- 响应格式:遵循 Pydantic 模型序列化,确保前后端契约一致。 +- 架构决策背景:选择异步栈以提升 I/O 密集场景性能;APScheduler 简化定时任务编排;JWT 适合无状态认证场景。 \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/后端系统架构/工作器系统.md b/.qoder/repowiki/zh/content/后端系统架构/工作器系统.md new file mode 100644 index 0000000..66b42fa --- /dev/null +++ b/.qoder/repowiki/zh/content/后端系统架构/工作器系统.md @@ -0,0 +1,460 @@ +# 工作器系统 + + +**本文档引用的文件** +- [backend/app/workers/scheduler.py](file://backend/app/workers/scheduler.py) +- [backend/app/workers/citation_engine.py](file://backend/app/workers/citation_engine.py) +- [backend/app/workers/platforms/base.py](file://backend/app/workers/platforms/base.py) +- [backend/app/workers/platforms/kimi.py](file://backend/app/workers/platforms/kimi.py) +- [backend/app/workers/platforms/wenxin.py](file://backend/app/workers/platforms/wenxin.py) +- [backend/app/workers/__init__.py](file://backend/app/workers/__init__.py) +- [backend/app/models/query.py](file://backend/app/models/query.py) +- [backend/app/models/query_task.py](file://backend/app/models/query_task.py) +- [backend/app/models/citation_record.py](file://backend/app/models/citation_record.py) +- [backend/app/main.py](file://backend/app/main.py) +- [backend/app/database.py](file://backend/app/database.py) +- [backend/app/config.py](file://backend/app/config.py) +- [backend/requirements.txt](file://backend/requirements.txt) +- [backend/app/api/queries.py](file://backend/app/api/queries.py) +- [backend/app/api/citations.py](file://backend/app/api/citations.py) + + +## 目录 +1. [引言](#引言) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖分析](#依赖分析) +7. [性能考虑](#性能考虑) +8. [故障排查指南](#故障排查指南) +9. [结论](#结论) +10. [附录](#附录) + +## 引言 +本文件为 GEO 工作器系统的全面架构文档,聚焦以下主题: +- APScheduler 任务调度器的配置与使用:调度策略、并发控制与错误恢复 +- 工作器抽象基类(BaseWorker)设计:通用接口、生命周期与状态跟踪 +- 平台适配器架构:BasePlatformAdapter 抽象、接口规范与扩展机制 +- Kimi 平台适配器实现:Playwright 自动化、页面交互与重试策略 +- 文心平台适配器设计:API 封装、响应解析与配置管理 +- 工作器注册、启动与停止流程 +- 性能监控、资源管理与故障诊断 + +## 项目结构 +后端采用 FastAPI + SQLAlchemy Async + APScheduler 架构,工作器模块位于 backend/app/workers,包含调度器、引用检测引擎与平台适配器;数据库模型位于 backend/app/models,API 路由位于 backend/app/api。 + +```mermaid +graph TB +subgraph "应用层" +API["FastAPI 应用
lifespan 启停"] +QUERIES["查询 API 路由"] +CITATIONS["引用 API 路由"] +end +subgraph "工作器层" +SCHED["调度器 QueryScheduler"] +ENGINE["引用检测引擎 CitationEngine"] +KIMI["Kimi 适配器"] +WENXIN["文心适配器"] +end +subgraph "数据层" +MODELS["SQLAlchemy 模型
Query/QueryTask/CitationRecord"] +DB["AsyncSessionLocal"] +end +API --> SCHED +API --> QUERIES +API --> CITATIONS +SCHED --> ENGINE +ENGINE --> KIMI +ENGINE --> WENXIN +ENGINE --> MODELS +SCHED --> MODELS +MODELS --> DB +``` + +图表来源 +- [backend/app/main.py:13-22](file://backend/app/main.py#L13-L22) +- [backend/app/workers/scheduler.py:25-40](file://backend/app/workers/scheduler.py#L25-L40) +- [backend/app/workers/citation_engine.py:148-158](file://backend/app/workers/citation_engine.py#L148-L158) +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [backend/app/models/query_task.py:11-39](file://backend/app/models/query_task.py#L11-L39) +- [backend/app/models/citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) +- [backend/app/database.py:12-18](file://backend/app/database.py#L12-L18) + +章节来源 +- [backend/app/main.py:13-22](file://backend/app/main.py#L13-L22) +- [backend/app/workers/__init__.py:1-13](file://backend/app/workers/__init__.py#L1-L13) + +## 核心组件 +- 调度器 QueryScheduler:基于 APScheduler AsyncIOScheduler,每小时扫描并执行到期的查询任务,负责事件循环与异步任务派发。 +- 引用检测引擎 CitationEngine:编排平台适配器、品牌匹配、竞争品牌检测与结果持久化,维护 QueryTask 状态与 Query 下次执行时间。 +- 平台适配器:BasePlatformAdapter 定义统一接口,KimiAdapter/WenxinAdapter 实现浏览器自动化与响应稳定检测。 +- 数据模型:Query(查询)、QueryTask(任务)、CitationRecord(引用记录)三者通过外键关联,配合索引优化查询性能。 + +章节来源 +- [backend/app/workers/scheduler.py:25-95](file://backend/app/workers/scheduler.py#L25-L95) +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) +- [backend/app/workers/platforms/base.py:4-18](file://backend/app/workers/platforms/base.py#L4-L18) +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [backend/app/models/query_task.py:11-39](file://backend/app/models/query_task.py#L11-L39) +- [backend/app/models/citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) + +## 架构总览 +系统通过 FastAPI 应用在 lifespan 中启动调度器,调度器周期性扫描数据库中到期的查询,调用 CitationEngine 执行跨平台检索与分析,并将结果写入数据库。平台适配器通过 Playwright 控制 Chromium 浏览器进行页面交互,具备指数退避重试与响应稳定性检测。 + +```mermaid +sequenceDiagram +participant App as "FastAPI 应用" +participant Sched as "QueryScheduler" +participant Engine as "CitationEngine" +participant DB as "数据库" +participant Plat as "平台适配器" +App->>Sched : "启动调度器" +loop 每小时 +Sched->>DB : "查询 active 且 next_query_at <= now()" +DB-->>Sched : "查询列表" +loop 遍历查询 +Sched->>Engine : "execute_query(query, db)" +Engine->>DB : "创建/更新 QueryTask" +Engine->>Plat : "adapter.query(keyword)" +Plat-->>Engine : "原始响应文本" +Engine->>Engine : "品牌匹配/竞争品牌检测" +Engine->>DB : "写入 CitationRecord" +Engine->>DB : "更新 Query.next_query_at" +end +end +``` + +图表来源 +- [backend/app/main.py:17-21](file://backend/app/main.py#L17-L21) +- [backend/app/workers/scheduler.py:30-90](file://backend/app/workers/scheduler.py#L30-L90) +- [backend/app/workers/citation_engine.py:159-234](file://backend/app/workers/citation_engine.py#L159-L234) +- [backend/app/models/query.py:27-31](file://backend/app/models/query.py#L27-L31) +- [backend/app/models/query_task.py:24-32](file://backend/app/models/query_task.py#L24-L32) +- [backend/app/models/citation_record.py:24-29](file://backend/app/models/citation_record.py#L24-L29) + +## 详细组件分析 + +### 调度器(APScheduler)配置与使用 +- 调度策略 + - 使用 AsyncIOScheduler,作业类型为间隔触发(每小时一次),作业 ID 为“check_queries”,名称为“检查并执行到期的查询任务”。 + - 通过 replace_existing=True 确保重复启动时替换旧作业。 +- 并发控制 + - 同步包装函数 _run_check 在没有运行中事件循环时使用新事件循环执行;否则在当前事件循环创建任务,避免阻塞。 + - 每次检查独立创建任务,避免阻塞后续调度。 +- 错误恢复 + - 单个查询执行失败会记录错误并继续处理下一个查询,不影响整体调度。 + - 关闭时优雅停止调度器并关闭引擎资源。 + +```mermaid +flowchart TD +Start(["启动调度器"]) --> AddJob["添加间隔作业
每小时触发"] +AddJob --> StartOK["启动成功"] +StartOK --> Loop["每小时执行一次"] +Loop --> CheckDB["查询到期的 Query"] +CheckDB --> ForEach{"是否有待执行查询?"} +ForEach --> |是| ExecOne["_execute_single_query()"] +ForEach --> |否| Sleep["等待下一小时"] +ExecOne --> TryExec["捕获异常并记录"] +TryExec --> Next["继续下一个查询"] +Sleep --> Loop +Next --> Loop +``` + +图表来源 +- [backend/app/workers/scheduler.py:30-90](file://backend/app/workers/scheduler.py#L30-L90) + +章节来源 +- [backend/app/workers/scheduler.py:25-95](file://backend/app/workers/scheduler.py#L25-L95) + +### 引擎(CitationEngine)设计与流程 +- 组件职责 + - 维护平台适配器映射(wenxin/kimi) + - 品牌匹配器(精确/别名/模糊)与竞争品牌检测 + - 任务状态管理(QueryTask)与下次查询时间计算 +- 生命周期与状态 + - 每个查询对应多个平台任务,逐个执行并更新 QueryTask 状态(pending → running → success/failed) + - 成功时写入 CitationRecord,失败时仍写入一条 cited=False 的记录用于占位 +- 数据持久化 + - 使用 AsyncSessionLocal 进行事务性读写,提交后刷新对象状态 +- 平台扩展 + - 新增平台只需在 platforms 字典中注册适配器实例,无需修改引擎主流程 + +```mermaid +classDiagram +class CitationEngine { ++platforms : dict ++matcher : BrandMatcher ++competitor_detector : CompetitorDetector ++execute_query(query, db) CitationRecord[] ++execute_single_platform(keyword, platform, target_brand, aliases) dict ++close() void +} +class BrandMatcher { ++target_brand : str ++brand_aliases : str[] ++match(text) dict +} +class CompetitorDetector { ++KNOWN_BRANDS : dict ++detect(text, target_brand) str[] +} +class KimiAdapter { ++platform_name : str ++platform_url : str ++query(keyword) str ++close() void +} +class WenxinAdapter { ++platform_name : str ++platform_url : str ++query(keyword) str ++close() void +} +CitationEngine --> KimiAdapter : "使用" +CitationEngine --> WenxinAdapter : "使用" +CitationEngine --> BrandMatcher : "使用" +CitationEngine --> CompetitorDetector : "使用" +``` + +图表来源 +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) +- [backend/app/workers/platforms/kimi.py:11-206](file://backend/app/workers/platforms/kimi.py#L11-L206) +- [backend/app/workers/platforms/wenxin.py:11-205](file://backend/app/workers/platforms/wenxin.py#L11-L205) + +章节来源 +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) + +### 平台适配器架构(BasePlatformAdapter) +- 接口规范 + - 必须实现异步 query(keyword) -> str,返回平台原始响应文本 + - 可选实现 close() 清理资源 +- 扩展机制 + - 新平台继承 BasePlatformAdapter,设置 platform_name 与 platform_url,实现 query 与可选 close + - 在 CitationEngine.platforms 中注册实例即可启用 + +```mermaid +classDiagram +class BasePlatformAdapter { +<> ++platform_name : str ++platform_url : str ++query(keyword) str* ++close() void* +} +``` + +图表来源 +- [backend/app/workers/platforms/base.py:4-18](file://backend/app/workers/platforms/base.py#L4-L18) + +章节来源 +- [backend/app/workers/platforms/base.py:4-18](file://backend/app/workers/platforms/base.py#L4-L18) + +### Kimi 平台适配器实现 +- 浏览器自动化 + - 使用 Playwright 启动 headless Chromium,设置视口与 UA + - 导航至平台首页,动态查找输入框与发送按钮,支持多种选择器与回退策略 +- 页面交互逻辑 + - 输入关键词后尝试点击发送按钮或按下 Enter 键 + - 等待回复稳定:连续多次检测文本不再变化,超时则返回当前文本 +- 错误重试策略 + - 最多重试 3 次,指数退避(2^attempt 秒延迟) + - 超时与异常转换为 RuntimeError 并记录日志 +- 资源管理 + - 每次查询结束后关闭 page/context,保证资源释放 + +```mermaid +sequenceDiagram +participant Adapter as "KimiAdapter" +participant PW as "Playwright" +participant Page as "Page" +Adapter->>Adapter : "_ensure_browser()" +Adapter->>PW : "启动 Chromium" +Adapter->>Page : "new_context() + new_page()" +Adapter->>Page : "goto(platform_url)" +Adapter->>Page : "查找输入框/发送按钮" +Adapter->>Page : "fill(keyword) / press Enter / click Send" +Adapter->>Adapter : "_wait_for_response_stable()" +Adapter-->>Adapter : "返回稳定文本" +Adapter->>Page : "close()" +Adapter->>PW : "stop()" +``` + +图表来源 +- [backend/app/workers/platforms/kimi.py:21-125](file://backend/app/workers/platforms/kimi.py#L21-L125) +- [backend/app/workers/platforms/kimi.py:126-197](file://backend/app/workers/platforms/kimi.py#L126-L197) + +章节来源 +- [backend/app/workers/platforms/kimi.py:11-206](file://backend/app/workers/platforms/kimi.py#L11-L206) + +### 文心平台适配器设计 +- 设计要点 + - 结构与 Kimi 类似,使用 Playwright 控制浏览器,支持多选择器定位输入框与发送按钮 + - 等待回复稳定算法一致,超时返回当前文本 + - 指数退避重试,异常转为 RuntimeError +- 配置管理 + - 通过 Config 设置 Playwright 浏览器路径等参数,适配容器环境 + +章节来源 +- [backend/app/workers/platforms/wenxin.py:11-205](file://backend/app/workers/platforms/wenxin.py#L11-L205) +- [backend/app/config.py:11-13](file://backend/app/config.py#L11-L13) + +### 工作器注册、启动与停止流程 +- 注册 + - 通过 workers/__init__.py 暴露 CitationEngine、KimiAdapter、WenxinAdapter、QueryScheduler、query_scheduler +- 启动 + - FastAPI lifespan 中调用 query_scheduler.start(),内部注册 APScheduler 作业并启动 +- 停止 + - 应用退出时调用 query_scheduler.shutdown(),优雅关闭调度器与引擎资源 + +```mermaid +sequenceDiagram +participant Main as "main.py" +participant Lifespan as "lifespan" +participant Sched as "QueryScheduler" +participant Engine as "CitationEngine" +Main->>Lifespan : "应用启动" +Lifespan->>Sched : "start()" +Sched->>Sched : "add_job(check_queries)" +Sched->>Engine : "初始化引擎" +Main-->>Lifespan : "yield" +Lifespan->>Sched : "shutdown()" +Sched->>Engine : "close()" +``` + +图表来源 +- [backend/app/main.py:13-22](file://backend/app/main.py#L13-L22) +- [backend/app/workers/scheduler.py:30-90](file://backend/app/workers/scheduler.py#L30-L90) + +章节来源 +- [backend/app/main.py:13-22](file://backend/app/main.py#L13-L22) +- [backend/app/workers/__init__.py:1-13](file://backend/app/workers/__init__.py#L1-L13) + +## 依赖分析 +- 外部依赖 + - Web/ASGI:FastAPI、Uvicorn + - ORM/数据库:SQLAlchemy 2.x、asyncpg、Alembic + - 任务调度:APScheduler + - 浏览器自动化:Playwright + - 配置:Pydantic Settings、python-dotenv +- 内部模块耦合 + - scheduler 依赖 database 与 models,调用 CitationEngine + - citation_engine 依赖 platforms 子模块与 models + - platforms 依赖 base 抽象类 + - main 依赖 scheduler 并在 lifespan 中启停 + +```mermaid +graph LR +REQ["requirements.txt"] --> FAST["FastAPI"] +REQ --> SQL["SQLAlchemy/asyncpg/Alembic"] +REQ --> APS["APScheduler"] +REQ --> PW["Playwright"] +REQ --> PYD["Pydantic Settings"] +MAIN["main.py"] --> SCHED["scheduler.py"] +SCHED --> ENGINE["citation_engine.py"] +ENGINE --> KIMI["platforms/kimi.py"] +ENGINE --> WENXIN["platforms/wenxin.py"] +ENGINE --> MODELS["models/*.py"] +SCHED --> MODELS +MODELS --> DB["database.py"] +``` + +图表来源 +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) +- [backend/app/main.py:10-21](file://backend/app/main.py#L10-L21) +- [backend/app/workers/scheduler.py:18-20](file://backend/app/workers/scheduler.py#L18-L20) +- [backend/app/workers/citation_engine.py:13-14](file://backend/app/workers/citation_engine.py#L13-L14) +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [backend/app/database.py:6-18](file://backend/app/database.py#L6-L18) + +章节来源 +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) + +## 性能考虑 +- 调度频率与并发 + - 每小时一次的调度频率适合周期性任务;如需更频繁,可调整 APScheduler 触发器 + - 检查过程为异步批处理,单个查询失败不会阻塞其他查询 +- 数据库访问 + - 使用 AsyncSessionLocal,开启索引(如 queries.status、queries.next_query_at)提升查询效率 + - 引擎在事务内批量写入,减少往返开销 +- 浏览器资源 + - 每次查询后及时关闭 page/context,避免内存泄漏 + - 重试采用指数退避,降低平台限流风险 +- 平台扩展 + - 新增平台仅需实现 query/close,通过引擎映射注册,不影响现有流程 + +## 故障排查指南 +- 调度器未启动 + - 确认 lifespan 正常执行,检查日志中“查询调度器已启动” + - 如无输出,检查 main.py 中 lifespan 注册与 FastAPI 版本兼容性 +- 查询未执行 + - 检查 Query.status 是否为 active,next_query_at 是否小于等于当前时间 + - 确认数据库连接字符串与表结构正确 +- 平台适配器异常 + - Playwright 未安装:根据日志提示运行安装命令 + - 页面选择器失效:平台 UI 变更导致,需更新选择器策略 + - 超时与不稳定:适当增加等待时间或放宽稳定性阈值 +- 引擎写入失败 + - 检查数据库事务提交与异常捕获,确认 CitationRecord 字段完整性 +- 资源泄露 + - 确认每次查询后 page/context 已关闭,必要时调用 adapter.close() + +章节来源 +- [backend/app/workers/scheduler.py:42-90](file://backend/app/workers/scheduler.py#L42-L90) +- [backend/app/workers/platforms/kimi.py:21-32](file://backend/app/workers/platforms/kimi.py#L21-L32) +- [backend/app/workers/platforms/wenxin.py:21-32](file://backend/app/workers/platforms/wenxin.py#L21-L32) +- [backend/app/workers/citation_engine.py:209-228](file://backend/app/workers/citation_engine.py#L209-L228) + +## 结论 +本系统以 APScheduler 为核心调度器,结合 CitationEngine 的平台编排能力与 Playwright 的浏览器自动化,实现了跨平台的引用检测与分析。通过清晰的抽象与模块化设计,系统具备良好的可扩展性与可维护性。建议在生产环境中进一步完善监控指标、日志分级与平台 UI 变更的自适应策略。 + +## 附录 +- API 路由概览 + - 查询管理:GET/POST/PUT/DELETE /api/v1/queries + - 引用数据:GET /api/v1/citations 与 GET /api/v1/citations/stats + - 立即执行:POST /api/v1/queries/{query_id}/run-now +- 数据模型 ER 关系 + +```mermaid +erDiagram +QUERY { +uuid id PK +uuid user_id FK +string keyword +string target_brand +json brand_aliases +json platforms +string frequency +string status +timestamp last_queried_at +timestamp next_query_at +} +QUERY_TASK { +uuid id PK +uuid query_id FK +string platform +string status +text error_message +timestamp scheduled_at +timestamp started_at +timestamp completed_at +} +CITATION_RECORD { +uuid id PK +uuid query_id FK +string platform +boolean cited +int citation_position +text citation_text +json competitor_brands +text raw_response +timestamp queried_at +} +QUERY ||--o{ QUERY_TASK : "包含" +QUERY ||--o{ CITATION_RECORD : "包含" +``` + +图表来源 +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [backend/app/models/query_task.py:11-39](file://backend/app/models/query_task.py#L11-L39) +- [backend/app/models/citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/后端系统架构/引用检测引擎.md b/.qoder/repowiki/zh/content/后端系统架构/引用检测引擎.md new file mode 100644 index 0000000..75c5ccc --- /dev/null +++ b/.qoder/repowiki/zh/content/后端系统架构/引用检测引擎.md @@ -0,0 +1,568 @@ +# 引用检测引擎 + + +**本文档引用的文件** +- [backend/app/workers/citation_engine.py](file://backend/app/workers/citation_engine.py) +- [backend/app/workers/platforms/base.py](file://backend/app/workers/platforms/base.py) +- [backend/app/workers/platforms/kimi.py](file://backend/app/workers/platforms/kimi.py) +- [backend/app/workers/platforms/wenxin.py](file://backend/app/workers/platforms/wenxin.py) +- [backend/app/workers/scheduler.py](file://backend/app/workers/scheduler.py) +- [backend/app/services/citation.py](file://backend/app/services/citation.py) +- [backend/app/api/citations.py](file://backend/app/api/citations.py) +- [backend/app/models/citation_record.py](file://backend/app/models/citation_record.py) +- [backend/app/models/query.py](file://backend/app/models/query.py) +- [backend/app/models/query_task.py](file://backend/app/models/query_task.py) +- [backend/app/config.py](file://backend/app/config.py) +- [backend/app/main.py](file://backend/app/main.py) +- [tests/test_citation_engine.py](file://tests/test_citation_engine.py) +- [backend/requirements.txt](file://backend/requirements.txt) + + +## 目录 +1. [简介](#简介) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖分析](#依赖分析) +7. [性能考虑](#性能考虑) +8. [故障排查指南](#故障排查指南) +9. [结论](#结论) +10. [附录](#附录) + +## 简介 +本技术文档面向“GEO引用检测引擎”,系统性阐述引用检测算法的实现原理与工程实践,涵盖文本预处理、品牌识别与上下文分析;详解BrandMatcher类的设计与实现(精确匹配、别名匹配、模糊匹配策略与正则规则);解析CompetitorDetector的竞争品牌识别机制(竞争关系定义、相似度计算与过滤规则);说明置信度评分体系(评分算法、阈值与结果排序);梳理异步处理与并发控制策略;并提供错误处理、日志记录与性能监控建议,以及与AI平台的集成接口与数据流转过程。 + +## 项目结构 +后端采用FastAPI + SQLAlchemy异步ORM + APScheduler定时任务的分层架构: +- API层:提供引用数据查询、统计与立即执行接口 +- 服务层:封装数据访问与聚合统计逻辑 +- 工作器层:引用检测引擎、平台适配器、调度器 +- 模型层:查询、任务、引用记录等持久化模型 +- 配置与入口:应用生命周期管理、CORS、路由注册 + +```mermaid +graph TB +subgraph "API层" +API_C["API: 引用数据
citations.py"] +end +subgraph "服务层" +SVC["服务: 引用数据
services/citation.py"] +end +subgraph "工作器层" +ENG["引擎: 引用检测
workers/citation_engine.py"] +SCH["调度器
workers/scheduler.py"] +ADP_BASE["适配器基类
workers/platforms/base.py"] +ADP_KIMI["适配器: Kimi
workers/platforms/kimi.py"] +ADP_WENXIN["适配器: 文心一言
workers/platforms/wenxin.py"] +end +subgraph "模型层" +M_QUERY["模型: 查询
models/query.py"] +M_TASK["模型: 任务
models/query_task.py"] +M_CIT["模型: 引用记录
models/citation_record.py"] +end +subgraph "配置与入口" +CFG["配置
config.py"] +MAIN["入口
main.py"] +end +API_C --> SVC +SVC --> ENG +ENG --> ADP_KIMI +ENG --> ADP_WENXIN +SCH --> ENG +ENG --> M_CIT +SVC --> M_CIT +API_C --> M_CIT +ENG --> M_QUERY +ENG --> M_TASK +MAIN --> SCH +MAIN --> API_C +CFG --> MAIN +``` + +图表来源 +- [backend/app/api/citations.py:1-78](file://backend/app/api/citations.py#L1-L78) +- [backend/app/services/citation.py:1-269](file://backend/app/services/citation.py#L1-L269) +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) +- [backend/app/workers/platforms/base.py:1-18](file://backend/app/workers/platforms/base.py#L1-L18) +- [backend/app/workers/platforms/kimi.py:1-206](file://backend/app/workers/platforms/kimi.py#L1-L206) +- [backend/app/workers/platforms/wenxin.py:1-205](file://backend/app/workers/platforms/wenxin.py#L1-L205) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [backend/app/models/query_task.py:1-39](file://backend/app/models/query_task.py#L1-L39) +- [backend/app/models/citation_record.py:1-42](file://backend/app/models/citation_record.py#L1-L42) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) + +章节来源 +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/api/citations.py:1-78](file://backend/app/api/citations.py#L1-L78) +- [backend/app/services/citation.py:1-269](file://backend/app/services/citation.py#L1-L269) +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [backend/app/models/query_task.py:1-39](file://backend/app/models/query_task.py#L1-L39) +- [backend/app/models/citation_record.py:1-42](file://backend/app/models/citation_record.py#L1-L42) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) + +## 核心组件 +- 引用检测引擎:负责跨平台查询、品牌匹配、竞争品牌识别与结果落库 +- 品牌匹配器BrandMatcher:精确匹配、别名匹配、模糊匹配三阶段策略 +- 竞争品牌检测器CompetitorDetector:基于预定义品牌类别集合进行竞争关系识别 +- 平台适配器:Kimi与文心一言适配器,封装Playwright自动化查询流程 +- 定时调度器:基于APScheduler的周期性任务调度 +- 服务与API:提供查询历史、统计与立即执行能力 + +章节来源 +- [backend/app/workers/citation_engine.py:19-309](file://backend/app/workers/citation_engine.py#L19-L309) +- [backend/app/workers/platforms/kimi.py:1-206](file://backend/app/workers/platforms/kimi.py#L1-L206) +- [backend/app/workers/platforms/wenxin.py:1-205](file://backend/app/workers/platforms/wenxin.py#L1-L205) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [backend/app/services/citation.py:1-269](file://backend/app/services/citation.py#L1-L269) +- [backend/app/api/citations.py:1-78](file://backend/app/api/citations.py#L1-L78) + +## 架构总览 +引擎通过API触发或定时调度器自动触发,调用平台适配器获取AI回复,随后由BrandMatcher进行品牌识别,CompetitorDetector识别竞争品牌,最终将结果写入数据库并返回。 + +```mermaid +sequenceDiagram +participant Client as "客户端" +participant API as "API : 引用数据" +participant Svc as "服务 : 引用数据" +participant Eng as "引擎 : 引用检测" +participant Plat as "平台适配器" +participant DB as "数据库" +Client->>API : GET /api/v1/citations/stats +API->>Svc : 统计查询 +Svc->>DB : 查询引用记录 +DB-->>Svc : 结果集 +Svc-->>API : 统计数据 +API-->>Client : 响应 +Client->>API : POST /api/v1/queries/{id}/run-now +API->>Svc : 触发立即查询 +Svc-->>API : 任务ID +API-->>Client : 202 Accepted +Note over Eng,DB : 定时调度器每小时检查到期查询 +Eng->>Plat : query(keyword) +Plat-->>Eng : 原始回复文本 +Eng->>Eng : BrandMatcher.match(text) +Eng->>Eng : CompetitorDetector.detect(text, target) +Eng->>DB : 写入CitationRecord +DB-->>Eng : 确认 +``` + +图表来源 +- [backend/app/api/citations.py:1-78](file://backend/app/api/citations.py#L1-L78) +- [backend/app/services/citation.py:1-269](file://backend/app/services/citation.py#L1-L269) +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) +- [backend/app/workers/platforms/kimi.py:33-125](file://backend/app/workers/platforms/kimi.py#L33-L125) +- [backend/app/workers/platforms/wenxin.py:33-123](file://backend/app/workers/platforms/wenxin.py#L33-L123) +- [backend/app/models/citation_record.py:1-42](file://backend/app/models/citation_record.py#L1-L42) + +## 详细组件分析 + +### BrandMatcher:品牌匹配器 +- 设计要点 + - 三阶段匹配策略:精确匹配 > 别名匹配 > 模糊匹配 + - 上下文提取:定位首次出现段落位置与截取片段 + - 置信度评分:精确=1.0,别名=0.9,模糊=基于相似度四舍五入 +- 实现细节 + - 精确匹配:直接包含判断 + - 别名匹配:遍历别名列表 + - 模糊匹配:对候选词(按非文字字符拆分且长度≥2)使用序列相似度比较,阈值>0.4 + - 候选词提取:支持中文与英文混合场景 + - 上下文截取:按换行切分段落,取首个命中段落前200字符 +- 复杂度分析 + - 候选词提取:O(n),n为文本长度 + - 模糊匹配:O(m*k),m为候选词数量,k为别名/目标词数量 + - 总体近似线性,受文本长度与候选词规模影响 + +```mermaid +flowchart TD +Start(["进入 match(text)"]) --> Empty{"text为空?"} +Empty --> |是| RetEmpty["返回未命中"] +Empty --> |否| Exact["精确匹配 target_brand"] +Exact --> |命中| PosCtx["提取段落位置与上下文"] --> RetExact["返回精确匹配结果"] +Exact --> |未命中| Alias["遍历别名进行匹配"] +Alias --> |命中| PosCtx2["提取段落位置与上下文"] --> RetAlias["返回别名匹配结果"] +Alias --> |未命中| Fuzzy["提取候选词并计算相似度"] +Fuzzy --> Best{"最佳相似度>0.4?"} +Best --> |是| PosCtx3["提取段落位置与上下文"] --> RetFuzzy["返回模糊匹配结果"] +Best --> |否| RetEmpty +``` + +图表来源 +- [backend/app/workers/citation_engine.py:26-119](file://backend/app/workers/citation_engine.py#L26-L119) + +章节来源 +- [backend/app/workers/citation_engine.py:19-120](file://backend/app/workers/citation_engine.py#L19-L120) +- [tests/test_citation_engine.py:6-54](file://tests/test_citation_engine.py#L6-L54) + +### CompetitorDetector:竞争品牌检测器 +- 设计要点 + - 基于预定义品牌类别集合(如保险、金融、科技) + - 排除目标品牌,返回去重后的竞争品牌列表 +- 实现细节 + - 遍历类别与品牌,进行包含判断 + - 使用集合去重,最终排序输出 +- 复杂度分析 + - O(C*B),C为类别数,B为类别内品牌数 + +```mermaid +flowchart TD +Start2(["进入 detect(text, target)"]) --> Empty2{"text为空?"} +Empty2 --> |是| RetEmpty2["返回空列表"] +Empty2 --> |否| LoopCat["遍历已知品牌类别"] +LoopCat --> LoopBrand["遍历类别内品牌"] +LoopBrand --> Filter{"品牌==target?"} +Filter --> |是| NextBrand["跳过"] +Filter --> |否| Contain{"品牌出现在text中?"} +Contain --> |是| Add["加入集合"] +Contain --> |否| NextBrand +NextBrand --> LoopBrand +LoopBrand --> Done["排序并返回列表"] +``` + +图表来源 +- [backend/app/workers/citation_engine.py:122-145](file://backend/app/workers/citation_engine.py#L122-L145) + +章节来源 +- [backend/app/workers/citation_engine.py:122-145](file://backend/app/workers/citation_engine.py#L122-L145) +- [tests/test_citation_engine.py:39-45](file://tests/test_citation_engine.py#L39-L45) + +### CitationEngine:引用检测引擎 +- 职责 + - 组织跨平台查询与检测流程 + - 维护查询任务状态与时间字段 + - 将结果持久化为引用记录 +- 关键流程 + - 初始化平台适配器与匹配器 + - 遍历平台列表,执行单平台查询与检测 + - 写入CitationRecord,更新Query时间字段 + - 异常处理:记录失败状态与错误信息 +- 并发与异步 + - 引擎方法为异步,平台适配器亦为异步 + - 平台间顺序执行(当前实现),可扩展为并行执行以提升吞吐 + +```mermaid +sequenceDiagram +participant Q as "Query" +participant E as "CitationEngine" +participant P as "平台适配器" +participant M as "BrandMatcher" +participant C as "CompetitorDetector" +participant R as "CitationRecord" +E->>E : 创建BrandMatcher(target, aliases) +loop 遍历平台 +E->>P : query(keyword) +P-->>E : 原始回复 +E->>M : match(raw_response) +M-->>E : 匹配结果 +E->>C : detect(raw_response, target) +C-->>E : 竞争品牌 +E->>R : 写入记录 +end +E->>Q : 更新last_queried_at/next_query_at +``` + +图表来源 +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) + +章节来源 +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) + +### 平台适配器:Kimi与文心一言 +- 抽象基类 + - BasePlatformAdapter定义平台名称、URL与抽象query/close方法 +- 具体实现 + - 启动Playwright与无头浏览器,导航至平台URL + - 自动查找输入框、填充关键词、提交查询(回车或点击发送) + - 等待回复稳定(连续多次检测文本不变),返回最新回复 + - 失败重试与指数退避,超时警告 + - 资源清理:关闭page/context/browser与playwright实例 +- 错误处理 + - 无法找到输入框、页面超时、最终失败抛出运行时异常 + - 日志记录每次重试与最终失败原因 + +```mermaid +classDiagram +class BasePlatformAdapter { ++string platform_name ++string platform_url ++query(keyword) str ++close() void +} +class KimiAdapter { ++platform_name = "kimi" ++platform_url = "https : //kimi.moonshot.cn" ++query(keyword) str ++close() void +} +class WenxinAdapter { ++platform_name = "wenxin" ++platform_url = "https : //yiyan.baidu.com" ++query(keyword) str ++close() void +} +KimiAdapter --|> BasePlatformAdapter +WenxinAdapter --|> BasePlatformAdapter +``` + +图表来源 +- [backend/app/workers/platforms/base.py:1-18](file://backend/app/workers/platforms/base.py#L1-L18) +- [backend/app/workers/platforms/kimi.py:1-206](file://backend/app/workers/platforms/kimi.py#L1-L206) +- [backend/app/workers/platforms/wenxin.py:1-205](file://backend/app/workers/platforms/wenxin.py#L1-L205) + +章节来源 +- [backend/app/workers/platforms/base.py:1-18](file://backend/app/workers/platforms/base.py#L1-L18) +- [backend/app/workers/platforms/kimi.py:1-206](file://backend/app/workers/platforms/kimi.py#L1-L206) +- [backend/app/workers/platforms/wenxin.py:1-205](file://backend/app/workers/platforms/wenxin.py#L1-L205) + +### 定时调度器:QueryScheduler +- 职责 + - 每小时扫描到期查询(status=active且next_query_at<=now) + - 逐条执行CitationEngine.execute_query + - 应用生命周期内启动与关闭 +- 并发控制 + - 使用AsyncIOScheduler与事件循环 + - 检查与执行过程均为异步,避免阻塞 + +```mermaid +flowchart TD +StartS(["启动调度器"]) --> AddJob["添加每小时任务"] +AddJob --> Loop["每小时执行检查"] +Loop --> Select["查询到期的Query"] +Select --> Exec["逐条执行execute_query"] +Exec --> Loop +``` + +图表来源 +- [backend/app/workers/scheduler.py:25-95](file://backend/app/workers/scheduler.py#L25-L95) + +章节来源 +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [backend/app/main.py:13-22](file://backend/app/main.py#L13-L22) + +### 服务与API:引用数据与统计 +- API + - 列表查询:支持按query_id、平台、时间范围筛选,分页 + - 统计接口:总查询次数、引用次数、引用率、平均位置、按平台统计、趋势 + - 立即执行:将查询任务加入队列,返回任务ID +- 服务 + - 权限校验:仅允许用户访问自己的查询数据 + - 统计聚合:使用SQL聚合函数计算各项指标 + - CSV导出:按查询导出引用记录CSV + +```mermaid +sequenceDiagram +participant Client as "客户端" +participant API as "API : 引用数据" +participant Svc as "服务 : 引用数据" +participant DB as "数据库" +Client->>API : GET /api/v1/citations/?query_id=... +API->>Svc : get_citations(...) +Svc->>DB : 查询引用记录(带权限校验) +DB-->>Svc : 记录列表与总数 +Svc-->>API : 返回数据 +API-->>Client : 响应 +Client->>API : GET /api/v1/citations/stats?query_id=... +API->>Svc : get_citation_stats(...) +Svc->>DB : 聚合统计 +DB-->>Svc : 统计结果 +Svc-->>API : 返回统计 +API-->>Client : 响应 +``` + +图表来源 +- [backend/app/api/citations.py:1-78](file://backend/app/api/citations.py#L1-L78) +- [backend/app/services/citation.py:24-269](file://backend/app/services/citation.py#L24-L269) + +章节来源 +- [backend/app/api/citations.py:1-78](file://backend/app/api/citations.py#L1-L78) +- [backend/app/services/citation.py:1-269](file://backend/app/services/citation.py#L1-L269) + +### 数据模型:查询、任务与引用记录 +- Query:查询词、目标品牌、别名、平台、频率、状态与时间字段 +- QueryTask:查询任务,记录平台、状态、错误信息与时间戳 +- CitationRecord:引用记录,包含是否引用、引用位置、引用文本、竞争品牌、原始响应与查询时间 + +```mermaid +erDiagram +QUERIES { +uuid id PK +uuid user_id FK +string keyword +string target_brand +json brand_aliases +json platforms +string frequency +string status +timestamp last_queried_at +timestamp next_query_at +} +QUERY_TASKS { +uuid id PK +uuid query_id FK +string platform +string status +text error_message +timestamp scheduled_at +timestamp started_at +timestamp completed_at +} +CITATION_RECORDS { +uuid id PK +uuid query_id FK +string platform +boolean cited +int citation_position +text citation_text +json competitor_brands +text raw_response +timestamp queried_at +} +QUERIES ||--o{ QUERY_TASKS : "包含" +QUERIES ||--o{ CITATION_RECORDS : "包含" +``` + +图表来源 +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [backend/app/models/query_task.py:1-39](file://backend/app/models/query_task.py#L1-L39) +- [backend/app/models/citation_record.py:1-42](file://backend/app/models/citation_record.py#L1-L42) + +章节来源 +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [backend/app/models/query_task.py:1-39](file://backend/app/models/query_task.py#L1-L39) +- [backend/app/models/citation_record.py:1-42](file://backend/app/models/citation_record.py#L1-L42) + +## 依赖分析 +- 外部依赖 + - Web框架:FastAPI、Uvicorn + - ORM与迁移:SQLAlchemy、Alembic + - 任务调度:APScheduler + - 浏览器自动化:Playwright + - 缓存:Redis(配置项存在) + - 配置:pydantic-settings +- 内部模块耦合 + - API依赖服务层;服务层依赖模型与引擎;引擎依赖适配器与模型 + - 调度器独立于API,通过引擎执行查询 + - 适配器与引擎解耦,便于扩展新平台 + +```mermaid +graph LR +FastAPI --> API +API --> Service +Service --> Engine +Engine --> AdapterBase +Engine --> Models +Scheduler --> Engine +AdapterKimi --> AdapterBase +AdapterWenxin --> AdapterBase +Config --> Main +Main --> Scheduler +Main --> API +``` + +图表来源 +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/api/citations.py:1-78](file://backend/app/api/citations.py#L1-L78) +- [backend/app/services/citation.py:1-269](file://backend/app/services/citation.py#L1-L269) +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) +- [backend/app/workers/platforms/base.py:1-18](file://backend/app/workers/platforms/base.py#L1-L18) +- [backend/app/workers/platforms/kimi.py:1-206](file://backend/app/workers/platforms/kimi.py#L1-L206) +- [backend/app/workers/platforms/wenxin.py:1-205](file://backend/app/workers/platforms/wenxin.py#L1-L205) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) + +章节来源 +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) + +## 性能考虑 +- 品牌匹配 + - 候选词提取与模糊匹配复杂度与文本长度、候选词数量相关,建议对长文本分段处理或限制最大长度 + - 模糊匹配阈值可按业务需求调整,平衡召回与精度 +- 平台适配器 + - Playwright初始化成本较高,适配器内部维护浏览器实例,避免重复启动 + - 等待回复稳定的轮询间隔与超时可按平台特性微调 +- 引擎与调度 + - 当前平台遍历为串行,可扩展为并发执行以提升吞吐(注意平台限流与稳定性) + - 定时任务每小时检查一次,可根据业务需要调整频率 +- 数据库 + - 引用记录、查询与任务均建立索引,统计查询使用聚合函数,建议定期分析与更新统计信息 + +[本节为通用性能建议,无需特定文件来源] + +## 故障排查指南 +- 浏览器与Playwright + - 现象:启动浏览器失败或提示需安装浏览器 + - 处理:按日志提示运行Playwright浏览器安装命令 + - 相关路径:[backend/app/workers/platforms/kimi.py:21-32](file://backend/app/workers/platforms/kimi.py#L21-L32)、[backend/app/workers/platforms/wenxin.py:21-32](file://backend/app/workers/platforms/wenxin.py#L21-L32) +- 页面交互失败 + - 现象:找不到输入框、页面超时 + - 处理:检查平台页面结构变化,适配器内置多选择器与超时重试,必要时调整选择器或等待策略 + - 相关路径:[backend/app/workers/platforms/kimi.py:67-88](file://backend/app/workers/platforms/kimi.py#L67-L88)、[backend/app/workers/platforms/wenxin.py:67-87](file://backend/app/workers/platforms/wenxin.py#L67-L87) +- 引擎执行异常 + - 现象:平台查询失败,记录失败状态与错误信息 + - 处理:查看任务表中的错误信息,确认平台可用性与网络状况 + - 相关路径:[backend/app/workers/citation_engine.py:211-227](file://backend/app/workers/citation_engine.py#L211-L227) +- API权限与参数 + - 现象:查询不到数据或返回空 + - 处理:确认查询ID归属当前用户,参数范围合理 + - 相关路径:[backend/app/services/citation.py:14-42](file://backend/app/services/citation.py#L14-L42) + +章节来源 +- [backend/app/workers/platforms/kimi.py:21-32](file://backend/app/workers/platforms/kimi.py#L21-L32) +- [backend/app/workers/platforms/wenxin.py:21-32](file://backend/app/workers/platforms/wenxin.py#L21-L32) +- [backend/app/workers/citation_engine.py:211-227](file://backend/app/workers/citation_engine.py#L211-L227) +- [backend/app/services/citation.py:14-42](file://backend/app/services/citation.py#L14-L42) + +## 结论 +该引擎以清晰的分层设计实现了从AI平台抓取、品牌识别、竞争品牌检测到结果落库与统计展示的完整链路。BrandMatcher与CompetitorDetector提供了稳健的文本处理与识别能力,平台适配器封装了复杂的浏览器自动化流程,调度器保障了周期性任务的可靠执行。建议后续在并发执行、阈值调优与平台扩展方面持续优化。 + +[本节为总结性内容,无需特定文件来源] + +## 附录 + +### 算法优化建议 +- 品牌匹配 + - 引入编辑距离或音近相似度算法,进一步提升模糊匹配鲁棒性 + - 对候选词进行词干化或拼音首字母预处理,减少误判 +- 并发与限流 + - 平台查询改为并发执行,并引入令牌桶限流防止被封禁 +- 缓存与降噪 + - 对热点关键词与平台响应进行缓存,降低重复请求 + - 上下文截取长度可动态调整,兼顾性能与准确性 + +[本节为通用优化建议,无需特定文件来源] + +### 自定义扩展指南 +- 新增平台适配器 + - 继承BasePlatformAdapter,实现query与close方法 + - 在CitationEngine中注册平台映射 + - 参考路径:[backend/app/workers/platforms/base.py:1-18](file://backend/app/workers/platforms/base.py#L1-L18)、[backend/app/workers/citation_engine.py:152-155](file://backend/app/workers/citation_engine.py#L152-L155) +- 调整匹配策略 + - 修改BrandMatcher的阈值、候选词提取规则或上下文截取长度 + - 参考路径:[backend/app/workers/citation_engine.py:68-92](file://backend/app/workers/citation_engine.py#L68-L92)、[backend/app/workers/citation_engine.py:102-119](file://backend/app/workers/citation_engine.py#L102-L119) +- 竞争品牌库扩展 + - 在CompetitorDetector中增加新的行业类别与品牌集合 + - 参考路径:[backend/app/workers/citation_engine.py:125-130](file://backend/app/workers/citation_engine.py#L125-L130) + +[本节为扩展性指导,无需特定文件来源] + +### 与AI平台的集成接口与数据流转 +- 接口职责 + - 平台适配器:统一query接口,屏蔽平台差异 + - 引擎:组织流程、落库、异常处理 + - API/服务:对外提供查询与统计能力 +- 数据流 + - 用户发起查询 → API/服务 → 引擎 → 平台适配器 → AI平台 → 引擎 → 数据库 → API/服务 → 前端展示 +- 监控与日志 + - 平台适配器与引擎均包含日志记录,便于追踪失败原因与性能瓶颈 + - 参考路径:[backend/app/workers/platforms/kimi.py:4-8](file://backend/app/workers/platforms/kimi.py#L4-L8)、[backend/app/workers/platforms/wenxin.py:4-8](file://backend/app/workers/platforms/wenxin.py#L4-L8)、[backend/app/workers/citation_engine.py:16-16](file://backend/app/workers/citation_engine.py#L16-L16) + +章节来源 +- [backend/app/workers/platforms/base.py:1-18](file://backend/app/workers/platforms/base.py#L1-L18) +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) +- [backend/app/api/citations.py:1-78](file://backend/app/api/citations.py#L1-L78) +- [backend/app/services/citation.py:1-269](file://backend/app/services/citation.py#L1-L269) \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/后端系统架构/数据模型设计.md b/.qoder/repowiki/zh/content/后端系统架构/数据模型设计.md new file mode 100644 index 0000000..59c5011 --- /dev/null +++ b/.qoder/repowiki/zh/content/后端系统架构/数据模型设计.md @@ -0,0 +1,462 @@ +# 数据模型设计 + + +**本文档中引用的文件** +- [user.py](file://backend/app/models/user.py) +- [query.py](file://backend/app/models/query.py) +- [citation_record.py](file://backend/app/models/citation_record.py) +- [query_task.py](file://backend/app/models/query_task.py) +- [subscription.py](file://backend/app/models/subscription.py) +- [database.py](file://backend/app/database.py) +- [__init__.py](file://backend/app/models/__init__.py) +- [488d0bd5ab01_initial_migration.py](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py) +- [config.py](file://backend/app/config.py) +- [query.py](file://backend/app/schemas/query.py) +- [citation.py](file://backend/app/schemas/citation.py) + + +## 目录 +1. [简介](#简介) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构概览](#架构概览) +5. [详细组件分析](#详细组件分析) +6. [依赖关系分析](#依赖关系分析) +7. [性能考虑](#性能考虑) +8. [故障排除指南](#故障排除指南) +9. [结论](#结论) + +## 简介 + +GEO平台的数据模型基于SQLAlchemy ORM构建,采用异步PostgreSQL数据库设计。该系统围绕用户查询管理、引用记录跟踪和任务调度构建,支持多平台内容监控和分析功能。数据模型设计遵循以下核心原则:异步数据库访问、强类型字段定义、级联关系管理和性能优化索引策略。 + +## 项目结构 + +GEO项目的数据层采用模块化设计,所有ORM模型位于`backend/app/models/`目录下,通过统一的Base类继承实现。数据库连接配置在`database.py`中定义,使用SQLAlchemy的异步引擎和会话管理。 + +```mermaid +graph TB +subgraph "数据模型层" +User[User模型] +Query[Query模型] +CitationRecord[CitationRecord模型] +QueryTask[QueryTask模型] +Subscription[Subscription模型] +end +subgraph "基础设施层" +Database[数据库引擎] +Config[配置管理] +Alembic[迁移管理] +end +subgraph "应用层" +API[API服务] +Schemas[Schemas验证] +Workers[工作进程] +end +User --> Query +Query --> CitationRecord +Query --> QueryTask +User --> Subscription +Database --> User +Database --> Query +Database --> CitationRecord +Database --> QueryTask +Database --> Subscription +Config --> Database +Alembic --> Database +``` + +**图表来源** +- [database.py:1-29](file://backend/app/database.py#L1-L29) +- [__init__.py:1-14](file://backend/app/models/__init__.py#L1-L14) + +**章节来源** +- [database.py:1-29](file://backend/app/database.py#L1-L29) +- [config.py:1-17](file://backend/app/config.py#L1-L17) + +## 核心组件 + +GEO平台包含五个核心数据模型,每个模型都经过精心设计以满足特定的业务需求: + +### 用户模型(User) +用户模型是整个系统的核心实体,负责管理平台用户的基本信息、订阅状态和查询配额。模型包含完整的身份认证信息和权限控制字段。 + +### 查询模型(Query) +查询模型代表用户的搜索请求,包含关键词、目标品牌、平台配置和调度参数。该模型支持复杂的JSONB字段存储动态配置数据。 + +### 引用记录模型(CitationRecord) +引用记录模型用于存储从各个平台抓取的内容引用信息,包括品牌提及、置信度评分和上下文数据。支持文本搜索和位置标记功能。 + +### 查询任务模型(QueryTask) +查询任务模型管理异步查询任务的生命周期,包括调度状态、执行历史和错误处理。支持多平台并发执行和状态跟踪。 + +### 订阅模型(Subscription) +订阅模型处理用户付费计划和账单管理,包含计划类型、状态、日期范围和支付信息。与用户模型建立一对一关系。 + +**章节来源** +- [user.py:11-41](file://backend/app/models/user.py#L11-L41) +- [query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) +- [query_task.py:11-39](file://backend/app/models/query_task.py#L11-L39) +- [subscription.py:11-37](file://backend/app/models/subscription.py#L11-L37) + +## 架构概览 + +GEO平台采用分层架构设计,数据模型层与业务逻辑层分离,确保了良好的可维护性和扩展性。 + +```mermaid +classDiagram +class User { ++UUID id ++String email ++String password_hash ++String name ++String plan ++Integer max_queries ++Boolean is_active ++DateTime created_at ++DateTime updated_at ++queries : List[Query] ++subscriptions : List[Subscription] +} +class Query { ++UUID id ++UUID user_id ++String keyword ++String target_brand ++List brand_aliases ++List platforms ++String frequency ++String status ++DateTime last_queried_at ++DateTime next_query_at ++DateTime created_at ++DateTime updated_at ++user : User ++citation_records : List[CitationRecord] ++query_tasks : List[QueryTask] +} +class CitationRecord { ++UUID id ++UUID query_id ++String platform ++Boolean cited ++Integer citation_position ++String citation_text ++List competitor_brands ++String raw_response ++DateTime queried_at ++query : Query +} +class QueryTask { ++UUID id ++UUID query_id ++String platform ++String status ++String error_message ++DateTime scheduled_at ++DateTime started_at ++DateTime completed_at ++query : Query +} +class Subscription { ++UUID id ++UUID user_id ++String plan ++String status ++Date start_date ++Date end_date ++Float amount ++String payment_method ++String payment_id ++DateTime created_at ++user : User +} +User "1" --> "many" Query : "has" +User "1" --> "many" Subscription : "has" +Query "1" --> "many" CitationRecord : "has" +Query "1" --> "many" QueryTask : "has" +Query "1" --> "1" User : "belongs to" +Subscription "1" --> "1" User : "belongs to" +CitationRecord "1" --> "1" Query : "belongs to" +``` + +**图表来源** +- [user.py:11-41](file://backend/app/models/user.py#L11-L41) +- [query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) +- [query_task.py:11-39](file://backend/app/models/query_task.py#L11-L39) +- [subscription.py:11-37](file://backend/app/models/subscription.py#L11-L37) + +## 详细组件分析 + +### 用户模型(User)设计 + +用户模型是系统的基础实体,采用UUID作为主键,确保分布式环境下的唯一性。邮箱字段设置为唯一约束,支持用户名密码认证。 + +#### 字段设计分析 + +| 字段名 | 类型 | 约束 | 默认值 | 描述 | +|--------|------|------|--------|------| +| id | UUID | 主键, 唯一 | 自动生成 | 用户唯一标识符 | +| email | String(255) | 唯一, 非空 | - | 用户登录邮箱 | +| password_hash | String(255) | 非空 | - | 密码哈希值 | +| name | String(100) | 可空 | - | 用户姓名 | +| plan | String(20) | 非空 | "free" | 用户套餐类型 | +| max_queries | Integer | 非空 | 5 | 每月最大查询次数 | +| is_active | Boolean | 非空 | True | 账户激活状态 | + +#### 关系映射 + +用户模型与查询模型和订阅模型建立一对多关系,使用级联删除确保数据一致性。 + +**章节来源** +- [user.py:11-41](file://backend/app/models/user.py#L11-L41) + +### 查询模型(Query)设计 + +查询模型代表用户的搜索请求,支持多平台内容监控和定期执行。 + +#### 字段设计分析 + +| 字段名 | 类型 | 约束 | 默认值 | 描述 | +|--------|------|------|--------|------| +| id | UUID | 主键 | 自动生成 | 查询唯一标识符 | +| user_id | UUID | 外键, 非空 | - | 所属用户ID | +| keyword | String(200) | 非空 | - | 搜索关键词 | +| target_brand | String(100) | 非空 | - | 目标品牌名称 | +| brand_aliases | JSONB | 默认空列表 | [] | 品牌别名列表 | +| platforms | JSONB | 非空, 默认["wenxin","kimi"] | ["wenxin","kimi"] | 监控平台列表 | +| frequency | String(20) | 非空 | "weekly" | 查询频率 | +| status | String(20) | 非空 | "active" | 查询状态 | +| last_queried_at | DateTime | 可空 | - | 最后查询时间 | +| next_query_at | DateTime | 可空 | - | 下次查询时间 | + +#### 关系映射 + +查询模型与用户、引用记录和查询任务建立复杂的关系: +- 与用户:一对多,外键约束确保数据完整性 +- 与引用记录:一对多,级联删除孤儿记录 +- 与查询任务:一对多,支持并行任务执行 + +#### 索引策略 + +查询模型包含三个关键索引: +- `idx_queries_user_id`: 加速用户查询 +- `idx_queries_status`: 支持状态过滤查询 +- `idx_queries_next_query_at`: 优化调度查询 + +**章节来源** +- [query.py:11-55](file://backend/app/models/query.py#L11-L55) + +### 引用记录模型(CitationRecord)设计 + +引用记录模型存储从各平台抓取的品牌提及信息,支持详细的上下文分析。 + +#### 字段设计分析 + +| 字段名 | 类型 | 约束 | 默认值 | 描述 | +|--------|------|------|--------|------| +| id | UUID | 主键 | 自动生成 | 记录唯一标识符 | +| query_id | UUID | 外键, 非空 | - | 所属查询ID | +| platform | String(50) | 非空 | - | 内容来源平台 | +| cited | Boolean | 非空, 默认False | False | 是否提及目标品牌 | +| citation_position | Integer | 可空 | - | 内容在结果中的位置 | +| citation_text | Text | 可空 | - | 提及的具体文本内容 | +| competitor_brands | JSONB | 默认空列表 | [] | 竞争对手品牌列表 | +| raw_response | Text | 可空 | - | 原始响应内容 | +| queried_at | DateTime | 非空 | 当前时间 | 记录创建时间 | + +#### 关系映射 + +引用记录模型与查询模型建立一对多关系,支持按查询分组统计分析。 + +#### 索引策略 + +引用记录模型包含四个关键索引: +- `idx_citation_records_query_id`: 加速查询关联查询 +- `idx_citation_records_queried_at`: 支持时间序列分析 +- `idx_citation_records_platform`: 平台维度统计 + +**章节来源** +- [citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) + +### 查询任务模型(QueryTask)设计 + +查询任务模型管理异步查询任务的完整生命周期,支持状态跟踪和错误处理。 + +#### 字段设计分析 + +| 字段名 | 类型 | 约束 | 默认值 | 描述 | +|--------|------|------|--------|------| +| id | UUID | 主键 | 自动生成 | 任务唯一标识符 | +| query_id | UUID | 外键, 非空 | - | 所属查询ID | +| platform | String(50) | 非空 | - | 执行平台 | +| status | String(20) | 非空, 默认"pending" | pending | 任务执行状态 | +| error_message | Text | 可空 | - | 错误信息 | +| scheduled_at | DateTime | 非空 | 当前时间 | 任务调度时间 | +| started_at | DateTime | 可空 | - | 任务开始时间 | +| completed_at | DateTime | 可空 | - | 任务完成时间 | + +#### 关系映射 + +查询任务模型与查询模型建立一对多关系,支持任务状态监控和统计分析。 + +#### 索引策略 + +查询任务模型包含一个关键索引: +- `idx_query_tasks_status`: 支持任务状态筛选和调度 + +**章节来源** +- [query_task.py:11-39](file://backend/app/models/query_task.py#L11-L39) + +### 订阅模型(Subscription)设计 + +订阅模型处理用户付费计划和账单管理,支持灵活的计费周期和状态管理。 + +#### 字段设计分析 + +| 字段名 | 类型 | 约束 | 默认值 | 描述 | +|--------|------|------|--------|------| +| id | UUID | 主键 | 自动生成 | 订阅唯一标识符 | +| user_id | UUID | 外键, 非空 | - | 所属用户ID | +| plan | String(20) | 非空 | - | 套餐类型 | +| status | String(20) | 非空, 默认"active" | active | 订阅状态 | +| start_date | Date | 非空 | - | 订阅开始日期 | +| end_date | Date | 非空 | - | 订阅结束日期 | +| amount | Numeric(10,2) | 可空 | - | 支付金额 | +| payment_method | String(50) | 可空 | - | 支付方式 | +| payment_id | String(255) | 可空 | - | 支付ID | +| created_at | DateTime | 非空 | 当前时间 | 创建时间 | + +#### 关系映射 + +订阅模型与用户模型建立一对多关系,支持用户订阅状态查询。 + +**章节来源** +- [subscription.py:11-37](file://backend/app/models/subscription.py#L11-L37) + +## 依赖关系分析 + +GEO平台的数据模型之间存在清晰的依赖关系,形成完整的业务数据流。 + +```mermaid +graph TD +subgraph "用户层" +Users[Users表] +Subscriptions[Subscriptions表] +end +subgraph "查询层" +Queries[Queries表] +QueryTasks[QueryTasks表] +end +subgraph "内容层" +CitationRecords[CitationRecords表] +end +Users --> Queries +Users --> Subscriptions +Queries --> CitationRecords +Queries --> QueryTasks +QueryTasks --> CitationRecords +``` + +**图表来源** +- [488d0bd5ab01_initial_migration.py:23-111](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L23-L111) + +### 外键约束分析 + +系统采用严格的外键约束确保数据完整性: +- 查询记录删除时自动删除相关引用记录和任务记录 +- 用户删除时自动清理其所有查询和订阅信息 +- 外键约束支持级联删除,防止悬挂数据 + +### 级联操作策略 + +- **查询记录**: 删除查询时级联删除所有相关记录 +- **用户信息**: 删除用户时级联清理所有关联数据 +- **任务管理**: 支持独立的任务生命周期管理 + +**章节来源** +- [488d0bd5ab01_initial_migration.py:55](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L55) +- [488d0bd5ab01_initial_migration.py:74](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L74) +- [488d0bd5ab01_initial_migration.py:92](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L92) + +## 性能考虑 + +GEO平台的数据模型在设计时充分考虑了性能优化,采用多种策略提升查询效率和系统吞吐量。 + +### 索引优化策略 + +#### 查询模型索引 +- `idx_queries_user_id`: 支持按用户快速检索查询 +- `idx_queries_status`: 优化状态过滤查询 +- `idx_queries_next_query_at`: 加速调度任务查询 + +#### 引用记录模型索引 +- `idx_citation_records_query_id`: 支持按查询分组统计 +- `idx_citation_records_queried_at`: 时间序列分析优化 +- `idx_citation_records_platform`: 平台维度查询优化 + +#### 查询任务模型索引 +- `idx_query_tasks_status`: 任务状态筛选优化 + +### 查询优化建议 + +1. **批量操作**: 使用批量插入和更新减少数据库往返 +2. **延迟加载**: 对于大型JSONB字段采用延迟加载策略 +3. **分页查询**: 对大量数据采用分页机制避免内存溢出 +4. **缓存策略**: 结合Redis缓存热点数据 + +### 数据库连接管理 + +系统使用异步数据库连接池,配置如下: +- 连接超时: 60秒 +- 连接池大小: 10-20个连接 +- 自动重连: 启用连接池重连机制 + +**章节来源** +- [database.py:6-18](file://backend/app/database.py#L6-L18) + +## 故障排除指南 + +### 常见问题诊断 + +#### 数据库连接问题 +- 检查DATABASE_URL配置是否正确 +- 验证PostgreSQL服务可用性 +- 确认网络连接和防火墙设置 + +#### 索引性能问题 +- 分析慢查询日志识别瓶颈 +- 检查索引使用情况 +- 考虑添加复合索引优化查询 + +#### 数据一致性问题 +- 验证外键约束是否正确设置 +- 检查级联删除行为 +- 确认事务边界设置 + +### 调试工具使用 + +1. **数据库监控**: 使用EXPLAIN ANALYZE分析查询计划 +2. **连接池监控**: 监控连接池使用率和等待时间 +3. **慢查询追踪**: 启用慢查询日志分析性能瓶颈 + +**章节来源** +- [config.py:7](file://backend/app/config.py#L7) + +## 结论 + +GEO平台的数据模型设计体现了现代Web应用的最佳实践,通过清晰的实体关系、完善的索引策略和异步数据库访问实现了高性能和高可用性。模型设计充分考虑了业务需求的复杂性,支持多平台内容监控、智能调度和详细统计分析。 + +系统的主要优势包括: +- **强类型设计**: 所有字段都有明确的类型定义和约束 +- **关系完整性**: 通过外键约束和级联操作确保数据一致性 +- **性能优化**: 合理的索引策略和异步数据库访问 +- **扩展性**: 模块化的架构设计便于功能扩展 + +未来可以考虑的改进方向: +- 添加审计日志支持 +- 实现数据分区策略 +- 增加数据备份和恢复机制 +- 优化大数据量场景下的查询性能 \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/后端系统架构/核心框架配置.md b/.qoder/repowiki/zh/content/后端系统架构/核心框架配置.md new file mode 100644 index 0000000..329f59e --- /dev/null +++ b/.qoder/repowiki/zh/content/后端系统架构/核心框架配置.md @@ -0,0 +1,388 @@ +# 核心框架配置 + + +**本文档引用的文件** +- [main.py](file://backend/app/main.py) +- [config.py](file://backend/app/config.py) +- [database.py](file://backend/app/database.py) +- [deps.py](file://backend/app/api/deps.py) +- [scheduler.py](file://backend/app/workers/scheduler.py) +- [auth.py](file://backend/app/api/auth.py) +- [queries.py](file://backend/app/api/queries.py) +- [query.py](file://backend/app/models/query.py) +- [docker-compose.yml](file://docker-compose.yml) +- [requirements.txt](file://backend/requirements.txt) + + +## 目录 +1. [简介](#简介) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖分析](#依赖分析) +7. [性能考虑](#性能考虑) +8. [故障排除指南](#故障排除指南) +9. [结论](#结论) +10. [附录](#附录) + +## 简介 +本文件面向GEO后端核心框架的配置与运行机制,重点覆盖以下方面: +- FastAPI应用实例的初始化、应用名称与版本管理、生命周期管理(lifespan) +- CORS中间件配置策略 +- 路由注册机制与健康检查端点 +- 配置管理系统(环境变量读取、配置验证、默认值设置) +- 数据库连接池配置、异步会话管理与连接超时设置 +- 应用启动与关闭流程及资源清理机制 +- 配置最佳实践与常见问题解决方案 + +## 项目结构 +后端采用分层组织方式,核心入口位于应用根目录,按功能划分为API、模型、服务、工具与工作器等模块。Docker Compose用于编排数据库、缓存与前后端服务。 + +```mermaid +graph TB +subgraph "后端应用" +MAIN["app/main.py
应用入口与生命周期"] +CONF["app/config.py
配置定义与默认值"] +DB["app/database.py
异步引擎与会话工厂"] +API_AUTH["app/api/auth.py
认证路由"] +API_QUERIES["app/api/queries.py
查询路由"] +DEPS["app/api/deps.py
依赖注入与鉴权"] +SCHED["app/workers/scheduler.py
定时任务调度器"] +MODEL_QUERY["app/models/query.py
查询模型"] +end +subgraph "外部服务" +DB_SRV["PostgreSQL"] +REDIS_SRV["Redis"] +end +MAIN --> API_AUTH +MAIN --> API_QUERIES +MAIN --> SCHED +API_AUTH --> DEPS +API_QUERIES --> DEPS +DEPS --> DB +SCHED --> DB +DB --> DB_SRV +CONF --> DB +CONF --> SCHED +``` + +图表来源 +- [main.py:1-48](file://backend/app/main.py#L1-L48) +- [config.py:1-17](file://backend/app/config.py#L1-L17) +- [database.py:1-29](file://backend/app/database.py#L1-L29) +- [auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [queries.py:1-86](file://backend/app/api/queries.py#L1-L86) +- [deps.py:1-43](file://backend/app/api/deps.py#L1-L43) +- [scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [query.py:1-55](file://backend/app/models/query.py#L1-L55) + +章节来源 +- [main.py:1-48](file://backend/app/main.py#L1-L48) +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) + +## 核心组件 +- 应用入口与生命周期:通过FastAPI实例与lifespan钩子实现启动与关闭阶段的资源管理。 +- 配置系统:基于Pydantic Settings的环境变量读取与默认值设置,支持.env文件加载。 +- 数据库层:SQLAlchemy 2.0异步引擎与会话工厂,提供异步依赖注入。 +- 路由与中间件:统一注册各业务路由,配置CORS以支持前端跨域访问。 +- 定时任务:APScheduler驱动的异步调度器,周期性检查并执行到期查询任务。 + +章节来源 +- [main.py:13-48](file://backend/app/main.py#L13-L48) +- [config.py:1-17](file://backend/app/config.py#L1-L17) +- [database.py:1-29](file://backend/app/database.py#L1-L29) +- [scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) + +## 架构总览 +下图展示从应用启动到请求处理的关键路径,包括配置加载、数据库会话、依赖注入与定时任务启动/关闭。 + +```mermaid +sequenceDiagram +participant Uvicorn as "Uvicorn服务器" +participant App as "FastAPI应用(main.py)" +participant Lifespan as "生命周期(lifespan)" +participant Scheduler as "查询调度器(scheduler.py)" +participant Router as "路由(auth/queries)" +participant Deps as "依赖注入(deps.py)" +participant DB as "数据库(database.py)" +Uvicorn->>App : 启动应用 +App->>Lifespan : 调用lifespan(启动) +Lifespan->>Scheduler : start() +Scheduler-->>Lifespan : 调度器就绪 +Lifespan-->>App : 启动完成 +Router->>Deps : 解析依赖(鉴权/数据库) +Deps->>DB : 获取异步会话 +DB-->>Deps : 返回AsyncSession +Deps-->>Router : 当前用户/数据库会话 +Uvicorn-->>App : 关闭信号 +App->>Lifespan : 调用lifespan(关闭) +Lifespan->>Scheduler : shutdown() +Scheduler-->>Lifespan : 调度器关闭 +Lifespan-->>App : 清理完成 +``` + +图表来源 +- [main.py:13-48](file://backend/app/main.py#L13-L48) +- [scheduler.py:30-90](file://backend/app/workers/scheduler.py#L30-L90) +- [deps.py:16-43](file://backend/app/api/deps.py#L16-L43) +- [database.py:23-29](file://backend/app/database.py#L23-L29) + +## 详细组件分析 + +### FastAPI应用初始化与生命周期管理 +- 应用名称与版本:在应用实例中设置标题与版本号,便于API文档与监控识别。 +- 生命周期钩子:通过lifespan上下文管理器在启动时导入ORM模型并启动调度器,在关闭时优雅停止调度器与资源清理。 +- 健康检查端点:提供简单健康检查接口,便于容器编排与负载均衡探测。 + +```mermaid +flowchart TD +Start(["应用启动"]) --> ImportModels["导入ORM模型"] +ImportModels --> StartScheduler["启动查询调度器"] +StartScheduler --> Ready(["应用就绪"]) +Shutdown(["应用关闭"]) --> StopScheduler["停止查询调度器"] +StopScheduler --> Cleanup(["资源清理完成"]) +``` + +图表来源 +- [main.py:13-28](file://backend/app/main.py#L13-L28) +- [scheduler.py:30-90](file://backend/app/workers/scheduler.py#L30-L90) + +章节来源 +- [main.py:13-48](file://backend/app/main.py#L13-L48) + +### CORS中间件配置 +- 允许来源:配置允许的前端地址,支持凭证传递。 +- 方法与头:开放所有HTTP方法与请求头,简化跨域交互。 +- 生效范围:对整个应用生效,确保前端与后端API通信顺畅。 + +章节来源 +- [main.py:30-36](file://backend/app/main.py#L30-L36) + +### 路由注册机制 +- 统一前缀与标签:各业务路由均带有统一的版本前缀与中文标签,便于API文档分类与维护。 +- 依赖注入:路由函数通过Depends获取数据库会话与当前用户信息,实现鉴权与数据访问分离。 +- 特殊路由:部分路由(如立即执行)复用同一前缀,保持命名一致性。 + +```mermaid +graph LR +APP["FastAPI应用(main.py)"] --> AUTH["认证路由(auth.py)"] +APP --> QUERIES["查询路由(queries.py)"] +AUTH --> DEPS["依赖注入(deps.py)"] +QUERIES --> DEPS +DEPS --> DB["数据库会话(database.py)"] +``` + +图表来源 +- [main.py:38-42](file://backend/app/main.py#L38-L42) +- [auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [queries.py:1-86](file://backend/app/api/queries.py#L1-L86) +- [deps.py:1-43](file://backend/app/api/deps.py#L1-L43) +- [database.py:23-29](file://backend/app/database.py#L23-L29) + +章节来源 +- [main.py:38-42](file://backend/app/main.py#L38-L42) +- [auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [queries.py:1-86](file://backend/app/api/queries.py#L1-L86) + +### 健康检查端点 +- 路径:/health +- 响应:返回状态字典,用于容器编排与服务发现。 +- 用途:Kubernetes/Compose健康检查、负载均衡探活。 + +章节来源 +- [main.py:45-48](file://backend/app/main.py#L45-L48) + +### 配置管理系统 +- 配置类:使用Pydantic Settings定义配置项,支持.env文件加载与额外字段忽略。 +- 默认值:数据库URL、Redis URL、JWT密钥与过期时间、浏览器自动化路径、第三方平台API Key等均有合理默认值。 +- 加载顺序:优先读取环境变量,未设置时使用默认值;生产环境务必覆盖敏感配置。 + +```mermaid +classDiagram +class Settings { ++DATABASE_URL : str ++REDIS_URL : str ++JWT_SECRET : str ++JWT_EXPIRE_HOURS : int ++PLAYWRIGHT_BROWSERS_PATH : str ++ZHIPU_API_KEY : str ++TONGYI_API_KEY : str +} +class ConfigInstance { ++settings : Settings +} +Settings <.. ConfigInstance : "实例化" +``` + +图表来源 +- [config.py:4-16](file://backend/app/config.py#L4-L16) + +章节来源 +- [config.py:1-17](file://backend/app/config.py#L1-L17) + +### 数据库连接池与异步会话管理 +- 引擎创建:基于配置中的数据库URL创建异步引擎,echo关闭,future启用。 +- 会话工厂:配置会话类、过期策略、自动刷新与提交行为,确保事务一致性。 +- 依赖注入:通过异步生成器提供会话,确保每次请求结束后正确关闭会话。 +- 连接超时:当前实现未显式设置超时参数,建议在生产环境根据数据库性能调优。 + +```mermaid +sequenceDiagram +participant Router as "路由函数" +participant Deps as "依赖注入(deps.py)" +participant SessionMaker as "AsyncSessionLocal" +participant Engine as "异步引擎" +participant DB as "数据库" +Router->>Deps : 调用get_db() +Deps->>SessionMaker : 创建会话 +SessionMaker->>Engine : 获取连接 +Engine-->>SessionMaker : 返回连接 +SessionMaker-->>Deps : 返回AsyncSession +Deps-->>Router : 提供会话 +Router->>DB : 执行查询/更新 +Router-->>Deps : 请求结束 +Deps->>SessionMaker : 关闭会话 +``` + +图表来源 +- [database.py:6-29](file://backend/app/database.py#L6-L29) +- [deps.py:16-43](file://backend/app/api/deps.py#L16-L43) + +章节来源 +- [database.py:1-29](file://backend/app/database.py#L1-L29) +- [deps.py:1-43](file://backend/app/api/deps.py#L1-L43) + +### 定时任务调度与资源清理 +- 调度器:使用APScheduler的AsyncIOScheduler,每小时检查并执行到期查询。 +- 启动流程:应用启动时调用start(),注册检查任务并启动调度器。 +- 关闭流程:应用关闭时调用shutdown(),停止调度器并关闭相关资源。 +- 任务执行:异步查询执行,异常捕获与日志记录,避免单个任务失败影响整体。 + +```mermaid +flowchart TD +Start(["应用启动"]) --> InitScheduler["初始化调度器"] +InitScheduler --> AddJob["添加定时任务(每小时)"] +AddJob --> Run["启动调度器"] +Run --> Tick["定时触发"] +Tick --> Check["查询到期任务"] +Check --> Exec["执行查询任务"] +Exec --> Tick +Shutdown(["应用关闭"]) --> Stop["停止调度器"] +Stop --> CloseRes["关闭资源"] +``` + +图表来源 +- [scheduler.py:25-95](file://backend/app/workers/scheduler.py#L25-L95) +- [main.py:13-22](file://backend/app/main.py#L13-L22) + +章节来源 +- [scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [main.py:13-22](file://backend/app/main.py#L13-L22) + +### 依赖注入与鉴权流程 +- OAuth2方案:使用OAuth2PasswordBearer,令牌端点为认证登录接口。 +- 用户解析:从令牌中提取用户ID,查询数据库获取用户对象。 +- 异常处理:令牌无效或用户不存在时抛出401错误,确保鉴权安全。 + +```mermaid +sequenceDiagram +participant Client as "客户端" +participant Router as "路由(auth.py)" +participant Deps as "依赖注入(deps.py)" +participant DB as "数据库(database.py)" +Client->>Router : 发起受保护请求 +Router->>Deps : 解析令牌与会话 +Deps->>Deps : 验证JWT令牌 +Deps->>DB : 查询用户信息 +DB-->>Deps : 返回用户对象 +Deps-->>Router : 返回当前用户 +Router-->>Client : 返回响应 +``` + +图表来源 +- [auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [deps.py:16-43](file://backend/app/api/deps.py#L16-L43) +- [database.py:23-29](file://backend/app/database.py#L23-L29) + +章节来源 +- [auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [deps.py:1-43](file://backend/app/api/deps.py#L1-L43) + +## 依赖分析 +- 应用入口依赖:路由模块、调度器与CORS中间件。 +- 路由依赖:依赖注入模块与数据库会话。 +- 调度器依赖:数据库会话工厂与查询模型。 +- 配置依赖:数据库与缓存URL、JWT配置等。 + +```mermaid +graph TB +MAIN["main.py"] --> ROUTERS["API路由模块"] +MAIN --> SCHED["scheduler.py"] +MAIN --> CORS["CORS中间件"] +ROUTERS --> DEPS["deps.py"] +DEPS --> DB["database.py"] +SCHED --> DB +SCHED --> MODEL["models/query.py"] +CONF["config.py"] --> DB +CONF --> SCHED +``` + +图表来源 +- [main.py:1-48](file://backend/app/main.py#L1-L48) +- [scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [deps.py:1-43](file://backend/app/api/deps.py#L1-L43) +- [database.py:1-29](file://backend/app/database.py#L1-L29) +- [query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [config.py:1-17](file://backend/app/config.py#L1-L17) + +章节来源 +- [main.py:1-48](file://backend/app/main.py#L1-L48) +- [requirements.txt:1-35](file://backend/requirements.txt#L1-L35) + +## 性能考虑 +- 异步数据库:使用SQLAlchemy异步引擎与会话,减少阻塞,提升并发能力。 +- 连接池参数:当前未显式设置连接池大小与超时,建议结合数据库性能与负载进行调优。 +- 定时任务频率:每小时检查一次,可根据业务量调整间隔,避免频繁扫描。 +- CORS开放策略:生产环境建议限制允许来源与方法,降低跨域风险。 + +## 故障排除指南 +- 数据库连接失败 + - 检查数据库URL与凭据是否正确,确认容器网络连通性。 + - 参考:[database.py:6-10](file://backend/app/database.py#L6-L10) +- Redis连接失败 + - 确认Redis服务可用与URL正确,检查容器健康检查状态。 + - 参考:[config.py](file://backend/app/config.py#L8) +- JWT鉴权失败 + - 确认JWT密钥与过期时间配置,检查令牌格式与有效期。 + - 参考:[config.py:9-10](file://backend/app/config.py#L9-L10) +- 定时任务不执行 + - 检查调度器启动日志与任务注册状态,确认数据库中查询状态与下次执行时间。 + - 参考:[scheduler.py:30-40](file://backend/app/workers/scheduler.py#L30-L40) +- CORS跨域问题 + - 确认允许来源与凭证设置,生产环境建议收紧策略。 + - 参考:[main.py:30-36](file://backend/app/main.py#L30-L36) + +章节来源 +- [database.py:6-10](file://backend/app/database.py#L6-L10) +- [config.py:8-10](file://backend/app/config.py#L8-L10) +- [scheduler.py:30-40](file://backend/app/workers/scheduler.py#L30-L40) +- [main.py:30-36](file://backend/app/main.py#L30-L36) + +## 结论 +GEO后端核心框架通过清晰的分层设计与异步化实现,提供了可扩展的配置体系、健壮的数据库会话管理与可靠的定时任务调度。建议在生产环境中完善连接池参数、收紧CORS策略并强化配置校验,以获得更稳定与安全的运行表现。 + +## 附录 +- 环境变量与默认值 + - 数据库URL:用于PostgreSQL连接,默认值见配置文件。 + - Redis URL:用于缓存与任务队列,默认值见配置文件。 + - JWT密钥与过期时间:用于令牌签发与验证。 + - 浏览器自动化路径:Playwright浏览器二进制路径。 + - 第三方平台API Key:用于集成外部服务。 +- Docker编排 + - 数据库与缓存服务通过Compose编排,后端服务依赖其健康状态。 + - 前端服务依赖后端服务,命令行启动参数可按需调整。 + +章节来源 +- [config.py:7-14](file://backend/app/config.py#L7-L14) +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/后端系统架构/认证系统.md b/.qoder/repowiki/zh/content/后端系统架构/认证系统.md new file mode 100644 index 0000000..e434fa0 --- /dev/null +++ b/.qoder/repowiki/zh/content/后端系统架构/认证系统.md @@ -0,0 +1,505 @@ +# 认证系统 + + +**本文档引用的文件** +- [backend/app/models/user.py](file://backend/app/models/user.py) +- [backend/app/schemas/auth.py](file://backend/app/schemas/auth.py) +- [backend/app/services/auth.py](file://backend/app/services/auth.py) +- [backend/app/api/auth.py](file://backend/app/api/auth.py) +- [backend/app/api/deps.py](file://backend/app/api/deps.py) +- [backend/app/config.py](file://backend/app/config.py) +- [backend/app/database.py](file://backend/app/database.py) +- [backend/app/main.py](file://backend/app/main.py) +- [backend/alembic/versions/488d0bd5ab01_initial_migration.py](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py) +- [frontend/lib/auth.ts](file://frontend/lib/auth.ts) +- [frontend/lib/api.ts](file://frontend/lib/api.ts) +- [frontend/app/(auth)/login/page.tsx](file://frontend/app/(auth)/login/page.tsx) +- [frontend/app/(auth)/register/page.tsx](file://frontend/app/(auth)/register/page.tsx) +- [frontend/app/api/auth/[...nextauth]/route.ts](file://frontend/app/api/auth/[...nextauth]/route.ts) +- [tests/test_auth.py](file://tests/test_auth.py) + + +## 目录 +1. [简介](#简介) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖分析](#依赖分析) +7. [性能考虑](#性能考虑) +8. [故障排查指南](#故障排查指南) +9. [结论](#结论) +10. [附录](#附录) + +## 简介 +本文件为 GEO 平台认证系统的实现文档,覆盖用户模型设计、JWT 令牌生成与验证、用户注册与登录流程、依赖注入在认证中的应用(当前用户获取与权限检查)、安全最佳实践、令牌存储策略与会话管理,以及认证错误处理与调试指南。系统采用后端 FastAPI + SQLAlchemy 异步 ORM + PostgreSQL + Redis(配置项存在);前端使用 NextAuth.js 作为会话与令牌桥接层,通过自定义凭据提供者对接后端认证接口。 + +## 项目结构 +后端认证相关模块组织如下: +- 模型层:用户模型定义与数据库表结构 +- 模式层:Pydantic 数据校验模型(注册、登录、响应) +- 服务层:密码哈希/校验、JWT 编码/解码、注册与认证业务逻辑 +- API 层:认证路由(注册、登录、当前用户)、依赖注入解析当前用户 +- 配置与数据库:设置加载、异步数据库引擎与会话工厂 +- 前端:NextAuth 配置、API 客户端、登录/注册页面 + +```mermaid +graph TB +subgraph "后端" +M["models/user.py
用户模型"] +S["schemas/auth.py
Pydantic 模型"] +SV["services/auth.py
密码/JWT/业务"] +A["api/auth.py
认证路由"] +D["api/deps.py
依赖注入解析当前用户"] +CFG["config.py
配置"] +DB["database.py
异步数据库"] +MAIN["main.py
应用入口"] +MIG["alembic 迁移
initial_migration.py"] +end +subgraph "前端" +FA["lib/api.ts
API 客户端"] +FN["lib/auth.ts
NextAuth 配置"] +LR["app/(auth)/login/page.tsx
登录页"] +RR["app/(auth)/register/page.tsx
注册页"] +NAR["app/api/auth/[...nextauth]/route.ts
NextAuth 路由"] +end +LR --> FA +RR --> FA +FA --> A +A --> SV +SV --> DB +D --> SV +D --> DB +MAIN --> A +MAIN --> D +CFG --> SV +CFG --> DB +MIG --> DB +FN --> NAR +FA --> FN +``` + +图表来源 +- [backend/app/models/user.py:11-41](file://backend/app/models/user.py#L11-L41) +- [backend/app/schemas/auth.py:7-34](file://backend/app/schemas/auth.py#L7-L34) +- [backend/app/services/auth.py:16-69](file://backend/app/services/auth.py#L16-L69) +- [backend/app/api/auth.py:13-43](file://backend/app/api/auth.py#L13-L43) +- [backend/app/api/deps.py:16-43](file://backend/app/api/deps.py#L16-L43) +- [backend/app/config.py:4-17](file://backend/app/config.py#L4-L17) +- [backend/app/database.py:6-29](file://backend/app/database.py#L6-L29) +- [backend/app/main.py:24-48](file://backend/app/main.py#L24-L48) +- [backend/alembic/versions/488d0bd5ab01_initial_migration.py:21-128](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L21-L128) +- [frontend/lib/api.ts:23-36](file://frontend/lib/api.ts#L23-L36) +- [frontend/lib/auth.ts:5-56](file://frontend/lib/auth.ts#L5-L56) +- [frontend/app/(auth)/login/page.tsx:19-93](file://frontend/app/(auth)/login/page.tsx#L19-L93) +- [frontend/app/(auth)/register/page.tsx:20-128](file://frontend/app/(auth)/register/page.tsx#L20-L128) +- [frontend/app/api/auth/[...nextauth]/route.ts:1-6](file://frontend/app/api/auth/[...nextauth]/route.ts#L1-L6) + +章节来源 +- [backend/app/main.py:24-48](file://backend/app/main.py#L24-L48) +- [backend/app/api/auth.py:13-43](file://backend/app/api/auth.py#L13-L43) +- [backend/app/api/deps.py:16-43](file://backend/app/api/deps.py#L16-L43) +- [backend/app/services/auth.py:16-69](file://backend/app/services/auth.py#L16-L69) +- [backend/app/schemas/auth.py:7-34](file://backend/app/schemas/auth.py#L7-L34) +- [backend/app/models/user.py:11-41](file://backend/app/models/user.py#L11-L41) +- [backend/app/config.py:4-17](file://backend/app/config.py#L4-L17) +- [backend/app/database.py:6-29](file://backend/app/database.py#L6-L29) +- [backend/alembic/versions/488d0bd5ab01_initial_migration.py:21-128](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L21-L128) +- [frontend/lib/api.ts:23-36](file://frontend/lib/api.ts#L23-L36) +- [frontend/lib/auth.ts:5-56](file://frontend/lib/auth.ts#L5-L56) +- [frontend/app/(auth)/login/page.tsx:19-93](file://frontend/app/(auth)/login/page.tsx#L19-L93) +- [frontend/app/(auth)/register/page.tsx:20-128](file://frontend/app/(auth)/register/page.tsx#L20-L128) +- [frontend/app/api/auth/[...nextauth]/route.ts:1-6](file://frontend/app/api/auth/[...nextauth]/route.ts#L1-L6) + +## 核心组件 +- 用户模型与数据库表 + - 字段与约束:UUID 主键、唯一邮箱、密码哈希、可选姓名、计划与配额、激活状态、时间戳等 + - 关系:与查询与订阅的一对多级联删除 +- Pydantic 模式 + - 注册:邮箱、密码最小长度、姓名长度限制 + - 登录:邮箱与密码 + - 响应:用户信息与创建时间 +- 服务层 + - 密码:bcrypt 上下文进行哈希与校验 + - JWT:HS256 签名,基于配置的密钥与过期时长 + - 注册:去重检查、哈希密码、持久化 + - 认证:邮箱查找、密码校验 +- API 层 + - 注册:接收注册体,调用服务,异常转 HTTP + - 登录:认证失败返回未授权,成功签发访问令牌 + - 当前用户:通过依赖注入解析当前用户 +- 依赖注入 + - OAuth2 密码流解析 Bearer 令牌,解码 JWT,按 ID 查询用户 +- 配置与数据库 + - 设置:数据库 URL、Redis URL、JWT 密钥、过期小时数等 + - 引擎:异步 PostgreSQL 引擎与会话工厂 +- 前端集成 + - NextAuth 使用凭据提供者对接后端登录接口,以 JWT 策略存储会话 + +章节来源 +- [backend/app/models/user.py:11-41](file://backend/app/models/user.py#L11-L41) +- [backend/alembic/versions/488d0bd5ab01_initial_migration.py:21-37](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L21-L37) +- [backend/app/schemas/auth.py:7-34](file://backend/app/schemas/auth.py#L7-L34) +- [backend/app/services/auth.py:16-69](file://backend/app/services/auth.py#L16-L69) +- [backend/app/api/auth.py:13-43](file://backend/app/api/auth.py#L13-L43) +- [backend/app/api/deps.py:16-43](file://backend/app/api/deps.py#L16-L43) +- [backend/app/config.py:4-17](file://backend/app/config.py#L4-L17) +- [backend/app/database.py:6-29](file://backend/app/database.py#L6-L29) +- [frontend/lib/auth.ts:5-56](file://frontend/lib/auth.ts#L5-L56) +- [frontend/lib/api.ts:23-36](file://frontend/lib/api.ts#L23-L36) + +## 架构总览 +后端认证流程概览: +- 前端登录/注册页面通过 API 客户端调用后端 /api/v1/auth/* 接口 +- 后端服务层完成密码哈希/校验与 JWT 签发 +- 依赖注入层解析 Bearer 令牌并解析当前用户 +- 前端 NextAuth 将后端返回的访问令牌存入本地会话(JWT 策略) + +```mermaid +sequenceDiagram +participant FE as "前端页面" +participant API as "API 客户端" +participant AUTH as "后端认证路由" +participant SVC as "认证服务" +participant DB as "数据库" +participant DEP as "依赖注入" +FE->>API : "提交登录/注册表单" +API->>AUTH : "POST /api/v1/auth/login 或 /register" +AUTH->>SVC : "authenticate_user/register_user" +SVC->>DB : "查询/插入用户" +DB-->>SVC : "返回用户/结果" +SVC-->>AUTH : "返回用户或令牌" +AUTH-->>API : "返回 {access_token, user}" +API-->>FE : "保存令牌并跳转" +FE->>DEP : "后续请求携带 Authorization : Bearer" +DEP->>SVC : "verify_token 解码" +SVC-->>DEP : "payload(sub)" +DEP->>DB : "按ID查询用户" +DB-->>DEP : "返回用户" +DEP-->>FE : "注入当前用户" +``` + +图表来源 +- [frontend/lib/api.ts:23-36](file://frontend/lib/api.ts#L23-L36) +- [backend/app/api/auth.py:13-43](file://backend/app/api/auth.py#L13-L43) +- [backend/app/services/auth.py:37-69](file://backend/app/services/auth.py#L37-L69) +- [backend/app/api/deps.py:16-43](file://backend/app/api/deps.py#L16-L43) +- [frontend/lib/auth.ts:13-32](file://frontend/lib/auth.ts#L13-L32) + +## 详细组件分析 + +### 用户模型与数据库表 +- 字段与类型 + - id: UUID 主键,默认生成 + - email: 字符串,唯一且非空 + - password_hash: 字符串,非空 + - name: 字符串,可空 + - plan: 字符串,默认 "free" + - max_queries: 整数,默认 5 + - is_active: 布尔,默认 true + - created_at/updated_at: 时间戳,默认当前时间,更新时同步 +- 约束与索引 + - 唯一约束:email + - 外键:查询与订阅表关联 users(id),级联删除 +- 关系 + - 一对多:用户 -> 查询、订阅 + +```mermaid +erDiagram +USERS { +uuid id PK +string email UK +string password_hash +string name +string plan +int max_queries +boolean is_active +timestamp created_at +timestamp updated_at +} +QUERIES { +uuid id PK +uuid user_id FK +string keyword +string target_brand +jsonb brand_aliases +jsonb platforms +string frequency +string status +timestamp last_queried_at +timestamp next_query_at +timestamp created_at +timestamp updated_at +} +SUBSCRIPTIONS { +uuid id PK +uuid user_id FK +string plan +string status +date start_date +date end_date +numeric amount +string payment_method +string payment_id +timestamp created_at +} +USERS ||--o{ QUERIES : "拥有" +USERS ||--o{ SUBSCRIPTIONS : "拥有" +``` + +图表来源 +- [backend/app/models/user.py:11-41](file://backend/app/models/user.py#L11-L41) +- [backend/alembic/versions/488d0bd5ab01_initial_migration.py:21-112](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L21-L112) + +章节来源 +- [backend/app/models/user.py:11-41](file://backend/app/models/user.py#L11-L41) +- [backend/alembic/versions/488d0bd5ab01_initial_migration.py:21-112](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L21-L112) + +### JWT 令牌生成与验证机制 +- 签名算法:HS256 +- 密钥来源:配置项 JWT_SECRET +- 过期时间:配置项 JWT_EXPIRE_HOURS(小时) +- 生成流程 + - 以用户 ID 作为 sub,加入过期时间,使用 HS256 签名编码 +- 验证流程 + - 使用相同密钥与算法解码,提取 sub(用户 ID),再按 ID 查询用户 +- 刷新策略 + - 代码中未实现刷新令牌机制;当前为一次性访问令牌,过期后需重新登录 + +```mermaid +flowchart TD +Start(["开始"]) --> Encode["构造载荷{sub, exp}
HS256 签名"] +Encode --> Token["生成 access_token"] +Token --> Verify["解码 access_token
校验算法与密钥"] +Verify --> ParseSub["提取 sub(用户ID)"] +ParseSub --> Lookup["按ID查询用户"] +Lookup --> Done(["结束"]) +``` + +图表来源 +- [backend/app/services/auth.py:24-34](file://backend/app/services/auth.py#L24-L34) +- [backend/app/api/deps.py:26-37](file://backend/app/api/deps.py#L26-L37) +- [backend/app/config.py:9-10](file://backend/app/config.py#L9-L10) + +章节来源 +- [backend/app/services/auth.py:24-34](file://backend/app/services/auth.py#L24-L34) +- [backend/app/api/deps.py:26-37](file://backend/app/api/deps.py#L26-L37) +- [backend/app/config.py:9-10](file://backend/app/config.py#L9-L10) + +### 用户注册流程 +- 输入校验:邮箱格式、密码最小长度、姓名长度 +- 去重检查:按邮箱查询,若已存在则抛出错误 +- 密码处理:bcrypt 哈希 +- 持久化:创建用户记录并提交事务 +- 返回:用户信息(不含敏感字段) + +```mermaid +flowchart TD +RStart(["注册入口"]) --> Validate["Pydantic 校验"] +Validate --> CheckDup["按邮箱查重"] +CheckDup --> Dup{"已存在?"} +Dup -- 是 --> Err["抛出错误"] +Dup -- 否 --> Hash["bcrypt 哈希密码"] +Hash --> Persist["创建用户并提交"] +Persist --> RDone(["返回用户信息"]) +Err --> RDone +``` + +图表来源 +- [backend/app/schemas/auth.py:7-11](file://backend/app/schemas/auth.py#L7-L11) +- [backend/app/services/auth.py:37-52](file://backend/app/services/auth.py#L37-L52) + +章节来源 +- [backend/app/schemas/auth.py:7-11](file://backend/app/schemas/auth.py#L7-L11) +- [backend/app/services/auth.py:37-52](file://backend/app/services/auth.py#L37-L52) +- [tests/test_auth.py:25-58](file://tests/test_auth.py#L25-L58) + +### 登录认证过程 +- 输入校验:邮箱与密码 +- 用户查找:按邮箱查询 +- 凭据验证:bcrypt 校验密码哈希 +- 会话管理:签发访问令牌(Bearer) +- 权限分配:当前用户解析后,后续路由可依赖注入获取用户 + +```mermaid +sequenceDiagram +participant C as "客户端" +participant A as "认证路由" +participant S as "认证服务" +participant DB as "数据库" +C->>A : "POST /login {email,password}" +A->>S : "authenticate_user(email,password)" +S->>DB : "select * from users where email=?" +DB-->>S : "用户或空" +S->>S : "bcrypt 校验" +S-->>A : "用户或None" +A-->>C : "{access_token,bearer,user}" +``` + +图表来源 +- [backend/app/api/auth.py:22-37](file://backend/app/api/auth.py#L22-L37) +- [backend/app/services/auth.py:55-69](file://backend/app/services/auth.py#L55-L69) + +章节来源 +- [backend/app/api/auth.py:22-37](file://backend/app/api/auth.py#L22-L37) +- [backend/app/services/auth.py:55-69](file://backend/app/services/auth.py#L55-L69) +- [tests/test_auth.py:61-84](file://tests/test_auth.py#L61-L84) + +### 依赖注入与当前用户获取 +- OAuth2 密码流:tokenUrl 指向 /api/v1/auth/login +- 依赖函数 get_current_user: + - 从 Authorization 头解析 Bearer 令牌 + - 调用 verify_token 解码 + - 提取 sub(UUID),查询用户 + - 不存在或解码失败返回未授权 +- 在路由中使用:Depends(get_current_user) 获取当前用户 + +```mermaid +flowchart TD +DS(["依赖注入入口"]) --> Parse["OAuth2 解析 Bearer"] +Parse --> Decode["verify_token 解码"] +Decode --> Sub{"sub 是否存在?"} +Sub -- 否 --> Unauth["401 未授权"] +Sub -- 是 --> Query["按ID查询用户"] +Query --> Found{"找到用户?"} +Found -- 否 --> Unauth +Found -- 是 --> Inject["注入当前用户"] +Inject --> Done(["下游路由可用"]) +Unauth --> Done +``` + +图表来源 +- [backend/app/api/deps.py:16-43](file://backend/app/api/deps.py#L16-L43) +- [backend/app/services/auth.py:32-34](file://backend/app/services/auth.py#L32-L34) + +章节来源 +- [backend/app/api/deps.py:16-43](file://backend/app/api/deps.py#L16-L43) +- [backend/app/api/auth.py:40-42](file://backend/app/api/auth.py#L40-L42) +- [tests/test_auth.py:87-104](file://tests/test_auth.py#L87-L104) + +### 前端集成与会话管理 +- NextAuth 配置 + - 凭据提供者:向后端发送邮箱/密码 + - authorize:调用后端登录接口,成功则将 access_token 与用户信息写入 JWT + - 会话策略:jwt + - 回调:将 access_token 与用户 ID 写入 token/session +- 页面交互 + - 登录页:收集邮箱/密码,调用 signIn("credentials"),错误提示“邮箱或密码错误” + - 注册页:校验两次密码一致,调用后端注册,随后自动登录 +- API 客户端 + - 自动在请求头添加 Authorization: Bearer token + - 统一错误处理与响应解析 + +```mermaid +sequenceDiagram +participant L as "登录页" +participant NA as "NextAuth" +participant API as "后端登录" +participant CL as "API 客户端" +L->>NA : "signIn('credentials',{email,password})" +NA->>API : "POST /api/v1/auth/login" +API-->>NA : "{access_token,user}" +NA->>NA : "回调写入 token.accessToken/token.id" +NA-->>L : "登录成功,跳转仪表盘" +L->>CL : "后续请求携带 Authorization : Bearer" +``` + +图表来源 +- [frontend/app/(auth)/login/page.tsx:26-42](file://frontend/app/(auth)/login/page.tsx#L26-L42) +- [frontend/app/(auth)/register/page.tsx:29-55](file://frontend/app/(auth)/register/page.tsx#L29-L55) +- [frontend/lib/auth.ts:13-32](file://frontend/lib/auth.ts#L13-L32) +- [frontend/lib/api.ts:12-21](file://frontend/lib/api.ts#L12-L21) +- [frontend/app/api/auth/[...nextauth]/route.ts:1-6](file://frontend/app/api/auth/[...nextauth]/route.ts#L1-L6) + +章节来源 +- [frontend/lib/auth.ts:5-56](file://frontend/lib/auth.ts#L5-L56) +- [frontend/app/(auth)/login/page.tsx:19-93](file://frontend/app/(auth)/login/page.tsx#L19-L93) +- [frontend/app/(auth)/register/page.tsx:20-128](file://frontend/app/(auth)/register/page.tsx#L20-L128) +- [frontend/lib/api.ts:23-36](file://frontend/lib/api.ts#L23-L36) +- [frontend/app/api/auth/[...nextauth]/route.ts:1-6](file://frontend/app/api/auth/[...nextauth]/route.ts#L1-L6) + +## 依赖分析 +- 组件耦合 + - API 层依赖服务层与数据库依赖注入 + - 服务层依赖配置与密码/JWT工具 + - 依赖注入层依赖服务层的令牌验证与数据库查询 +- 外部依赖 + - 数据库:PostgreSQL(异步驱动) + - 缓存:Redis(配置项存在,当前认证未直接使用) + - 加密:bcrypt(passlib) + - JWT:jose(python-jose) +- 可能的循环依赖 + - 当前模块间无明显循环导入 + +```mermaid +graph LR +API["api/auth.py"] --> SVC["services/auth.py"] +API --> DB["database.py"] +DEP["api/deps.py"] --> SVC +DEP --> DB +SVC --> CFG["config.py"] +SVC --> DB +MAIN["main.py"] --> API +MAIN --> DEP +``` + +图表来源 +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/app/api/deps.py:1-43](file://backend/app/api/deps.py#L1-L43) +- [backend/app/services/auth.py:1-69](file://backend/app/services/auth.py#L1-L69) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) + +章节来源 +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/app/api/deps.py:1-43](file://backend/app/api/deps.py#L1-L43) +- [backend/app/services/auth.py:1-69](file://backend/app/services/auth.py#L1-L69) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) + +## 性能考虑 +- 数据库连接 + - 使用异步引擎与会话工厂,减少阻塞 + - 会话配置:关闭自动提交/刷写,expire_on_commit 控制回话生命周期 +- 密码哈希 + - bcrypt 默认成本适中,建议在生产环境评估成本参数 +- JWT + - HS256 为对称算法,计算开销低;注意密钥安全与轮换 +- 网络与缓存 + - Redis 配置存在但未在认证中使用,可考虑用于令牌黑名单或会话缓存(需额外实现) + +## 故障排查指南 +- 常见错误与定位 + - 注册重复邮箱:后端抛出值错误,测试断言 400 与错误详情 + - 登录凭据错误:后端返回 401 未授权,测试断言错误详情 + - 未携带或无效令牌:依赖注入解析失败,返回 401 +- 前端登录失败 + - 检查 NextAuth authorize 是否收到 access_token + - 确认后端 /api/v1/auth/login 返回结构 +- API 请求失败 + - 检查 Authorization 头是否正确附加 Bearer 令牌 + - 统一错误处理会将后端错误消息透传 + +章节来源 +- [tests/test_auth.py:25-84](file://tests/test_auth.py#L25-L84) +- [backend/app/api/auth.py:17-30](file://backend/app/api/auth.py#L17-L30) +- [backend/app/api/deps.py:20-41](file://backend/app/api/deps.py#L20-L41) +- [frontend/lib/api.ts:16-21](file://frontend/lib/api.ts#L16-L21) + +## 结论 +本认证系统采用清晰的分层设计:模式层负责输入校验,服务层封装密码与令牌逻辑,API 层提供 REST 接口,依赖注入统一解析当前用户。前端通过 NextAuth 与后端无缝衔接,形成完整的登录/注册与会话管理闭环。当前实现具备良好的扩展性,建议在生产环境中强化令牌刷新、黑名单与密钥轮换策略,并结合 Redis 实现更完善的会话与令牌治理。 + +## 附录 +- 安全最佳实践 + - 密钥管理:确保 JWT_SECRET 不泄露,定期轮换 + - 传输安全:启用 HTTPS,避免明文传输 + - 输入校验:前端与后端双重校验,防止越界与注入 + - 令牌策略:考虑引入刷新令牌与黑名单机制 + - 会话策略:前端 JWT 策略简单可靠,建议配合 HttpOnly Cookie 与 SameSite 策略进一步加固 +- 令牌存储策略 + - 前端:NextAuth JWT 存储在浏览器会话中 + - 后端:当前未实现服务端会话存储,建议结合 Redis 实现 +- 权限检查 + - 当前仅实现“当前用户”解析;如需细粒度权限,可在服务层增加角色/资源权限映射并在路由中校验 \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/开发指南/代码规范.md b/.qoder/repowiki/zh/content/开发指南/代码规范.md new file mode 100644 index 0000000..a080a4c --- /dev/null +++ b/.qoder/repowiki/zh/content/开发指南/代码规范.md @@ -0,0 +1,329 @@ +# 代码规范 + + +**本文档引用的文件** +- [backend/requirements.txt](file://backend/requirements.txt) +- [backend/alembic.ini](file://backend/alembic.ini) +- [backend/app/main.py](file://backend/app/main.py) +- [backend/app/config.py](file://backend/app/config.py) +- [backend/app/api/auth.py](file://backend/app/api/auth.py) +- [backend/app/schemas/auth.py](file://backend/app/schemas/auth.py) +- [backend/app/models/user.py](file://backend/app/models/user.py) +- [backend/app/workers/citation_engine.py](file://backend/app/workers/citation_engine.py) +- [tests/test_auth.py](file://tests/test_auth.py) +- [frontend/package.json](file://frontend/package.json) +- [.eslintrc.json](file://frontend/.eslintrc.json) +- [frontend/tsconfig.json](file://frontend/tsconfig.json) +- [frontend/app/layout.tsx](file://frontend/app/layout.tsx) +- [frontend/lib/api.ts](file://frontend/lib/api.ts) +- [frontend/components/ui/button.tsx](file://frontend/components/ui/button.tsx) +- [frontend/types/next-auth.d.ts](file://frontend/types/next-auth.d.ts) + + +## 目录 +1. [引言](#引言) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖分析](#依赖分析) +7. [性能考虑](#性能考虑) +8. [故障排查指南](#故障排查指南) +9. [结论](#结论) +10. [附录](#附录) + +## 引言 +本文件为 GEO 项目的统一代码规范文档,覆盖后端 Python 与前端 TypeScript/Next.js 的风格与实践,明确命名约定、导入顺序、注释规范、类型与接口设计、模块组织、错误处理模式以及前后端一致性要求,并提供可操作的自动化检查工具配置与使用方法。文档同时给出“正确/错误”示例的路径指引,便于团队在开发过程中快速对照与改进。 + +## 项目结构 +- 后端采用 FastAPI + SQLAlchemy 2.x + Pydantic v2,使用异步数据库会话与依赖注入,模块按功能分层:API 路由、服务层、模型与模式(schemas)、工作流与调度。 +- 前端采用 Next.js App Router + TypeScript + TailwindCSS,UI 组件基于 Radix UI 与 class-variance-authority,类型扩展用于 NextAuth。 + +```mermaid +graph TB +subgraph "后端" +MAIN["app/main.py
应用入口与路由挂载"] +AUTH_API["app/api/auth.py
认证路由"] +SCHEMA_AUTH["app/schemas/auth.py
认证模式"] +MODEL_USER["app/models/user.py
用户模型"] +WORKER["app/workers/citation_engine.py
引用检测引擎"] +CONFIG["app/config.py
配置读取"] +end +subgraph "前端" +LAYOUT["app/layout.tsx
根布局与元数据"] +API_TS["lib/api.ts
API 封装"] +BTN["components/ui/button.tsx
按钮组件"] +TYPES["types/next-auth.d.ts
NextAuth 类型扩展"] +end +MAIN --> AUTH_API +AUTH_API --> SCHEMA_AUTH +AUTH_API --> MODEL_USER +MAIN --> WORKER +MAIN --> CONFIG +LAYOUT --> API_TS +API_TS --> AUTH_API +BTN --> LAYOUT +TYPES --> LAYOUT +``` + +图示来源 +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/app/schemas/auth.py:1-34](file://backend/app/schemas/auth.py#L1-L34) +- [backend/app/models/user.py:1-41](file://backend/app/models/user.py#L1-L41) +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [frontend/app/layout.tsx:1-37](file://frontend/app/layout.tsx#L1-L37) +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) +- [frontend/components/ui/button.tsx:1-57](file://frontend/components/ui/button.tsx#L1-L57) +- [frontend/types/next-auth.d.ts:1-26](file://frontend/types/next-auth.d.ts#L1-L26) + +章节来源 +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [frontend/app/layout.tsx:1-37](file://frontend/app/layout.tsx#L1-L37) + +## 核心组件 +- 应用入口与中间件:统一设置 CORS、健康检查端点、路由挂载与生命周期管理。 +- 认证模块:注册、登录、当前用户信息获取,配合 Pydantic 模式与数据库模型。 +- 引擎与工作流:引用检测引擎封装品牌匹配、竞争品牌识别与多平台适配器调用。 +- 前端 API 封装:统一鉴权头、错误处理与资源访问;UI 组件通过变体系统实现一致风格。 +- 类型与扩展:NextAuth 类型扩展确保会话与 JWT 字段的类型安全。 + +章节来源 +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/app/schemas/auth.py:1-34](file://backend/app/schemas/auth.py#L1-L34) +- [backend/app/models/user.py:1-41](file://backend/app/models/user.py#L1-L41) +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) +- [frontend/types/next-auth.d.ts:1-26](file://frontend/types/next-auth.d.ts#L1-L26) + +## 架构总览 +后端通过 FastAPI 提供 REST 接口,前端通过 Next.js App Router 渲染页面并通过封装的 API 客户端访问后端。测试覆盖认证流程与错误场景,确保行为符合预期。 + +```mermaid +sequenceDiagram +participant FE as "前端页面" +participant API as "lib/api.ts" +participant BE as "FastAPI 路由" +participant SVC as "服务/模型" +FE->>API : "调用认证/查询/报告接口" +API->>BE : "携带 Authorization 头发起请求" +BE->>SVC : "依赖注入与业务处理" +SVC-->>BE : "返回响应或异常" +BE-->>API : "标准化响应" +API-->>FE : "解析并渲染结果" +``` + +图示来源 +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) + +## 详细组件分析 + +### Python 后端代码规范 +- 语言与版本:使用 Python 3.x,推荐 3.10+。 +- 依赖管理:通过 requirements.txt 管理生产与测试依赖。 +- 代码风格与静态检查:Alembic 配置中预留了格式化与检查钩子(如 black/ruff),建议在本地与 CI 中启用以保持一致性。 +- 命名约定 + - 模块与包:小写、下划线分隔(如 app/api/auth.py)。 + - 类:大驼峰(如 User、CitationEngine)。 + - 函数与变量:小写加下划线(如 register_user、execute_query)。 + - 常量:全大写加下划线(如 DATABASE_URL)。 +- 导入顺序 + - 标准库 → 第三方库 → 项目内相对导入(遵循相对导入与绝对导入的一致性)。 + - FastAPI 路由模块中,先导入标准库与第三方,再导入项目内模块。 +- 注释规范 + - 模块级 docstring:简述模块职责。 + - 类与函数:使用三引号 docstring,描述参数、返回值与异常。 + - 关键逻辑处添加行内注释,解释复杂算法或边界条件。 +- 错误处理 + - 使用 HTTPException 抛出语义化错误,避免泄露内部细节。 + - 服务层捕获业务异常并转换为 HTTP 状态码。 +- 类型与数据模型 + - 使用 Pydantic v2 BaseModel 定义输入输出模式,配合 EmailStr、Field 等约束。 + - SQLAlchemy ORM 模型使用 mapped_column、mapped_alias 等现代语法。 +- 并发与异步 + - 使用 AsyncSession 与 async/await,避免阻塞操作。 +- 示例路径 + - 正确:[认证路由与异常处理:1-43](file://backend/app/api/auth.py#L1-L43) + - 正确:[Pydantic 模式定义:1-34](file://backend/app/schemas/auth.py#L1-L34) + - 正确:[SQLAlchemy 模型字段与关系:1-41](file://backend/app/models/user.py#L1-L41) + - 正确:[引擎类与方法注释:1-309](file://backend/app/workers/citation_engine.py#L1-L309) + +章节来源 +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) +- [backend/alembic.ini:92-114](file://backend/alembic.ini#L92-L114) +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/app/schemas/auth.py:1-34](file://backend/app/schemas/auth.py#L1-L34) +- [backend/app/models/user.py:1-41](file://backend/app/models/user.py#L1-L41) +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) + +### TypeScript/JavaScript 前端代码规范 +- 语言与编译选项:严格模式、ESNext 模块解析、Bundler 解析、JSX 保留。 +- 命名约定 + - 文件:小写加连字符(如 api.ts),组件使用 PascalCase(如 Button)。 + - 变量与函数:小驼峰(如 fetchWithAuth)。 + - 类型:接口与类型别名使用大驼峰(如 ButtonProps)。 +- 导入顺序 + - 先标准库与第三方,再内部路径别名(如 @/*)。 +- 注释规范 + - 函数与接口:使用 JSDoc 风格注释,说明参数、返回值与异常。 + - 复杂逻辑处补充行内注释。 +- 类型定义与接口设计 + - 使用 TypeScript 接口与联合类型表达数据结构。 + - NextAuth 类型扩展确保会话与 JWT 字段类型安全。 +- 模块组织 + - API 客户端集中于 lib/api.ts,按领域拆分子模块。 + - UI 组件位于 components/ui,通过变体系统统一风格。 +- 错误处理模式 + - 统一的 fetchWithAuth 封装:校验响应状态、提取错误详情并抛出错误。 + - 页面侧通过 try/catch 或 Promise.catch 捕获并提示。 +- 示例路径 + - 正确:[TS 编译配置:1-27](file://frontend/tsconfig.json#L1-L27) + - 正确:[ESLint 扩展:1-4](file://frontend/.eslintrc.json#L1-L4) + - 正确:[API 客户端封装与错误处理:1-58](file://frontend/lib/api.ts#L1-58) + - 正确:[UI 组件变体系统:1-57](file://frontend/components/ui/button.tsx#L1-57) + - 正确:[NextAuth 类型扩展:1-26](file://frontend/types/next-auth.d.ts#L1-26) + +章节来源 +- [frontend/tsconfig.json:1-27](file://frontend/tsconfig.json#L1-L27) +- [frontend/.eslintrc.json:1-4](file://frontend/.eslintrc.json#L1-L4) +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) +- [frontend/components/ui/button.tsx:1-57](file://frontend/components/ui/button.tsx#L1-L57) +- [frontend/types/next-auth.d.ts:1-26](file://frontend/types/next-auth.d.ts#L1-L26) + +### 前后端一致性要求 +- 接口契约 + - 请求与响应字段命名保持一致(如 email、password、access_token)。 + - 错误响应结构统一(包含 detail 字段)。 +- 鉴权与头部 + - 前端统一在 Authorization 头中携带 Bearer Token。 + - 后端路由对未认证请求返回 401。 +- 资源路径 + - 前端 API 基础地址通过环境变量控制,与后端路由前缀保持一致。 +- 示例路径 + - 正确:[后端路由前缀与标签:38-42](file://backend/app/main.py#L38-L42) + - 正确:[前端 API 基础地址与鉴权头:1-21](file://frontend/lib/api.ts#L1-L21) + +章节来源 +- [backend/app/main.py:38-42](file://backend/app/main.py#L38-L42) +- [frontend/lib/api.ts:1-21](file://frontend/lib/api.ts#L1-L21) + +### 自动化代码检查工具配置与使用 +- 后端 + - Alembic 配置预留了 post_write_hooks,可集成 black 与 ruff 进行格式化与检查。 + - 建议在本地与 CI 中启用:格式化(black)与静态检查(ruff/flake8/pylint)。 + - 依赖声明参考 requirements.txt,确保工具链可用。 +- 前端 + - ESLint 通过 .eslintrc.json 扩展 next/core-web-vitals 与 next/typescript。 + - 建议在本地与 CI 中运行 lint 脚本,修复问题后再提交。 + - TS 编译配置启用 strict 模式,提升类型安全性。 +- 示例路径 + - 正确:[Alembic hooks 配置:92-114](file://backend/alembic.ini#L92-L114) + - 正确:[ESLint 扩展:1-4](file://frontend/.eslintrc.json#L1-L4) + - 正确:[TS 严格模式与模块解析:6-12](file://frontend/tsconfig.json#L6-L12) + - 正确:[依赖声明:1-35](file://backend/requirements.txt#L1-L35) + +章节来源 +- [backend/alembic.ini:92-114](file://backend/alembic.ini#L92-L114) +- [frontend/.eslintrc.json:1-4](file://frontend/.eslintrc.json#L1-L4) +- [frontend/tsconfig.json:6-12](file://frontend/tsconfig.json#L6-L12) +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) + +## 依赖分析 +- 后端依赖 + - Web 框架:FastAPI、Uvicorn + - 数据库:SQLAlchemy 2.x、Asyncpg、Alembic + - 数据验证与配置:Pydantic v2、Pydantic-Settings、email-validator + - 认证与安全:python-jose、passlib、multipart + - 缓存与任务调度:Redis、APScheduler + - 浏览器自动化:Playwright + - HTTP 客户端与工具:HTTPX、python-dotenv + - 测试:pytest、pytest-asyncio、aiosqlite +- 前端依赖 + - 运行时:Next.js、React、NextAuth + - UI:Radix UI、Recharts、TailwindCSS + - 开发工具:ESLint、TypeScript、PostCSS、TailwindCSS + +```mermaid +graph LR +subgraph "后端" +FASTAPI["FastAPI"] +SQLA["SQLAlchemy 2.x"] +PYD["Pydantic v2"] +REDIS["Redis"] +APS["APScheduler"] +PW["Playwright"] +HTTPX["HTTPX"] +PYTEST["pytest"] +end +subgraph "前端" +NEXT["Next.js"] +REACT["React"] +NA["NextAuth"] +RUI["Radix UI"] +REC["Recharts"] +TSC["TypeScript"] +ESL["ESLint"] +end +FASTAPI --> SQLA +FASTAPI --> PYD +FASTAPI --> REDIS +FASTAPI --> APS +FASTAPI --> HTTPX +NEXT --> REACT +NEXT --> NA +NEXT --> RUI +NEXT --> REC +NEXT --> TSC +NEXT --> ESL +``` + +图示来源 +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) +- [frontend/package.json:1-40](file://frontend/package.json#L1-L40) + +章节来源 +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) +- [frontend/package.json:1-40](file://frontend/package.json#L1-L40) + +## 性能考虑 +- 异步优先:后端使用 AsyncSession 与异步 I/O,避免阻塞事件循环。 +- 任务调度:APScheduler 合理设置频率与并发,避免重复任务与资源争用。 +- 缓存策略:Redis 用于热点数据缓存与会话存储,减少数据库压力。 +- 前端渲染:Next.js App Router 与静态生成/预渲染结合,降低首屏延迟。 +- 网络请求:统一的 API 客户端封装,避免重复请求与无谓重试。 + +## 故障排查指南 +- 认证失败 + - 检查前端是否正确传递 Authorization 头。 + - 检查后端路由是否正确处理 401 场景。 + - 参考测试用例断言与错误消息。 +- 数据库连接 + - 确认 DATABASE_URL 与容器网络可达。 + - 检查 Alembic 迁移是否成功。 +- 任务执行异常 + - 查看日志与 QueryTask 状态更新,定位具体平台适配器问题。 +- 前端类型错误 + - 启用 TS 严格模式,修复类型不匹配。 + - 使用 ESLint 规则约束代码质量。 + +章节来源 +- [tests/test_auth.py:1-104](file://tests/test_auth.py#L1-L104) +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/app/workers/citation_engine.py:211-228](file://backend/app/workers/citation_engine.py#L211-L228) +- [frontend/lib/api.ts:16-21](file://frontend/lib/api.ts#L16-L21) + +## 结论 +本规范明确了 GEO 项目在 Python 与 TypeScript 生态下的风格与实践,强调一致性、可维护性与可测试性。建议在本地与 CI 中强制执行格式化与静态检查,持续完善测试覆盖,确保前后端契约稳定与类型安全。 + +## 附录 +- 快速检查清单 + - 后端:使用 Alembic hooks 格式化与检查;Pydantic 字段约束完整;HTTP 异常语义化;异步会话正确使用。 + - 前端:ESLint 通过;TS 严格模式开启;UI 组件统一变体;API 客户端统一错误处理;NextAuth 类型扩展齐全。 +- 示例路径(正确/错误对照) + - 正确:[认证路由与异常处理:1-43](file://backend/app/api/auth.py#L1-L43) + - 正确:[API 客户端封装与错误处理:1-58](file://frontend/lib/api.ts#L1-58) + - 正确:[NextAuth 类型扩展:1-26](file://frontend/types/next-auth.d.ts#L1-26) \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/开发指南/开发工具.md b/.qoder/repowiki/zh/content/开发指南/开发工具.md new file mode 100644 index 0000000..d2cf486 --- /dev/null +++ b/.qoder/repowiki/zh/content/开发指南/开发工具.md @@ -0,0 +1,360 @@ +# 开发工具 + + +**本文档引用的文件** +- [docker-compose.yml](file://docker-compose.yml) +- [backend/Dockerfile](file://backend/Dockerfile) +- [frontend/Dockerfile](file://frontend/Dockerfile) +- [backend/requirements.txt](file://backend/requirements.txt) +- [frontend/package.json](file://frontend/package.json) +- [backend/app/main.py](file://backend/app/main.py) +- [backend/app/config.py](file://backend/app/config.py) +- [backend/alembic.ini](file://backend/alembic.ini) +- [backend/alembic/env.py](file://backend/alembic/env.py) +- [backend/app/api/auth.py](file://backend/app/api/auth.py) +- [backend/app/workers/scheduler.py](file://backend/app/workers/scheduler.py) +- [frontend/lib/api.ts](file://frontend/lib/api.ts) +- [frontend/tsconfig.json](file://frontend/tsconfig.json) +- [frontend/.eslintrc.json](file://frontend/.eslintrc.json) +- [frontend/next.config.mjs](file://frontend/next.config.mjs) + + +## 目录 +1. [简介](#简介) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖分析](#依赖分析) +7. [性能考虑](#性能考虑) +8. [故障排查指南](#故障排查指南) +9. [结论](#结论) +10. [附录](#附录) + +## 简介 +本文件面向GEO项目的开发者,提供从IDE配置、调试工具、开发辅助工具到命令行与脚本使用的完整指南。内容覆盖: +- VS Code配置与推荐插件(Python、TypeScript/Next.js) +- 断点调试、日志分析与性能分析方法 +- API测试、数据库管理与Docker容器管理 +- 命令行工具与脚本使用 +- 开发环境优化与常见问题排查 + +## 项目结构 +GEO采用前后端分离的多容器架构,通过Docker Compose编排数据库、缓存、后端服务与前端开发服务器。后端基于FastAPI,前端基于Next.js(App Router),数据库迁移使用Alembic。 + +```mermaid +graph TB +subgraph "本地开发环境" +VSCode["VS Code 开发环境"] +Postman["API 测试工具"] +DBTools["数据库管理工具"] +DockerCLI["Docker CLI"] +end +subgraph "容器编排" +DC["docker-compose.yml"] +DB["Postgres 容器"] +Redis["Redis 容器"] +BE["后端容器"] +FE["前端容器"] +end +VSCode --> DC +Postman --> BE +DBTools --> DB +DockerCLI --> DC +DC --> DB +DC --> Redis +DC --> BE +DC --> FE +``` + +图表来源 +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) + +章节来源 +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) +- [backend/Dockerfile:1-41](file://backend/Dockerfile#L1-L41) +- [frontend/Dockerfile:1-15](file://frontend/Dockerfile#L1-L15) + +## 核心组件 +- 后端服务(FastAPI) + - 应用入口与生命周期管理、CORS配置、路由注册与健康检查接口 + - 配置加载与外部服务密钥(数据库、Redis、JWT、浏览器路径等) +- 数据库迁移(Alembic) + - 异步迁移环境、日志级别与SQLAlchemy连接配置 +- 前端应用(Next.js App Router) + - API封装、类型配置、ESLint规则与构建配置 +- 定时任务调度器(APScheduler + Playwright) + - 异步调度、周期性查询检查与执行 + +章节来源 +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [backend/alembic/env.py:1-89](file://backend/alembic/env.py#L1-L89) +- [backend/alembic.ini:1-150](file://backend/alembic.ini#L1-L150) +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) +- [frontend/tsconfig.json:1-27](file://frontend/tsconfig.json#L1-L27) +- [frontend/.eslintrc.json:1-4](file://frontend/.eslintrc.json#L1-L4) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) + +## 架构总览 +下图展示本地开发与容器化运行时的交互关系,以及各组件在容器内的端口映射与依赖顺序。 + +```mermaid +graph TB +Dev["开发者主机"] +subgraph "Docker Compose 编排" +subgraph "服务" +DB["db:5432 → POSTGRES"] +RD["redis:6379 → 缓存"] +BE["backend:8000 → FastAPI"] +FE["frontend:3000 → Next.js dev"] +end +end +Dev --> |"浏览器访问"| FE +Dev --> |"API 请求"| BE +BE --> |"数据库/缓存"| DB +BE --> |"缓存/队列"| RD +FE --> |"调用后端 API"| BE +``` + +图表来源 +- [docker-compose.yml:3-66](file://docker-compose.yml#L3-L66) + +章节来源 +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) + +## 详细组件分析 + +### 后端服务(FastAPI)调试与配置 +- 应用入口与生命周期 + - 使用生命周期钩子初始化模型与启动调度器,并在关闭时优雅停机 + - 注册认证、查询、引用、报告等路由前缀与标签 +- CORS策略 + - 允许来自前端开发地址的跨域请求 +- 健康检查 + - 提供基础健康检查端点用于容器健康探测 + +```mermaid +sequenceDiagram +participant Client as "客户端" +participant FE as "前端(3000)" +participant API as "后端(8000)" +participant DB as "数据库" +participant Cache as "缓存" +Client->>FE : "打开应用" +FE->>API : "发起登录/查询/报告等请求" +API->>DB : "读写数据(异步ORM)" +API->>Cache : "读写缓存(可选)" +API-->>FE : "返回JSON响应" +FE-->>Client : "渲染界面" +``` + +图表来源 +- [backend/app/main.py:13-47](file://backend/app/main.py#L13-L47) +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) + +章节来源 +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) + +### 数据库迁移(Alembic)流程 +- 运行模式 + - 支持离线与在线两种迁移模式,使用异步引擎连接数据库 +- 日志配置 + - 控制根日志、SQLAlchemy引擎与Alembic的日志级别 +- 数据库URL + - 默认指向本地或容器内Postgres服务 + +```mermaid +flowchart TD +Start(["启动迁移"]) --> Mode{"选择模式"} +Mode --> |离线| Offline["使用配置URL直接迁移"] +Mode --> |在线| Online["创建异步引擎并连接"] +Offline --> Tx["开启事务并执行迁移"] +Online --> Tx +Tx --> Done(["完成"]) +``` + +图表来源 +- [backend/alembic/env.py:33-88](file://backend/alembic/env.py#L33-L88) +- [backend/alembic.ini:115-150](file://backend/alembic.ini#L115-L150) + +章节来源 +- [backend/alembic/env.py:1-89](file://backend/alembic/env.py#L1-L89) +- [backend/alembic.ini:1-150](file://backend/alembic.ini#L1-L150) + +### 前端(Next.js App Router)配置要点 +- TypeScript与路径别名 + - 严格类型检查、Bundler模块解析、路径别名配置 +- ESLint规则 + - 继承Next.js核心Web Vitals与TypeScript规则 +- 构建与开发 + - 开发服务器端口、构建产物与类型生成 + +```mermaid +flowchart TD +TS["tsconfig.json
严格类型/路径别名"] --> ESL[".eslintrc.json
继承Next规则"] +ESL --> Build["next.config.mjs
默认配置"] +Build --> Dev["npm run dev
3000端口"] +``` + +图表来源 +- [frontend/tsconfig.json:1-27](file://frontend/tsconfig.json#L1-L27) +- [frontend/.eslintrc.json:1-4](file://frontend/.eslintrc.json#L1-L4) +- [frontend/next.config.mjs:1-5](file://frontend/next.config.mjs#L1-L5) + +章节来源 +- [frontend/tsconfig.json:1-27](file://frontend/tsconfig.json#L1-L27) +- [frontend/.eslintrc.json:1-4](file://frontend/.eslintrc.json#L1-L4) +- [frontend/next.config.mjs:1-5](file://frontend/next.config.mjs#L1-L5) + +### 定时任务调度器(APScheduler + Playwright) +- 调度策略 + - 每小时触发一次检查,筛选到期的活跃查询并交由引文引擎执行 +- 异常处理 + - 单条任务失败不影响整体调度;关闭时优雅停止调度器与浏览器资源 + +```mermaid +classDiagram +class QueryScheduler { ++start() ++check_and_execute_queries() ++shutdown() +-_run_check() +-_execute_single_query(query, db) +} +class CitationEngine { ++execute_query(query, db) ++close() +} +QueryScheduler --> CitationEngine : "执行查询" +``` + +图表来源 +- [backend/app/workers/scheduler.py:25-95](file://backend/app/workers/scheduler.py#L25-L95) + +章节来源 +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) + +## 依赖分析 +- 后端依赖 + - Web框架、异步数据库、序列化、认证、缓存、任务调度、浏览器自动化、HTTP客户端、环境变量、测试工具 +- 前端依赖 + - Next.js、React、UI库、Tailwind、TypeScript与开发工具链 + +```mermaid +graph LR +BE["后端应用"] --> Req["requirements.txt"] +FE["前端应用"] --> Pkg["package.json"] +Req --> FastAPI["FastAPI"] +Req --> SQLA["SQLAlchemy/AsyncPG/Alembic"] +Req --> RedisDep["Redis/APScheduler"] +Req --> PW["Playwright"] +Req --> HTTPX["HTTPX/HTTPX"] +Req --> Test["pytest/pytest-asyncio/aiosqlite"] +Pkg --> Next["Next.js"] +Pkg --> UI["@radix-ui/*"] +Pkg --> Tail["Tailwind/Recharts"] +Pkg --> TSDev["TypeScript/ESLint"] +``` + +图表来源 +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) +- [frontend/package.json:1-40](file://frontend/package.json#L1-L40) + +章节来源 +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) +- [frontend/package.json:1-40](file://frontend/package.json#L1-L40) + +## 性能考虑 +- 后端 + - 使用异步ORM与连接池,避免阻塞;合理设置任务调度间隔,避免频繁扫描 + - 浏览器自动化(Playwright)需预装浏览器二进制,减少重复下载开销 +- 前端 + - 开发模式启用快速刷新;生产构建建议启用最小化与Tree-shaking +- 数据库 + - 迁移日志级别按需调整,避免过度输出影响性能 + +## 故障排查指南 +- 容器健康与端口 + - 确认数据库与缓存容器健康检查通过,端口映射正确 + - 后端与前端容器分别监听8000与3000端口 +- 数据库连接 + - 检查数据库URL与凭据;确认Alembic配置与容器网络可达 +- 调试与日志 + - 后端生命周期钩子负责启动/关闭调度器;查看容器日志定位异常 + - 前端API封装统一处理错误响应,便于定位后端返回的错误详情 +- 定时任务 + - 若查询未按时执行,检查调度器状态与异常日志;确认UTC时间与时区一致性 + +章节来源 +- [docker-compose.yml:16-34](file://docker-compose.yml#L16-L34) +- [backend/app/main.py:13-21](file://backend/app/main.py#L13-L21) +- [frontend/lib/api.ts:16-21](file://frontend/lib/api.ts#L16-L21) +- [backend/app/workers/scheduler.py:42-90](file://backend/app/workers/scheduler.py#L42-L90) + +## 结论 +通过容器化编排与严格的前后端配置,GEO提供了可复现的开发环境。配合本文档提供的IDE配置、调试与运维工具清单,开发者可以高效地进行功能迭代与问题定位。 + +## 附录 + +### IDE配置与推荐插件(VS Code) +- Python相关 + - 扩展:Python、Pylance、Black、isort、pytest + - 设置:启用Pylance类型检查、自动格式化、导入排序 +- TypeScript/Next.js相关 + - 扩展:ESLint、TypeScript Importer、Tailwind CSS IntelliSense + - 设置:启用严格模式、路径别名、TS/TSX语法高亮 +- Docker + - 扩展:Docker、Kubernetes(可选) + - 用途:可视化容器状态、日志查看、镜像构建与部署 + +[本节为通用实践建议,不直接分析具体源码文件] + +### 调试工具使用方法 +- 断点调试 + - 后端:在FastAPI路由或业务逻辑处设置断点,结合容器调试或本地调试 + - 前端:在API封装函数与页面逻辑处设置断点,观察请求/响应 +- 日志分析 + - 后端:关注调度器与API层日志;容器日志中过滤关键字定位异常 + - 数据库:调整Alembic日志级别以获取更详细的迁移信息 +- 性能分析 + - 后端:对慢查询与任务执行耗时进行采样;评估浏览器自动化并发 + - 前端:利用Next.js开发服务器热重载与性能面板 + +[本节为通用实践建议,不直接分析具体源码文件] + +### 开发辅助工具 +- API测试 + - 工具:Postman、Insomnia、curl + - 建议:使用环境变量管理不同环境的API基础URL与令牌 +- 数据库管理 + - 工具:pgAdmin、DBeaver、DataGrip + - 建议:使用Alembic进行版本化迁移,避免手写DDL +- Docker容器管理 + - 工具:Docker Desktop、Lens(可选) + - 建议:使用Compose一键拉起所有服务,查看容器日志与健康状态 + +[本节为通用实践建议,不直接分析具体源码文件] + +### 命令行工具与脚本使用 +- 后端 + - 安装依赖:pip install -r requirements.txt + - 运行开发服务器:uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload + - 数据库迁移:alembic revision --autogenerate,alembic upgrade head +- 前端 + - 安装依赖:npm ci + - 开发:npm run dev(3000端口) + - 构建:npm run build(14.x) +- Docker + - 构建与启动:docker-compose up --build + - 查看日志:docker-compose logs -f 服务名 + - 进入容器:docker-compose exec 服务名 sh + +章节来源 +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) +- [backend/Dockerfile:28-33](file://backend/Dockerfile#L28-L33) +- [frontend/Dockerfile:6-7](file://frontend/Dockerfile#L6-L7) +- [docker-compose.yml:36-66](file://docker-compose.yml#L36-L66) \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/开发指南/开发指南.md b/.qoder/repowiki/zh/content/开发指南/开发指南.md new file mode 100644 index 0000000..2fcb08f --- /dev/null +++ b/.qoder/repowiki/zh/content/开发指南/开发指南.md @@ -0,0 +1,413 @@ +# 开发指南 + + +**本文档引用的文件** +- [backend/requirements.txt](file://backend/requirements.txt) +- [frontend/package.json](file://frontend/package.json) +- [backend/app/main.py](file://backend/app/main.py) +- [backend/Dockerfile](file://backend/Dockerfile) +- [frontend/Dockerfile](file://frontend/Dockerfile) +- [docker-compose.yml](file://docker-compose.yml) +- [backend/alembic.ini](file://backend/alembic.ini) +- [backend/app/config.py](file://backend/app/config.py) +- [frontend/tsconfig.json](file://frontend/tsconfig.json) +- [frontend/tailwind.config.ts](file://frontend/tailwind.config.ts) +- [frontend/.eslintrc.json](file://frontend/.eslintrc.json) +- [tests/conftest.py](file://tests/conftest.py) +- [backend/app/api/auth.py](file://backend/app/api/auth.py) +- [backend/app/schemas/auth.py](file://backend/app/schemas/auth.py) +- [backend/app/models/user.py](file://backend/app/models/user.py) + + +## 目录 +1. 引言 +2. 项目结构 +3. 核心组件 +4. 架构总览 +5. 详细组件分析 +6. 依赖分析 +7. 性能考虑 +8. 故障排查指南 +9. 结论 +10. 附录 + +## 引言 +本开发指南面向GEO项目的开发者,旨在统一前后端代码规范与最佳实践,明确开发流程与工作流(包括分支策略、代码评审与版本发布),并提供开发工具使用方法(IDE配置、调试与性能分析)、新功能开发指导原则(模块设计、接口定义与测试要求),以及常见问题的排查方案。本指南以仓库中现有实现为依据,确保内容可落地、可执行。 + +## 项目结构 +GEO采用前后端分离架构,后端基于FastAPI,前端基于Next.js,数据库使用PostgreSQL,缓存使用Redis,任务调度使用APScheduler,浏览器自动化使用Playwright。项目通过Docker与docker-compose进行容器化编排,便于本地开发与部署。 + +```mermaid +graph TB +subgraph "前端" +FE_Docker["frontend/Dockerfile"] +FE_Pkg["frontend/package.json"] +FE_TS["frontend/tsconfig.json"] +FE_ESLint[".eslintrc.json"] +FE_Tailwind["frontend/tailwind.config.ts"] +end +subgraph "后端" +BE_Docker["backend/Dockerfile"] +BE_Req["backend/requirements.txt"] +BE_Main["backend/app/main.py"] +BE_Config["backend/app/config.py"] +BE_Alembic["backend/alembic.ini"] +end +subgraph "基础设施" +DB["PostgreSQL 容器"] +REDIS["Redis 容器"] +DC["docker-compose.yml"] +end +FE_Docker --> FE_Pkg +FE_TS --> FE_ESLint +FE_Tailwind --> FE_Pkg +BE_Docker --> BE_Req +BE_Main --> BE_Config +BE_Alembic --> DB +DC --> FE_Docker +DC --> BE_Docker +DC --> DB +DC --> REDIS +``` + +图表来源 +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) +- [frontend/Dockerfile:1-15](file://frontend/Dockerfile#L1-L15) +- [backend/Dockerfile:1-41](file://backend/Dockerfile#L1-L41) +- [frontend/package.json:1-40](file://frontend/package.json#L1-L40) +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [backend/alembic.ini:1-150](file://backend/alembic.ini#L1-L150) + +章节来源 +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) +- [frontend/Dockerfile:1-15](file://frontend/Dockerfile#L1-L15) +- [backend/Dockerfile:1-41](file://backend/Dockerfile#L1-L41) +- [frontend/package.json:1-40](file://frontend/package.json#L1-L40) +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [backend/alembic.ini:1-150](file://backend/alembic.ini#L1-L150) + +## 核心组件 +- 后端服务入口:FastAPI应用在生命周期内启动查询调度器,并注册认证、查询词、引用数据、报告等路由。 +- 配置管理:使用Pydantic Settings加载环境变量,集中管理数据库、Redis、JWT、浏览器路径与第三方平台密钥等配置项。 +- 前端构建与运行:Next.js项目通过package.json脚本控制开发、构建与启动;TypeScript严格模式开启;ESLint规则继承Next.js核心Web Vitals与TypeScript默认规则;Tailwind CSS按需扫描组件与页面目录。 +- 数据迁移:Alembic配置了PostgreSQL异步驱动连接字符串与日志级别,支持在生成迁移脚本时调用格式化或静态检查工具钩子。 +- 测试基础:pytest会自动注入后端源码路径,提供模拟调度器、认证用户、依赖覆盖与异步HTTP客户端等测试夹具。 + +章节来源 +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [frontend/package.json:1-40](file://frontend/package.json#L1-L40) +- [frontend/tsconfig.json:1-27](file://frontend/tsconfig.json#L1-L27) +- [frontend/.eslintrc.json:1-4](file://frontend/.eslintrc.json#L1-L4) +- [frontend/tailwind.config.ts:1-57](file://frontend/tailwind.config.ts#L1-L57) +- [backend/alembic.ini:86-114](file://backend/alembic.ini#L86-L114) +- [tests/conftest.py:1-71](file://tests/conftest.py#L1-L71) + +## 架构总览 +下图展示了从浏览器到后端API再到数据库与缓存的整体交互路径,以及容器化编排关系。 + +```mermaid +graph TB +Browser["浏览器/移动端"] +NextApp["Next.js 应用
frontend/"] +FastAPI["FastAPI 应用
backend/app/main.py"] +Postgres["PostgreSQL
geo_platform"] +Redis["Redis"] +Playwright["Playwright 浏览器"] +Browser --> NextApp +NextApp --> FastAPI +FastAPI --> Postgres +FastAPI --> Redis +FastAPI --> Playwright +``` + +图表来源 +- [backend/app/main.py:24-47](file://backend/app/main.py#L24-L47) +- [backend/app/config.py:7-13](file://backend/app/config.py#L7-L13) +- [backend/Dockerfile:31-33](file://backend/Dockerfile#L31-L33) +- [docker-compose.yml:4-20](file://docker-compose.yml#L4-L20) +- [docker-compose.yml:22-34](file://docker-compose.yml#L22-L34) + +## 详细组件分析 + +### 后端服务与路由 +- 应用生命周期:在应用启动时导入模型并启动查询调度器,在关闭时优雅停止调度器。 +- 路由注册:认证、查询词、引用数据、报告等模块化路由按前缀挂载,便于版本化与职责划分。 +- 健康检查:提供/health端点返回服务状态。 + +```mermaid +sequenceDiagram +participant Client as "客户端" +participant App as "FastAPI 应用" +participant Scheduler as "查询调度器" +participant Router as "各模块路由" +Client->>App : "GET /health" +App-->>Client : "{status : ok}" +App->>Scheduler : "start()" +App->>Router : "include_router(...)" +Router-->>App : "注册完成" +``` + +图表来源 +- [backend/app/main.py:13-21](file://backend/app/main.py#L13-L21) +- [backend/app/main.py:38-42](file://backend/app/main.py#L38-L42) +- [backend/app/main.py:45-47](file://backend/app/main.py#L45-L47) + +章节来源 +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) + +### 认证模块与数据模型 +- 认证接口:注册、登录、当前用户信息读取,返回TokenResponse与UserResponse。 +- Pydantic模型:UserRegister、UserLogin、UserResponse、TokenResponse,约束字段类型与长度。 +- SQLAlchemy模型:User表包含邮箱唯一性、密码哈希、计划等级、配额、激活状态与时间戳等字段,并与Query、Subscription建立一对多关系。 + +```mermaid +classDiagram +class UserRegister { ++邮箱 ++密码(最小长度) ++姓名(长度范围) +} +class UserLogin { ++邮箱 ++密码 +} +class UserResponse { ++id(UUID) ++email ++name ++plan ++max_queries ++is_active ++created_at +} +class TokenResponse { ++access_token ++token_type ++user(UserResponse) +} +class User { ++id(UUID) ++email(唯一) ++password_hash ++name ++plan ++max_queries ++is_active ++created_at ++updated_at +} +UserRegister --> UserResponse : "注册输入" +UserLogin --> TokenResponse : "登录输出" +UserResponse --> User : "序列化自属性" +``` + +图表来源 +- [backend/app/schemas/auth.py:7-34](file://backend/app/schemas/auth.py#L7-L34) +- [backend/app/models/user.py:11-41](file://backend/app/models/user.py#L11-L41) + +章节来源 +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/app/schemas/auth.py:1-34](file://backend/app/schemas/auth.py#L1-L34) +- [backend/app/models/user.py:1-41](file://backend/app/models/user.py#L1-L41) + +### 数据库与迁移 +- 连接字符串:使用异步PostgreSQL驱动,指向compose中的db服务。 +- 日志配置:设置SQLAlchemy与Alembic日志级别,便于定位迁移问题。 +- 工具钩子:可选集成格式化与静态检查工具对生成的迁移脚本进行处理。 + +```mermaid +flowchart TD +Start(["开始"]) --> CheckURL["检查数据库URL"] +CheckURL --> LogCfg["配置日志级别"] +LogCfg --> Hooks{"是否启用钩子?"} +Hooks --> |是| RunHooks["运行格式化/静态检查"] +Hooks --> |否| SkipHooks["跳过钩子"] +RunHooks --> Done(["完成"]) +SkipHooks --> Done +``` + +图表来源 +- [backend/alembic.ini:86-114](file://backend/alembic.ini#L86-L114) + +章节来源 +- [backend/alembic.ini:1-150](file://backend/alembic.ini#L1-L150) +- [backend/app/config.py:7-8](file://backend/app/config.py#L7-L8) + +### 前端工程化 +- 构建与运行:dev/build/start/lint脚本由Next.js提供。 +- TypeScript:严格模式、不输出JS、模块解析采用bundler、路径别名@/*映射根目录。 +- ESLint:继承Next.js核心Web Vitals与TypeScript规则。 +- Tailwind:按需扫描pages/components/app目录,启用动画插件。 + +章节来源 +- [frontend/package.json:1-40](file://frontend/package.json#L1-L40) +- [frontend/tsconfig.json:1-27](file://frontend/tsconfig.json#L1-L27) +- [frontend/.eslintrc.json:1-4](file://frontend/.eslintrc.json#L1-L4) +- [frontend/tailwind.config.ts:1-57](file://frontend/tailwind.config.ts#L1-L57) + +## 依赖分析 +- 后端依赖:FastAPI、SQLAlchemy、Pydantic、Redis、APScheduler、Playwright、HTTPX、dotenv、pytest等。 +- 前端依赖:Next.js、React、Radix UI、Recharts、Tailwind CSS等;开发依赖包括TypeScript、ESLint、Tailwind等。 +- 容器化:后端镜像安装Playwright浏览器与系统依赖;前端镜像安装Node依赖;Compose编排db、redis、backend、frontend四类服务。 + +```mermaid +graph LR +subgraph "后端" +FastAPI["FastAPI"] +SQLA["SQLAlchemy"] +Pydantic["Pydantic"] +RedisDep["Redis"] +APS["APScheduler"] +PW["Playwright"] +HTTPX["HTTPX"] +DOTENV["python-dotenv"] +PyTest["pytest"] +end +subgraph "前端" +Next["Next.js"] +React["React"] +Radix["Radix UI"] +Recharts["Recharts"] +Tailwind["Tailwind CSS"] +TS["TypeScript"] +ESL["ESLint"] +end +FastAPI --> SQLA +FastAPI --> Pydantic +FastAPI --> RedisDep +FastAPI --> APS +FastAPI --> PW +FastAPI --> HTTPX +FastAPI --> DOTENV +Next --> React +Next --> Tailwind +Next --> Radix +Next --> Recharts +Next --> TS +Next --> ESL +``` + +图表来源 +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) +- [frontend/package.json:11-38](file://frontend/package.json#L11-L38) + +章节来源 +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) +- [frontend/package.json:1-40](file://frontend/package.json#L1-L40) + +## 性能考虑 +- 异步化:后端使用异步数据库驱动与异步HTTP客户端,减少阻塞,提升并发能力。 +- 缓存:Redis用于任务调度与会话等场景,建议在热点数据访问处引入缓存层。 +- 任务调度:APScheduler负责周期性任务,注意避免重复任务与资源泄漏,结合优雅停机逻辑。 +- 前端构建:严格模式与按需扫描Tailwind可降低包体与构建开销;生产构建建议开启压缩与Tree Shaking。 +- 数据库:合理索引与查询优化,避免N+1查询;批量写入与事务合并可减少往返次数。 + +## 故障排查指南 +- 启动失败(后端):检查数据库与Redis健康状态,确认连接字符串与端口映射正确;查看Uvicorn日志与容器重启策略。 +- 认证异常:核对JWT密钥与过期时间配置;确认请求头携带正确的Bearer Token;检查依赖覆盖与用户mock是否生效。 +- 数据迁移问题:检查Alembic日志级别与钩子配置;确认数据库URL与凭据;必要时手动回滚或修复迁移脚本。 +- 前端样式异常:确认Tailwind content扫描路径与组件目录一致;清理.next缓存后重新构建。 +- 测试失败:确认pytest会话注入后端路径;检查调度器mock与依赖覆盖;使用异步HTTP客户端发起请求。 + +章节来源 +- [docker-compose.yml:4-34](file://docker-compose.yml#L4-L34) +- [backend/app/config.py:7-13](file://backend/app/config.py#L7-L13) +- [tests/conftest.py:19-50](file://tests/conftest.py#L19-L50) +- [backend/alembic.ini:115-150](file://backend/alembic.ini#L115-L150) +- [frontend/tailwind.config.ts:5-9](file://frontend/tailwind.config.ts#L5-L9) + +## 结论 +本指南基于仓库现有实现,给出了统一的代码规范、开发流程与工具使用建议。建议在后续迭代中补充更详细的Git分支策略、代码评审清单与发布流程文档,并持续完善测试覆盖率与性能监控体系。 + +## 附录 + +### 代码规范与最佳实践 + +- Python(后端) + - 使用Pydantic v2进行数据校验与配置管理,字段约束与默认值清晰明确。 + - 异步编程:优先使用异步数据库与HTTP客户端,避免阻塞操作。 + - 错误处理:对外抛出HTTPException并设置合适的状态码与错误信息。 + - 模块化:API、Schema、Model、Service分层清晰,职责单一。 + - 配置:通过Pydantic Settings从.env加载配置,区分开发与生产环境。 + +- TypeScript(前端) + - 严格模式开启,禁用输出JS,使用bundler解析模块,确保类型安全。 + - ESLint规则继承Next.js核心Web Vitals与TypeScript默认规则,保持一致性。 + - Tailwind按需扫描组件与页面目录,减少CSS体积;启用动画插件提升交互体验。 + - 路径别名@/*映射根目录,简化导入路径。 + +- 命名约定 + - Python:模块与类使用PascalCase;函数与变量使用snake_case;常量使用UPPER_CASE。 + - TypeScript:接口与类型使用PascalCase;变量与函数使用camelCase;枚举使用UPPER_CASE。 + +### 开发流程与工作流 + +- Git分支策略(建议) + - 主分支:保护分支,仅允许通过PR合并。 + - 功能分支:feature/xxx,完成后合并到develop。 + - 发布分支:release/x.y.z,用于预发布与回归测试。 + - 热修复分支:hotfix/xxx,直接修改主分支并回放至develop。 + +- 代码评审(建议) + - PR必须包含变更说明、测试用例与性能影响评估。 + - 至少一名Reviewer同意后方可合并。 + - 评审关注点:代码质量、安全性、可维护性与兼容性。 + +- 版本发布管理(建议) + - 语义化版本:小版本用于新增功能,补丁版本用于修复。 + - 发布前:更新CHANGELOG,运行全量测试,检查依赖安全漏洞。 + - 发布后:同步文档与环境配置,监控线上指标。 + +### 开发工具使用方法 + +- IDE配置(建议) + - VS Code:安装Python与TypeScript扩展,启用ESLint与Prettier;配置Python解释器为虚拟环境。 + - 前端:启用TypeScript智能提示与ESLint实时检查;Tailwind IntelliSense增强CSS类提示。 + +- 调试技巧 + - 后端:使用Uvicorn的reload选项热重载;在FastAPI中设置调试日志级别;利用依赖注入覆盖与mock替换真实外部服务。 + - 前端:使用Next.js dev模式热更新;在浏览器开发者工具中检查网络与状态;Tailwind调试辅助类辅助布局。 + +- 性能分析工具(建议) + - 后端:使用cProfile或py-spy分析CPU与内存;结合APScheduler监控任务耗时。 + - 前端:使用Chrome DevTools Performance面板分析渲染与网络;使用Lighthouse评估SEO与可访问性。 + +### 新功能开发指导原则 + +- 模块设计 + - 遵循“API-Service-Model”三层架构,保持关注点分离。 + - 将业务逻辑封装在Service层,避免在API层直接操作数据库。 + +- 接口定义 + - 使用Pydantic模型定义请求与响应结构,明确字段类型与约束。 + - 对外暴露RESTful接口,遵循统一的前缀与标签组织路由。 + +- 测试要求 + - 单元测试:覆盖关键业务逻辑与边界条件。 + - 集成测试:使用pytest与AsyncClient发起HTTP请求,验证端到端流程。 + - Mock策略:对调度器、外部服务与数据库进行合理Mock,保证测试稳定性。 + +### 常见问题与解决方案 + +- 数据库连接失败 + - 检查PostgreSQL容器健康状态与端口映射;确认DATABASE_URL与凭据。 +- Redis连接失败 + - 检查Redis容器健康状态与端口映射;确认REDIS_URL。 +- Playwright无法启动浏览器 + - 确认Dockerfile中已安装Playwright浏览器与系统依赖;检查PLAYWRIGHT_BROWSERS_PATH。 +- CORS跨域问题 + - 核对CORS中间件配置的allow_origins与headers;确保前端域名与端口匹配。 +- JWT认证失败 + - 检查JWT_SECRET与过期时间;确认请求头Authorization格式为Bearer Token。 + +章节来源 +- [backend/app/main.py:30-36](file://backend/app/main.py#L30-L36) +- [backend/app/config.py:9-13](file://backend/app/config.py#L9-L13) +- [backend/Dockerfile:31-33](file://backend/Dockerfile#L31-L33) +- [docker-compose.yml:4-20](file://docker-compose.yml#L4-L20) +- [docker-compose.yml:22-34](file://docker-compose.yml#L22-L34) \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/开发指南/开发流程.md b/.qoder/repowiki/zh/content/开发指南/开发流程.md new file mode 100644 index 0000000..66ccede --- /dev/null +++ b/.qoder/repowiki/zh/content/开发指南/开发流程.md @@ -0,0 +1,283 @@ +# 开发流程 + + +**本文引用的文件** +- [docker-compose.yml](file://docker-compose.yml) +- [backend/Dockerfile](file://backend/Dockerfile) +- [frontend/Dockerfile](file://frontend/Dockerfile) +- [backend/requirements.txt](file://backend/requirements.txt) +- [frontend/package.json](file://frontend/package.json) +- [tests/conftest.py](file://tests/conftest.py) +- [backend/alembic.ini](file://backend/alembic.ini) +- [backend/alembic/versions/488d0bd5ab01_initial_migration.py](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py) +- [frontend/README.md](file://frontend/README.md) + + +## 目录 +1. [简介](#简介) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖关系分析](#依赖关系分析) +7. [性能考虑](#性能考虑) +8. [故障排查指南](#故障排查指南) +9. [结论](#结论) +10. [附录](#附录) + +## 简介 +本文件面向GEO项目的开发团队,提供一套完整的开发流程规范,涵盖Git分支策略与工作流、代码审查流程、版本发布管理、持续集成/持续部署(CI/CD)配置说明,以及开发环境搭建与团队协作最佳实践。内容基于仓库现有技术栈与容器化配置进行设计,确保前后端协同开发、数据库迁移与测试流程可追溯、可复现。 + +## 项目结构 +GEO采用前后端分离与容器编排的组织方式: +- 前端:Next.js 14 应用,使用TypeScript与TailwindCSS,开发端口3000。 +- 后端:FastAPI + Uvicorn,使用异步数据库驱动与Redis缓存,开发端口8000。 +- 数据层:PostgreSQL 15 + Redis 7,通过Docker Compose统一编排。 +- 迁移与版本管理:Alembic用于数据库迁移脚本生成与执行。 +- 测试:pytest + pytest-asyncio,配合HTTP异步客户端与依赖注入覆盖。 + +```mermaid +graph TB +subgraph "本地开发环境" +FE["前端服务
Next.js 3000"] +BE["后端服务
FastAPI 8000"] +DB["数据库
PostgreSQL 15"] +RC["缓存
Redis 7"] +end +FE --> |"HTTP 请求"| BE +BE --> DB +BE --> RC +``` + +图表来源 +- [docker-compose.yml:36-66](file://docker-compose.yml#L36-L66) +- [backend/Dockerfile:1-41](file://backend/Dockerfile#L1-L41) +- [frontend/Dockerfile:1-15](file://frontend/Dockerfile#L1-L15) + +章节来源 +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) +- [backend/Dockerfile:1-41](file://backend/Dockerfile#L1-L41) +- [frontend/Dockerfile:1-15](file://frontend/Dockerfile#L1-L15) + +## 核心组件 +- 前端工程 + - 使用Next.js应用路由与API路由,开发脚本与构建脚本在package.json中定义。 + - TypeScript与ESLint配置位于前端根目录,保证类型安全与代码风格一致。 +- 后端工程 + - FastAPI应用入口与异步数据库连接,依赖通过requirements.txt集中管理。 + - Alembic迁移配置与初始迁移脚本存在,支持数据库演进。 +- 测试工程 + - pytest会话级fixture用于屏蔽真实定时任务,提供模拟用户与认证头,便于快速集成测试。 +- 容器化与编排 + - Dockerfile分别构建前后端镜像;docker-compose统一启动数据库、缓存、后端与前端服务,并设置健康检查与依赖顺序。 + +章节来源 +- [frontend/package.json:1-40](file://frontend/package.json#L1-L40) +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) +- [tests/conftest.py:1-71](file://tests/conftest.py#L1-L71) +- [backend/alembic.ini:1-134](file://backend/alembic.ini#L1-L134) +- [backend/alembic/versions/488d0bd5ab01_initial_migration.py](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py) + +## 架构总览 +下图展示从浏览器到后端API再到数据库与缓存的整体调用链路,体现开发环境中的典型请求路径与数据流向。 + +```mermaid +sequenceDiagram +participant Browser as "浏览器" +participant Frontend as "前端应用" +participant Backend as "后端API" +participant DB as "数据库" +participant Cache as "缓存" +Browser->>Frontend : "访问页面" +Frontend->>Backend : "发起HTTP请求" +Backend->>DB : "查询/写入数据" +DB-->>Backend : "返回结果" +Backend->>Cache : "读取/更新缓存" +Cache-->>Backend : "返回缓存命中/更新" +Backend-->>Frontend : "响应JSON" +Frontend-->>Browser : "渲染页面" +``` + +图表来源 +- [docker-compose.yml:36-66](file://docker-compose.yml#L36-L66) +- [backend/Dockerfile:1-41](file://backend/Dockerfile#L1-L41) +- [frontend/Dockerfile:1-15](file://frontend/Dockerfile#L1-L15) + +## 详细组件分析 + +### Git 分支策略与工作流 +建议采用Git Flow变体,结合Feature分支与Release分支,确保开发与发布的可控性与可追溯性: + +- 主分支 + - main:仅允许来自release或hotfix的合并,保持生产就绪状态。 + - develop:集成各功能特性,作为后续release的基线。 +- 功能分支(feature) + - 命名:feature/-短描述 + - 从develop切出,完成后合并回develop并删除分支。 +- 预发布分支(release) + - 从develop切出,进行最终修复与回归测试,完成后合并至main与develop并打标签。 +- 热修复分支(hotfix) + - 从main切出,紧急修复后同时合并回main与develop。 + +```mermaid +flowchart TD +A["开始"] --> B["创建功能分支 feature/-desc"] +B --> C["提交到功能分支"] +C --> D{"完成开发?"} +D --> |否| C +D --> |是| E["推送并发起PR到 develop"] +E --> F["代码审查与测试通过"] +F --> G["合并到 develop 并删除分支"] +G --> H["准备 release 分支"] +H --> I["最终测试与修复"] +I --> J["合并到 main 与 develop 并打标签"] +``` + +说明 +- PR需关联Issue,确保需求可追溯。 +- 合并前必须通过本地与CI测试,且无冲突。 + +### 代码审查流程 +- Pull Request 模板 + - 必填字段:标题(简述变更)、类型(修复/特性/重构/文档)、影响范围、变更摘要、测试要点、风险评估与回滚预案。 + - 关联信息:关联Issue编号、相关PR链接、模块负责人。 +- 审查标准 + - 正确性:单元测试与集成测试覆盖率达标;边界条件与异常处理完备。 + - 可读性:命名规范、注释清晰、函数长度与复杂度合理。 + - 兼容性:接口变更需向后兼容或提供迁移方案;数据库变更需可逆。 + - 安全性:输入校验、权限控制、敏感信息脱敏。 +- 合并要求 + - 至少一名审查者批准。 + - CI流水线通过。 + - 无未解决评论。 + - 合并后清理功能分支。 + +### 版本发布管理 +- 语义化版本控制 + - 主版本号:破坏性变更 + - 次版本号:新增兼容功能 + - 修订号:修复兼容问题 +- 变更日志维护 + - 按版本记录:新增、修复、改进、废弃与破坏性变更。 + - 引用相关PR与Issue,便于追溯。 +- 发布标签 + - 在release分支合并后于main打标签,格式:vMAJOR.MINOR.REVISION。 + - 为每个版本生成对应Docker镜像并推送到制品库。 + +### 持续集成/持续部署(CI/CD) +当前仓库未包含CI配置文件,建议在CI系统中实现以下流程: +- 触发条件 + - push到develop/release/main/hotfix或打开PR时触发。 +- 步骤 + - 安装依赖:后端pip、前端npm ci。 + - 代码质量:后端使用ruff/black,前端使用ESLint。 + - 单元测试:pytest与pytest-asyncio,生成覆盖率报告。 + - 容器构建:分别构建后端与前端镜像。 + - 健康检查:启动docker-compose并在数据库/缓存健康后运行端到端测试。 + - 制品归档:上传测试报告与镜像。 +- 部署 + - develop:自动部署到预发布环境。 + - release:手动审批后部署到生产环境。 + - hotfix:自动部署到生产并回补到develop。 + +```mermaid +flowchart TD +S["提交/PR触发"] --> L["安装依赖与代码质量检查"] +L --> T["运行单元测试与覆盖率"] +T --> B["构建后端/前端镜像"] +B --> D["启动容器并健康检查"] +D --> E["端到端测试"] +E --> R{"是否发布?"} +R --> |否| P["归档制品与报告"] +R --> |是| DEP["部署到目标环境"] +DEP --> TAG["打标签并发布"] +``` + +说明 +- 以上为通用CI/CD流程建议,具体实现需在CI平台配置相应作业与工件。 + +### 开发环境搭建与团队协作最佳实践 +- 环境准备 + - 安装Docker与Docker Compose,确保端口未被占用。 + - 复制示例环境变量文件并按需调整数据库与缓存参数。 +- 启动步骤 + - 后端:进入backend目录,安装依赖并启动服务;或使用docker-compose一键启动。 + - 前端:进入frontend目录,安装依赖并启动开发服务器。 +- 团队协作 + - 统一代码风格:后端使用ruff/black,前端使用ESLint/TailwindCSS。 + - 提交信息规范:type(scope): subject,如feat(api): 新增用户认证接口。 + - 冲突解决:优先rebase保持线性历史,必要时使用merge并保留解决记录。 + - 文档同步:变更涉及用户界面或API时同步更新README与变更日志。 + +章节来源 +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) +- [frontend/README.md:1-37](file://frontend/README.md#L1-L37) + +## 依赖关系分析 +- 技术栈依赖 + - 前端:Next.js、React、TypeScript、TailwindCSS、Radix UI等。 + - 后端:FastAPI、SQLAlchemy、AsyncPG、Alembic、Redis、APScheduler、Playwright等。 +- 容器与编排 + - docker-compose定义了数据库、缓存、后端与前端服务的依赖关系与健康检查。 +- 测试依赖 + - pytest与pytest-asyncio用于异步测试;ASGI传输用于HTTP客户端测试;依赖注入覆盖用于模拟认证上下文。 + +```mermaid +graph LR +FE["前端应用"] --> |HTTP| BE["后端API"] +BE --> DB["PostgreSQL"] +BE --> RC["Redis"] +BE --> AL["Alembic 迁移"] +TEST["pytest 测试"] --> BE +``` + +图表来源 +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) +- [docker-compose.yml:36-66](file://docker-compose.yml#L36-L66) +- [tests/conftest.py:1-71](file://tests/conftest.py#L1-L71) + +章节来源 +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) +- [tests/conftest.py:1-71](file://tests/conftest.py#L1-L71) +- [backend/alembic.ini:1-134](file://backend/alembic.ini#L1-L134) + +## 性能考虑 +- 数据库 + - 使用异步驱动与连接池,避免阻塞;迁移脚本应最小化DDL变更,避免长事务锁表。 +- 缓存 + - 合理设置TTL与键空间,热点数据优先缓存;定期清理过期键。 +- API + - 控制响应大小与分页;对高频接口启用缓存;限制并发与速率。 +- 前端 + - 图片与静态资源优化;按需加载;TailwindCSS类名避免冗余。 +- 容器 + - 使用多阶段构建减少镜像体积;健康检查降低重启时间;资源限制防止资源争用。 + +## 故障排查指南 +- 本地服务无法启动 + - 检查端口占用与防火墙;确认数据库与缓存服务已健康运行。 +- 数据库迁移失败 + - 查看Alembic配置与URL;确认迁移脚本语法正确;必要时回滚至上一版本。 +- 测试异常 + - 确认pytest会话级fixture已正确屏蔽定时任务;检查依赖注入覆盖是否生效。 +- 前后端联调失败 + - 核对CORS与代理配置;确认API路由与鉴权头正确传递。 + +章节来源 +- [docker-compose.yml:16-20](file://docker-compose.yml#L16-L20) +- [docker-compose.yml:30-34](file://docker-compose.yml#L30-L34) +- [backend/alembic.ini:86-89](file://backend/alembic.ini#L86-L89) +- [tests/conftest.py:19-25](file://tests/conftest.py#L19-L25) + +## 结论 +通过明确的分支策略、严格的代码审查流程、规范的版本发布管理与可落地的CI/CD实践,GEO项目能够在保障交付质量的同时提升团队协作效率。建议尽快在CI平台落地上述流程,并根据实际运行情况迭代优化。 + +## 附录 +- 快速启动命令 + - docker-compose:启动所有服务并进入交互模式。 + - 前端:进入frontend目录,安装依赖后启动开发服务器。 + - 后端:进入backend目录,安装依赖后启动Uvicorn服务。 +- 参考文档 + - Next.js官方文档与部署指南。 + - FastAPI与SQLAlchemy异步数据库实践。 + - Alembic迁移最佳实践。 \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/开发指南/新功能开发.md b/.qoder/repowiki/zh/content/开发指南/新功能开发.md new file mode 100644 index 0000000..209da95 --- /dev/null +++ b/.qoder/repowiki/zh/content/开发指南/新功能开发.md @@ -0,0 +1,359 @@ +# 新功能开发 + + +**本文档引用的文件** +- [backend/app/main.py](file://backend/app/main.py) +- [backend/app/api/auth.py](file://backend/app/api/auth.py) +- [backend/app/api/queries.py](file://backend/app/api/queries.py) +- [backend/app/schemas/auth.py](file://backend/app/schemas/auth.py) +- [backend/app/schemas/query.py](file://backend/app/schemas/query.py) +- [backend/app/models/user.py](file://backend/app/models/user.py) +- [backend/app/models/query.py](file://backend/app/models/query.py) +- [backend/app/services/auth.py](file://backend/app/services/auth.py) +- [backend/app/services/query.py](file://backend/app/services/query.py) +- [frontend/app/layout.tsx](file://frontend/app/layout.tsx) +- [frontend/components/providers.tsx](file://frontend/components/providers.tsx) +- [frontend/lib/api.ts](file://frontend/lib/api.ts) +- [tests/test_auth.py](file://tests/test_auth.py) + + +## 目录 +1. [简介](#简介) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖分析](#依赖分析) +7. [性能考虑](#性能考虑) +8. [故障排查指南](#故障排查指南) +9. [结论](#结论) +10. [附录](#附录) + +## 简介 +本指导文档面向在 GEO 平台进行新功能开发的工程师,系统阐述后端 API 模块设计原则、接口定义与依赖管理;前端页面开发规范(页面组织、组件设计、状态管理);数据库模型设计指导(表结构、关系映射、索引策略);以及测试驱动开发(TDD)最佳实践与单元测试编写指南。文档以现有代码库为依据,结合可扩展性与一致性原则,帮助团队高效、安全地交付高质量功能。 + +## 项目结构 +- 后端采用 FastAPI + SQLAlchemy 异步 ORM 架构,按领域分层组织:API 路由、服务层、模型层、模式层(Pydantic)、依赖注入与工具。 +- 前端采用 Next.js App Router,使用 TypeScript、TailwindCSS 与 NextAuth 管理会话状态,通过统一的 API 客户端封装调用。 +- 测试覆盖后端 API 与服务逻辑,使用 pytest 异步客户端与依赖覆盖机制。 + +```mermaid +graph TB +subgraph "前端" +FE_App["Next.js 应用
根布局与全局样式"] +FE_Providers["会话提供者
NextAuth Provider"] +FE_API["API 客户端
统一请求封装"] +end +subgraph "后端" +BE_Main["FastAPI 应用
生命周期与中间件"] +BE_Routers["API 路由
认证/查询/引用/报告"] +BE_Services["服务层
业务逻辑"] +BE_Models["模型层
SQLAlchemy ORM"] +BE_Schemas["模式层
Pydantic 校验"] +end +FE_App --> FE_Providers +FE_API --> BE_Routers +BE_Main --> BE_Routers +BE_Routers --> BE_Services +BE_Services --> BE_Models +BE_Services --> BE_Schemas +``` + +图表来源 +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [frontend/app/layout.tsx:1-37](file://frontend/app/layout.tsx#L1-L37) +- [frontend/components/providers.tsx:1-9](file://frontend/components/providers.tsx#L1-L9) +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) + +章节来源 +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [frontend/app/layout.tsx:1-37](file://frontend/app/layout.tsx#L1-L37) +- [frontend/components/providers.tsx:1-9](file://frontend/components/providers.tsx#L1-L9) +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) + +## 核心组件 +- 后端应用与生命周期:定义 CORS、路由注册、健康检查与任务调度器启动/关闭。 +- 认证模块:注册、登录、当前用户信息,配合 JWT 令牌与密码哈希。 +- 查询模块:CRUD、分页、权限校验(用户配额限制)、频率变更触发下次执行时间重算。 +- 数据模型:用户、查询、引用记录、查询任务、订阅等,定义字段、外键与索引。 +- 前端布局与会话:根布局、全局样式、NextAuth Provider 包裹。 +- 前端 API 客户端:统一封装鉴权头、错误处理与各模块接口。 + +章节来源 +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/app/api/queries.py:1-86](file://backend/app/api/queries.py#L1-L86) +- [backend/app/models/user.py:1-41](file://backend/app/models/user.py#L1-L41) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [frontend/app/layout.tsx:1-37](file://frontend/app/layout.tsx#L1-L37) +- [frontend/components/providers.tsx:1-9](file://frontend/components/providers.tsx#L1-L9) +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) + +## 架构总览 +后端通过 FastAPI 聚合多个领域路由,每个路由对应一个服务层函数,服务层通过 SQLAlchemy 异步会话访问数据库模型,并使用 Pydantic 模式进行输入输出校验。前端通过统一 API 客户端发起请求,自动附加鉴权头,错误时统一解析并抛出。 + +```mermaid +sequenceDiagram +participant FE as "前端页面" +participant API as "API 客户端" +participant Router as "FastAPI 路由" +participant Service as "服务层" +participant Model as "模型层(SQLAlchemy)" +participant DB as "数据库" +FE->>API : "调用接口(带鉴权头)" +API->>Router : "HTTP 请求" +Router->>Service : "调用业务方法" +Service->>Model : "ORM 查询/更新" +Model->>DB : "异步 SQL 执行" +DB-->>Model : "结果集" +Model-->>Service : "领域对象" +Service-->>Router : "返回结果" +Router-->>API : "JSON 响应" +API-->>FE : "解析并渲染" +``` + +图表来源 +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/app/api/queries.py:1-86](file://backend/app/api/queries.py#L1-L86) +- [backend/app/services/auth.py:1-69](file://backend/app/services/auth.py#L1-L69) +- [backend/app/services/query.py:1-130](file://backend/app/services/query.py#L1-L130) +- [backend/app/models/user.py:1-41](file://backend/app/models/user.py#L1-L41) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) + +## 详细组件分析 + +### 后端模块设计原则与依赖管理 +- 模块划分 + - API 层:定义路由、参数校验、异常映射与响应模型。 + - 服务层:封装业务规则、权限控制与跨模型操作。 + - 模型层:定义表结构、关系与索引。 + - 模式层:Pydantic 模型用于请求/响应序列化与校验。 +- 接口定义 + - 统一前缀与标签:如 /api/v1/auth、/api/v1/queries 等。 + - 参数校验:Query 分页参数、Pydantic 字段长度与枚举校验。 + - 错误处理:HTTP 状态码与错误详情映射。 +- 依赖管理 + - 依赖注入:数据库会话、当前用户通过 Depends 获取。 + - 生命周期:应用启动时初始化模型与调度器,关闭时优雅退出。 + +章节来源 +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/app/api/queries.py:1-86](file://backend/app/api/queries.py#L1-L86) +- [backend/app/schemas/auth.py:1-34](file://backend/app/schemas/auth.py#L1-L34) +- [backend/app/schemas/query.py:1-94](file://backend/app/schemas/query.py#L1-L94) +- [backend/app/services/auth.py:1-69](file://backend/app/services/auth.py#L1-L69) +- [backend/app/services/query.py:1-130](file://backend/app/services/query.py#L1-L130) +- [backend/app/models/user.py:1-41](file://backend/app/models/user.py#L1-L41) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) + +### 认证模块(API/服务/模式) +- 路由设计 + - 注册:接收注册体,返回用户信息(201)。 + - 登录:校验凭据,签发 JWT,返回令牌与用户信息。 + - 当前用户:基于依赖注入获取已认证用户。 +- 处理逻辑 + - 密码哈希与校验、JWT 过期时间配置、邮箱唯一性检查。 +- 错误处理 + - 注册重复邮箱、登录凭据无效、未授权访问。 + +```mermaid +sequenceDiagram +participant C as "客户端" +participant R as "认证路由" +participant S as "认证服务" +participant U as "用户模型" +participant T as "JWT/密码库" +C->>R : "POST /api/v1/auth/register" +R->>S : "register_user()" +S->>U : "查询邮箱是否已存在" +U-->>S : "不存在" +S->>T : "hash_password()" +S->>U : "创建用户并持久化" +U-->>S : "返回用户" +S-->>R : "用户信息" +R-->>C : "201 + 用户" +C->>R : "POST /api/v1/auth/login" +R->>S : "authenticate_user()" +S->>U : "查找用户" +U-->>S : "用户对象" +S->>T : "verify_password()" +T-->>S : "匹配成功" +S-->>R : "用户+令牌" +R-->>C : "200 + 令牌" +``` + +图表来源 +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/app/services/auth.py:1-69](file://backend/app/services/auth.py#L1-L69) +- [backend/app/models/user.py:1-41](file://backend/app/models/user.py#L1-L41) +- [backend/app/schemas/auth.py:1-34](file://backend/app/schemas/auth.py#L1-L34) + +章节来源 +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/app/services/auth.py:1-69](file://backend/app/services/auth.py#L1-L69) +- [backend/app/models/user.py:1-41](file://backend/app/models/user.py#L1-L41) +- [backend/app/schemas/auth.py:1-34](file://backend/app/schemas/auth.py#L1-L34) + +### 查询模块(API/服务/模式/模型) +- 路由设计 + - 列表:支持 skip/limit 分页,返回 items 与 total。 + - 创建:校验平台与频率,检查用户配额,计算 next_query_at。 + - 读取/更新/删除:基于 query_id 与 user_id 的权限校验。 +- 处理逻辑 + - 频率变更时重算下次执行时间。 + - 删除级联清理关联任务与引用记录。 +- 错误处理 + - 403 配额超限、404 未找到、401 未授权。 + +```mermaid +flowchart TD +Start(["创建查询"]) --> Count["统计用户查询数量"] +Count --> CheckLimit{"是否超过配额?"} +CheckLimit --> |是| Raise403["抛出权限错误(403)"] +CheckLimit --> |否| CalcNext["根据频率计算 next_query_at"] +CalcNext --> Persist["持久化查询记录"] +Persist --> Done(["完成"]) +Raise403 --> End(["结束"]) +Done --> End +``` + +图表来源 +- [backend/app/services/query.py:45-81](file://backend/app/services/query.py#L45-L81) + +章节来源 +- [backend/app/api/queries.py:1-86](file://backend/app/api/queries.py#L1-L86) +- [backend/app/services/query.py:1-130](file://backend/app/services/query.py#L1-L130) +- [backend/app/schemas/query.py:1-94](file://backend/app/schemas/query.py#L1-L94) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) + +### 前端页面开发规范 +- 页面组织 + - 使用 Next.js App Router 的分组路由与布局,根布局统一注入 Provider。 +- 组件设计 + - UI 组件库化,遵循语义化与可复用性;页面组件负责数据获取与展示。 +- 状态管理 + - 使用 NextAuth 提供的会话状态,通过 Provider 在应用根部注入。 +- API 调用 + - 统一的 api.ts 封装:自动添加 Content-Type 与 Authorization 头,统一错误处理与 JSON 解析。 + +```mermaid +graph LR +Root["根布局
layout.tsx"] --> Providers["会话提供者
providers.tsx"] +Providers --> Pages["页面与组件"] +Pages --> API["API 客户端
api.ts"] +API --> Backend["后端 API"] +``` + +图表来源 +- [frontend/app/layout.tsx:1-37](file://frontend/app/layout.tsx#L1-L37) +- [frontend/components/providers.tsx:1-9](file://frontend/components/providers.tsx#L1-L9) +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) + +章节来源 +- [frontend/app/layout.tsx:1-37](file://frontend/app/layout.tsx#L1-L37) +- [frontend/components/providers.tsx:1-9](file://frontend/components/providers.tsx#L1-L9) +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) + +### 数据库模型设计指导 +- 表结构设计 + - 使用 UUID 主键,字符串长度约束,布尔与整数字段合理设置默认值。 + - JSONB 字段用于存储动态列表(品牌别名、平台列表)。 +- 关系映射 + - 外键约束与级联删除(如用户删除时级联删除其查询与订阅)。 + - 双向关系与回溯属性(back_populates)。 +- 索引策略 + - 对高频过滤字段建立复合或单列索引(如 user_id、status、next_query_at)。 + +章节来源 +- [backend/app/models/user.py:1-41](file://backend/app/models/user.py#L1-L41) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) + +### 测试驱动开发(TDD)最佳实践与单元测试编写指南 +- 设计原则 + - 先写失败的测试,再实现最小逻辑使其通过;关注边界条件与错误路径。 + - 使用依赖覆盖(dependency override)隔离外部依赖,确保测试可重复。 +- 单元测试编写 + - 使用 pytest 异步客户端发起请求,断言状态码与响应结构。 + - Mock 服务层或模型层行为,验证路由层的参数传递与异常映射。 +- 示例参考 + - 认证模块测试覆盖注册成功/重复邮箱、登录成功/凭据错误、当前用户接口的鉴权场景。 + +章节来源 +- [tests/test_auth.py:1-104](file://tests/test_auth.py#L1-L104) + +## 依赖分析 +- 后端模块耦合 + - API 路由依赖服务层;服务层依赖模型层与配置;模式层独立于业务逻辑。 + - 依赖注入降低耦合,便于替换与测试。 +- 前后端交互 + - 前端通过统一 API 客户端与后端路由通信,鉴权头由 NextAuth 管理。 + +```mermaid +graph TB +API_Auth["认证路由"] --> Svc_Auth["认证服务"] +API_Queries["查询路由"] --> Svc_Query["查询服务"] +Svc_Auth --> Model_User["用户模型"] +Svc_Query --> Model_Query["查询模型"] +FE_API["前端 API 客户端"] --> API_Auth +FE_API --> API_Queries +``` + +图表来源 +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/app/api/queries.py:1-86](file://backend/app/api/queries.py#L1-L86) +- [backend/app/services/auth.py:1-69](file://backend/app/services/auth.py#L1-L69) +- [backend/app/services/query.py:1-130](file://backend/app/services/query.py#L1-L130) +- [backend/app/models/user.py:1-41](file://backend/app/models/user.py#L1-L41) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) + +章节来源 +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/app/api/queries.py:1-86](file://backend/app/api/queries.py#L1-L86) +- [backend/app/services/auth.py:1-69](file://backend/app/services/auth.py#L1-L69) +- [backend/app/services/query.py:1-130](file://backend/app/services/query.py#L1-L130) +- [backend/app/models/user.py:1-41](file://backend/app/models/user.py#L1-L41) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) + +## 性能考虑 +- 数据库层面 + - 为高频查询字段建立索引,避免全表扫描;对分页与排序字段保持索引覆盖。 + - 使用批量查询与延迟加载策略,减少 N+1 查询风险。 +- 服务层层面 + - 合理拆分查询与计数,避免不必要的 COUNT 开销;缓存热点数据(如平台白名单)。 +- API 层面 + - 控制分页大小上限,防止过大数据量返回;对敏感字段进行选择性序列化。 +- 前端层面 + - 使用虚拟列表与懒加载优化长列表渲染;统一错误提示与加载态,提升用户体验。 + +## 故障排查指南 +- 常见问题定位 + - 401 未授权:确认鉴权头是否正确传递与令牌是否过期。 + - 403 权限错误:检查用户配额与资源归属(user_id)。 + - 404 未找到:核对主键与所属用户 ID 是否匹配。 + - 400 参数错误:检查 Pydantic 校验规则与枚举值。 +- 日志与监控 + - 后端开启路由级日志与异常捕获;前端统一错误上报与用户提示。 +- 回滚与恢复 + - 数据库迁移版本化管理;关键变更需配套回滚脚本与数据备份。 + +章节来源 +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/app/api/queries.py:1-86](file://backend/app/api/queries.py#L1-L86) +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) + +## 结论 +通过清晰的模块划分、严格的依赖注入、完善的模式校验与统一的前后端交互规范,GEO 平台具备良好的扩展性与可维护性。建议在新增功能时遵循本文档的设计原则与测试实践,确保功能快速落地且质量可控。 + +## 附录 +- 新增功能开发清单 + - 明确领域边界与模块职责,避免跨模块耦合。 + - 编写路由与模式定义,确保参数与响应结构清晰。 + - 实现服务层业务逻辑,覆盖边界条件与异常路径。 + - 设计数据库模型与索引,评估性能影响。 + - 编写单元测试与集成测试,保证功能正确性。 + - 前端页面与组件开发,统一状态管理与错误处理。 + - 文档与回归测试同步推进,保障发布质量。 \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/快速开始.md b/.qoder/repowiki/zh/content/快速开始.md new file mode 100644 index 0000000..8e86a09 --- /dev/null +++ b/.qoder/repowiki/zh/content/快速开始.md @@ -0,0 +1,454 @@ +# 快速开始 + + +**本文引用的文件** +- [docker-compose.yml](file://docker-compose.yml) +- [backend/Dockerfile](file://backend/Dockerfile) +- [frontend/Dockerfile](file://frontend/Dockerfile) +- [backend/requirements.txt](file://backend/requirements.txt) +- [frontend/package.json](file://frontend/package.json) +- [backend/app/main.py](file://backend/app/main.py) +- [backend/app/config.py](file://backend/app/config.py) +- [backend/app/api/auth.py](file://backend/app/api/auth.py) +- [backend/app/api/queries.py](file://backend/app/api/queries.py) +- [backend/app/api/citations.py](file://backend/app/api/citations.py) +- [backend/app/models/user.py](file://backend/app/models/user.py) +- [backend/app/models/query.py](file://backend/app/models/query.py) +- [backend/app/models/citation_record.py](file://backend/app/models/citation_record.py) +- [frontend/lib/api.ts](file://frontend/lib/api.ts) +- [frontend/README.md](file://frontend/README.md) + + +## 目录 +1. [简介](#简介) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖分析](#依赖分析) +7. [性能考虑](#性能考虑) +8. [故障排除指南](#故障排除指南) +9. [结论](#结论) +10. [附录](#附录) + +## 简介 +本指南面向新开发者,帮助你在最短时间内完成 GEO 平台的本地开发与容器化部署,并掌握核心功能的使用方式。你将学会: +- 环境要求与依赖安装 +- 本地开发环境配置 +- Docker 容器化部署全流程 +- 基本使用示例:用户注册/登录、创建查询任务、查看引用数据 +- 调试方法与常见问题解决 + +## 项目结构 +项目采用前后端分离架构,后端基于 FastAPI,前端基于 Next.js,数据库使用 PostgreSQL,缓存使用 Redis,任务调度与浏览器自动化通过 APScheduler 和 Playwright 实现。 + +```mermaid +graph TB +subgraph "容器编排" +DC["docker-compose.yml"] +end +subgraph "后端服务" +BK_MAIN["backend/app/main.py"] +BK_CONF["backend/app/config.py"] +BK_AUTH["backend/app/api/auth.py"] +BK_QUERIES["backend/app/api/queries.py"] +BK_CIT["backend/app/api/citations.py"] +BK_MODELS["backend/app/models/*"] +BK_REQ["backend/requirements.txt"] +BK_DOCK["backend/Dockerfile"] +end +subgraph "前端服务" +FE_API["frontend/lib/api.ts"] +FE_PKG["frontend/package.json"] +FE_README["frontend/README.md"] +FE_DOCK["frontend/Dockerfile"] +end +DB["PostgreSQL 容器"] +RDS["Redis 容器"] +DC --> DB +DC --> RDS +DC --> BK_MAIN +DC --> FE_API +BK_MAIN --> BK_AUTH +BK_MAIN --> BK_QUERIES +BK_MAIN --> BK_CIT +BK_MAIN --> BK_MODELS +BK_MAIN --> BK_CONF +FE_API --> BK_MAIN +``` + +图表来源 +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/app/api/queries.py:1-86](file://backend/app/api/queries.py#L1-L86) +- [backend/app/api/citations.py:1-78](file://backend/app/api/citations.py#L1-L78) +- [backend/app/models/user.py:1-41](file://backend/app/models/user.py#L1-L41) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [backend/app/models/citation_record.py:1-42](file://backend/app/models/citation_record.py#L1-L42) +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) +- [frontend/package.json:1-40](file://frontend/package.json#L1-L40) + +章节来源 +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [frontend/README.md:1-37](file://frontend/README.md#L1-L37) + +## 核心组件 +- 后端 API(FastAPI):提供认证、查询词管理、引用数据与报告相关接口;内置健康检查端点。 +- 前端应用(Next.js):通过统一的 API 封装调用后端接口,支持注册、登录、查询列表、引用数据与导出。 +- 数据库(PostgreSQL):存储用户、查询词、引用记录等业务数据。 +- 缓存与任务(Redis + APScheduler + Playwright):用于任务调度与浏览器自动化抓取。 + +章节来源 +- [backend/app/main.py:24-47](file://backend/app/main.py#L24-L47) +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) +- [backend/app/config.py:7-13](file://backend/app/config.py#L7-L13) + +## 架构总览 +下图展示了容器化部署时各服务之间的交互关系与启动顺序。 + +```mermaid +graph TB +subgraph "网络" +L3000["前端: http://localhost:3000"] +L8000["后端: http://localhost:8000"] +end +subgraph "容器" +DB["Postgres:5432"] +RDS["Redis:6379"] +BE["后端容器"] +FE["前端容器"] +end +L3000 --> FE +FE --> BE +BE --> DB +BE --> RDS +``` + +图表来源 +- [docker-compose.yml:3-66](file://docker-compose.yml#L3-L66) +- [backend/app/main.py:30-36](file://backend/app/main.py#L30-L36) + +章节来源 +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) +- [backend/app/main.py:24-47](file://backend/app/main.py#L24-L47) + +## 详细组件分析 + +### 后端 API 组件 +- 应用入口与生命周期:在应用启动时初始化模型与调度器,在关闭时优雅停机。 +- 中间件:启用 CORS,允许前端域名访问。 +- 路由:认证、查询词、引用数据、报告等模块路由均已挂载。 +- 健康检查:提供 /health 探针。 + +```mermaid +sequenceDiagram +participant C as "客户端" +participant F as "前端(Next.js)" +participant B as "后端(FastAPI)" +participant D as "数据库(PostgreSQL)" +participant Q as "调度器(APScheduler)" +C->>F : 打开页面 +F->>B : GET /health +B-->>F : {"status" : "ok"} +Note over B,Q : 应用启动时启动调度器 +B->>Q : 启动任务调度 +Q->>D : 按计划执行查询 +``` + +图表来源 +- [backend/app/main.py:13-21](file://backend/app/main.py#L13-L21) +- [backend/app/main.py:45-47](file://backend/app/main.py#L45-L47) + +章节来源 +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) + +### 认证与用户模型 +- 认证接口:注册、登录、获取当前用户信息。 +- 用户模型:包含邮箱、密码哈希、名称、订阅计划、配额、活跃状态及时间戳字段。 + +```mermaid +classDiagram +class User { ++UUID id ++string email ++string password_hash ++string name ++string plan ++int max_queries ++bool is_active ++datetime created_at ++datetime updated_at +} +``` + +图表来源 +- [backend/app/models/user.py:11-41](file://backend/app/models/user.py#L11-L41) + +章节来源 +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/app/models/user.py:1-41](file://backend/app/models/user.py#L1-L41) + +### 查询词与引用数据 +- 查询词接口:分页列出、创建、更新、删除查询词。 +- 引用数据接口:分页查询引用记录、统计信息、立即触发查询任务。 +- 查询词模型:包含关键词、目标品牌、别名、平台集合、频率、状态、下次执行时间等。 +- 引用记录模型:包含平台、是否被引、位置、文本、竞品品牌、原始响应、抓取时间等。 + +```mermaid +erDiagram +USER ||--o{ QUERY : "拥有" +QUERY ||--o{ CITATION_RECORD : "产生" +USER { +uuid id +string email +string name +} +QUERY { +uuid id +uuid user_id +string keyword +string target_brand +json brand_aliases +json platforms +string frequency +string status +timestamp last_queried_at +timestamp next_query_at +} +CITATION_RECORD { +uuid id +uuid query_id +string platform +bool cited +int citation_position +text citation_text +json competitor_brands +text raw_response +timestamp queried_at +} +``` + +图表来源 +- [backend/app/models/user.py:11-41](file://backend/app/models/user.py#L11-L41) +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [backend/app/models/citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) + +章节来源 +- [backend/app/api/queries.py:1-86](file://backend/app/api/queries.py#L1-L86) +- [backend/app/api/citations.py:1-78](file://backend/app/api/citations.py#L1-L78) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [backend/app/models/citation_record.py:1-42](file://backend/app/models/citation_record.py#L1-L42) + +### 前端 API 封装 +- 基础地址:优先读取环境变量 NEXT_PUBLIC_API_URL,否则回退到本地后端地址。 +- 认证封装:自动注入 Authorization Bearer Token。 +- 接口覆盖:认证、查询词、引用数据、报告导出等。 + +```mermaid +flowchart TD +Start(["发起 API 请求"]) --> BuildHeaders["构建请求头
含 Content-Type 与可选 Authorization"] +BuildHeaders --> FetchCall["fetch 发起请求"] +FetchCall --> RespOk{"响应成功?"} +RespOk --> |是| ParseJson["解析 JSON"] +RespOk --> |否| ThrowErr["抛出错误(包含 HTTP 状态或详情)"] +ParseJson --> End(["返回数据"]) +ThrowErr --> End +``` + +图表来源 +- [frontend/lib/api.ts:3-21](file://frontend/lib/api.ts#L3-L21) + +章节来源 +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) + +## 依赖分析 +- 后端依赖:Web 框架、数据库 ORM、异步驱动、配置校验、认证与安全、缓存与任务调度、浏览器自动化、HTTP 客户端、测试工具等。 +- 前端依赖:Next.js、UI 组件库、样式与类型工具等。 +- 容器镜像:后端基于 Python slim 镜像,安装 Playwright 依赖;前端基于 Node Alpine 镜像,使用 npm ci 安装依赖。 + +```mermaid +graph LR +subgraph "后端" +PY["Python 3.11"] +REQ["requirements.txt"] +PLY["Playwright(chromium)"] +end +subgraph "前端" +NODE["Node 20"] +PKG["package.json"] +end +REQ --> PY +PLY --> PY +PKG --> NODE +``` + +图表来源 +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) +- [frontend/package.json:1-40](file://frontend/package.json#L1-L40) +- [backend/Dockerfile:6-33](file://backend/Dockerfile#L6-L33) +- [frontend/Dockerfile:6-7](file://frontend/Dockerfile#L6-L7) + +章节来源 +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) +- [frontend/package.json:1-40](file://frontend/package.json#L1-L40) +- [backend/Dockerfile:1-41](file://backend/Dockerfile#L1-L41) +- [frontend/Dockerfile:1-15](file://frontend/Dockerfile#L1-L15) + +## 性能考虑 +- 数据库索引:查询词与引用记录表均建立复合索引以优化分页与过滤查询。 +- 异步与连接池:使用异步 SQLAlchemy 与异步 PostgreSQL 驱动,提升并发处理能力。 +- 任务调度:APSCheduler 在后台按计划执行查询,避免阻塞主请求线程。 +- 前端缓存:合理利用浏览器缓存与 Next.js 的静态资源优化策略。 + +章节来源 +- [backend/app/models/query.py:50-54](file://backend/app/models/query.py#L50-L54) +- [backend/app/models/citation_record.py:37-41](file://backend/app/models/citation_record.py#L37-L41) + +## 故障排除指南 +- 健康检查失败 + - 现象:访问 /health 返回异常或容器反复重启。 + - 排查:确认数据库与 Redis 健康检查通过;检查后端日志输出。 + - 参考:后端健康检查端点与容器编排健康检查配置。 + + 章节来源 + - [backend/app/main.py:45-47](file://backend/app/main.py#L45-L47) + - [docker-compose.yml:16-20](file://docker-compose.yml#L16-L20) + - [docker-compose.yml:30-34](file://docker-compose.yml#L30-L34) + +- 前端无法访问后端接口 + - 现象:前端控制台出现跨域错误或 404。 + - 排查:确认后端 CORS 允许前端域名;确认前端 API 基础地址正确;确认后端端口映射为 8000。 + - 参考:CORS 配置与前端 API 基础地址。 + + 章节来源 + - [backend/app/main.py:30-36](file://backend/app/main.py#L30-L36) + - [frontend/lib/api.ts:1-1](file://frontend/lib/api.ts#L1-L1) + +- 数据库连接失败 + - 现象:后端启动时报数据库连接错误。 + - 排查:确认数据库容器已就绪且凭据正确;检查 .env 或环境变量;确认数据库端口映射为 5432。 + - 参考:数据库默认连接串与容器编排。 + + 章节来源 + - [backend/app/config.py:7-7](file://backend/app/config.py#L7-L7) + - [docker-compose.yml:8-13](file://docker-compose.yml#L8-L13) + +- Redis 连接失败 + - 现象:任务调度或缓存相关功能异常。 + - 排查:确认 Redis 容器已就绪;检查 REDIS_URL;确认端口映射为 6379。 + - 参考:Redis 默认连接串与容器编排。 + + 章节来源 + - [backend/app/config.py:8-8](file://backend/app/config.py#L8-L8) + - [docker-compose.yml:22-34](file://docker-compose.yml#L22-L34) + +- Playwright 报错(浏览器不可用) + - 现象:抓取任务失败,提示浏览器相关错误。 + - 排查:确认容器内已安装 Playwright 依赖;检查浏览器路径配置;必要时重建后端镜像。 + - 参考:后端 Dockerfile 中 Playwright 安装步骤。 + + 章节来源 + - [backend/Dockerfile:31-33](file://backend/Dockerfile#L31-L33) + +## 结论 +通过本指南,你可以完成 GEO 平台的环境准备、依赖安装与容器化部署,并快速上手核心功能。建议在本地开发时结合健康检查与日志排查,逐步验证认证、查询与引用数据链路,再进行更复杂的任务调度与导出功能验证。 + +## 附录 + +### 环境要求 +- 操作系统:Linux/macOS/Windows(WSL2) +- 容器运行时:Docker Engine 与 Compose 插件 +- 前端运行时:Node.js 20.x +- 后端运行时:Python 3.11 + +章节来源 +- [backend/Dockerfile:1-1](file://backend/Dockerfile#L1-L1) +- [frontend/Dockerfile:1-1](file://frontend/Dockerfile#L1-L1) +- [frontend/package.json:1-40](file://frontend/package.json#L1-L40) + +### 依赖安装步骤 +- 后端依赖 + - 使用 pip 安装 requirements.txt 中的包。 + - 安装 Playwright 浏览器与系统依赖。 +- 前端依赖 + - 使用 npm ci 安装 package.json 中的包。 + +章节来源 +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) +- [backend/Dockerfile:6-33](file://backend/Dockerfile#L6-L33) +- [frontend/package.json:1-40](file://frontend/package.json#L1-L40) +- [frontend/Dockerfile:6-7](file://frontend/Dockerfile#L6-L7) + +### 本地开发环境配置 +- 后端 + - 设置数据库与 Redis 连接字符串(默认来自配置类)。 + - 启动后端服务,监听 8000 端口。 +- 前端 + - 设置 NEXT_PUBLIC_API_URL 指向后端地址。 + - 启动开发服务器,监听 3000 端口。 + +章节来源 +- [backend/app/config.py:7-13](file://backend/app/config.py#L7-L13) +- [backend/app/main.py:24-47](file://backend/app/main.py#L24-L47) +- [frontend/lib/api.ts:1-1](file://frontend/lib/api.ts#L1-L1) +- [frontend/README.md:5-15](file://frontend/README.md#L5-L15) + +### Docker 容器化部署流程 +- 步骤 1:准备环境 + - 确保 Docker 已安装并运行。 +- 步骤 2:构建镜像 + - 后端镜像:基于 Python slim,安装系统与 Python 依赖,预装 Playwright。 + - 前端镜像:基于 Node Alpine,使用 npm ci 安装依赖。 +- 步骤 3:启动容器 + - 使用 docker-compose 启动 db、redis、backend、frontend 四个服务。 + - 等待数据库与 Redis 健康检查通过后,后端与前端启动完成。 +- 步骤 4:访问应用 + - 前端:http://localhost:3000 + - 后端:http://localhost:8000 + +章节来源 +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) +- [backend/Dockerfile:1-41](file://backend/Dockerfile#L1-L41) +- [frontend/Dockerfile:1-15](file://frontend/Dockerfile#L1-L15) + +### 基本使用示例 +- 用户注册与登录 + - 注册:调用认证接口提交用户名、邮箱、密码。 + - 登录:提交邮箱与密码获取访问令牌。 + - 获取当前用户:携带令牌调用“获取当前用户”接口。 +- 创建查询任务 + - 列表:分页获取查询词列表。 + - 创建:提交关键词、目标品牌、平台集合等参数。 + - 更新/删除:按需更新或删除指定查询词。 +- 查看引用数据 + - 列表:按查询词、平台、日期范围分页查询引用记录。 + - 统计:获取引用统计信息。 + - 立即执行:触发一次查询任务。 +- 导出报告 + - 导出 CSV:根据查询 ID 导出对应报告。 + +章节来源 +- [backend/app/api/auth.py:13-42](file://backend/app/api/auth.py#L13-L42) +- [backend/app/api/queries.py:15-85](file://backend/app/api/queries.py#L15-L85) +- [backend/app/api/citations.py:25-77](file://backend/app/api/citations.py#L25-L77) +- [frontend/lib/api.ts:24-56](file://frontend/lib/api.ts#L24-L56) + +### 开发调试方法 +- 后端 + - 使用 Uvicorn 启动并开启热重载,便于快速迭代。 + - 关注健康检查端点与日志输出,定位服务状态。 +- 前端 + - 使用 Next.js 开发服务器,自动刷新页面。 + - 在浏览器开发者工具中观察网络请求与错误信息。 +- 容器 + - 通过 docker-compose logs 查看服务日志。 + - 使用 docker exec 进入容器内部排查依赖与端口占用。 + +章节来源 +- [docker-compose.yml:51-66](file://docker-compose.yml#L51-L66) +- [backend/app/main.py:45-47](file://backend/app/main.py#L45-L47) +- [frontend/README.md:5-15](file://frontend/README.md#L5-L15) \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/扩展与定制/功能扩展.md b/.qoder/repowiki/zh/content/扩展与定制/功能扩展.md new file mode 100644 index 0000000..bc37460 --- /dev/null +++ b/.qoder/repowiki/zh/content/扩展与定制/功能扩展.md @@ -0,0 +1,308 @@ +# 功能扩展 + + +**本文档引用的文件** +- [backend/app/main.py](file://backend/app/main.py) +- [backend/app/api/citations.py](file://backend/app/api/citations.py) +- [backend/app/schemas/citation.py](file://backend/app/schemas/citation.py) +- [backend/app/services/citation.py](file://backend/app/services/citation.py) +- [backend/app/models/query.py](file://backend/app/models/query.py) +- [backend/app/models/citation_record.py](file://backend/app/models/citation_record.py) +- [backend/app/workers/citation_engine.py](file://backend/app/workers/citation_engine.py) +- [backend/app/workers/platforms/base.py](file://backend/app/workers/platforms/base.py) +- [backend/app/workers/platforms/kimi.py](file://backend/app/workers/platforms/kimi.py) +- [backend/app/workers/platforms/wenxin.py](file://backend/app/workers/platforms/wenxin.py) +- [frontend/app/layout.tsx](file://frontend/app/layout.tsx) +- [frontend/lib/api.ts](file://frontend/lib/api.ts) +- [frontend/app/(dashboard)/dashboard/page.tsx](file://frontend/app/(dashboard)/dashboard/page.tsx) +- [frontend/components/ui/button.tsx](file://frontend/components/ui/button.tsx) +- [frontend/components/layout/header.tsx](file://frontend/components/layout/header.tsx) +- [frontend/components/charts/trend-chart.tsx](file://frontend/components/charts/trend-chart.tsx) + + +## 目录 +1. [简介](#简介) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖分析](#依赖分析) +7. [性能考虑](#性能考虑) +8. [故障排查指南](#故障排查指南) +9. [结论](#结论) +10. [附录](#附录) + +## 简介 +本文件面向GEO平台的功能扩展,系统性地说明如何新增API接口(后端路由、数据模型、业务逻辑)、前端页面扩展(Next.js路由、组件与状态管理)、引用检测引擎扩展(新品牌匹配策略、竞争品牌识别算法)、以及UI组件库扩展(新组件开发、样式定制与响应式设计)。文档同时提供流程图、序列图与类图,帮助开发者快速理解并落地实施。 + +## 项目结构 +- 后端采用FastAPI,模块化组织API、模型、服务与工作流;数据库使用SQLAlchemy ORM,迁移通过Alembic管理。 +- 前端采用Next.js App Router,以功能域分层组织页面、组件与工具函数;UI基于Radix UI与Tailwind CSS。 + +```mermaid +graph TB +subgraph "后端" +A["FastAPI 应用
backend/app/main.py"] +B["API 路由
backend/app/api/*"] +C["模型与表结构
backend/app/models/*"] +D["服务层
backend/app/services/*"] +E["工作流与引擎
backend/app/workers/*"] +end +subgraph "前端" +F["应用布局
frontend/app/layout.tsx"] +G["API 客户端
frontend/lib/api.ts"] +H["页面与组件
frontend/app/(dashboard)/*"] +I["UI 组件库
frontend/components/ui/*"] +J["图表组件
frontend/components/charts/*"] +end +A --> B +B --> D +D --> C +D --> E +F --> H +H --> G +H --> I +H --> J +``` + +**图表来源** +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [frontend/app/layout.tsx:1-37](file://frontend/app/layout.tsx#L1-L37) + +**章节来源** +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [frontend/app/layout.tsx:1-37](file://frontend/app/layout.tsx#L1-L37) + +## 核心组件 +- 后端API入口与路由挂载:在应用生命周期内注册认证、查询、引用、报告等路由,并开放健康检查端点。 +- 引用检测引擎:负责品牌匹配、竞争品牌识别、跨平台适配与任务调度。 +- 数据模型:查询、引用记录、任务等核心实体及索引。 +- 服务层:封装数据访问、统计聚合、导出与即时触发逻辑。 +- 前端API客户端:统一处理鉴权头、错误与响应解析。 +- UI组件库:基于Radix UI的可变样式组件,支持主题与尺寸变体。 + +**章节来源** +- [backend/app/main.py:38-47](file://backend/app/main.py#L38-L47) +- [backend/app/workers/citation_engine.py:148-301](file://backend/app/workers/citation_engine.py#L148-L301) +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [backend/app/models/citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) +- [backend/app/services/citation.py:24-201](file://backend/app/services/citation.py#L24-L201) +- [frontend/lib/api.ts:23-57](file://frontend/lib/api.ts#L23-L57) +- [frontend/components/ui/button.tsx:1-57](file://frontend/components/ui/button.tsx#L1-L57) + +## 架构总览 +下图展示从前端到后端的关键交互路径,包括API调用、服务层处理与引擎执行。 + +```mermaid +sequenceDiagram +participant FE as "前端页面
frontend/app/(dashboard)/dashboard/page.tsx" +participant API as "API 客户端
frontend/lib/api.ts" +participant APP as "FastAPI 应用
backend/app/main.py" +participant ROUTER as "引用路由
backend/app/api/citations.py" +participant SVC as "服务层
backend/app/services/citation.py" +participant ENG as "引擎
backend/app/workers/citation_engine.py" +FE->>API : "调用统计数据接口" +API->>APP : "GET /api/v1/citations/stats" +APP->>ROUTER : "转发到引用统计路由" +ROUTER->>SVC : "get_citation_stats(user_id, query_id?)" +SVC->>ENG : "聚合统计含趋势、平台分布" +ENG-->>SVC : "返回统计结果" +SVC-->>ROUTER : "返回统计结果" +ROUTER-->>API : "JSON 响应" +API-->>FE : "渲染图表与卡片" +``` + +**图表来源** +- [frontend/app/(dashboard)/dashboard/page.tsx:30-44](file://frontend/app/(dashboard)/dashboard/page.tsx#L30-L44) +- [frontend/lib/api.ts:46-50](file://frontend/lib/api.ts#L46-L50) +- [backend/app/main.py:38-42](file://backend/app/main.py#L38-L42) +- [backend/app/api/citations.py:49-56](file://backend/app/api/citations.py#L49-L56) +- [backend/app/services/citation.py:76-201](file://backend/app/services/citation.py#L76-L201) +- [backend/app/workers/citation_engine.py:148-301](file://backend/app/workers/citation_engine.py#L148-L301) + +## 详细组件分析 + +### 新增API接口:后端路由、模型与业务逻辑 +- 新增后端路由 + - 在API包中创建新的路由模块,定义路径、参数与响应模型。 + - 在应用入口注册路由并设置前缀与标签。 + - 示例参考:引用路由与统计接口的组织方式。 + + **章节来源** + - [backend/app/api/citations.py:21-78](file://backend/app/api/citations.py#L21-L78) + - [backend/app/main.py:38-42](file://backend/app/main.py#L38-L42) + +- 数据模型扩展 + - 在models目录新增或修改ORM模型,定义字段、索引与外键关系。 + - 使用PostgreSQL JSONB类型存储动态配置(如品牌别名、平台列表)。 + - 示例参考:查询与引用记录模型的字段与索引设计。 + + **章节来源** + - [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) + - [backend/app/models/citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) + +- 业务逻辑实现 + - 在services目录编写数据访问与聚合逻辑,确保权限校验与边界条件处理。 + - 对于复杂统计,使用SQL聚合函数与分组查询,保证性能与准确性。 + - 示例参考:引用统计与CSV导出的实现。 + + **章节来源** + - [backend/app/services/citation.py:76-201](file://backend/app/services/citation.py#L76-L201) + - [backend/app/services/citation.py:237-269](file://backend/app/services/citation.py#L237-L269) + +- Pydantic模型与响应格式 + - 在schemas目录定义请求/响应模型,确保类型安全与序列化一致性。 + - 示例参考:引用列表、统计与运行任务响应模型。 + + **章节来源** + - [backend/app/schemas/citation.py:7-50](file://backend/app/schemas/citation.py#L7-L50) + +- 引擎扩展:品牌匹配与竞争品牌识别 + - 品牌匹配策略:精确匹配、别名匹配、模糊匹配(编辑距离),并返回置信度与上下文片段。 + - 竞争品牌识别:预定义行业品牌库,检测除目标品牌外的其他品牌。 + - 引擎执行流程:按平台顺序执行查询、匹配与记录生成,并更新查询任务状态与下次查询时间。 + + **章节来源** + - [backend/app/workers/citation_engine.py:19-120](file://backend/app/workers/citation_engine.py#L19-L120) + - [backend/app/workers/citation_engine.py:122-146](file://backend/app/workers/citation_engine.py#L122-L146) + - [backend/app/workers/citation_engine.py:159-234](file://backend/app/workers/citation_engine.py#L159-L234) + +- 平台适配器扩展 + - 继承基类实现query方法,处理页面交互、稳定性检测与异常重试。 + - 支持多平台并行扩展,统一由引擎调度。 + + **章节来源** + - [backend/app/workers/platforms/base.py:4-18](file://backend/app/workers/platforms/base.py#L4-L18) + - [backend/app/workers/platforms/kimi.py:33-125](file://backend/app/workers/platforms/kimi.py#L33-L125) + - [backend/app/workers/platforms/wenxin.py](file://backend/app/workers/platforms/wenxin.py) + +### 前端页面扩展:Next.js路由、组件与状态管理 +- Next.js页面路由添加 + - 在App Router中新增页面目录与page组件,使用useSession获取会话信息。 + - 在根布局中注入Provider,确保全局状态可用。 + + **章节来源** + - [frontend/app/layout.tsx:22-36](file://frontend/app/layout.tsx#L22-L36) + - [frontend/app/(dashboard)/dashboard/page.tsx:20-44](file://frontend/app/(dashboard)/dashboard/page.tsx#L20-L44) + +- 组件开发与状态管理集成 + - 使用React Hook进行数据加载与错误处理,结合UI组件库构建卡片与图表。 + - 在图表组件中使用Recharts实现响应式可视化。 + + **章节来源** + - [frontend/components/layout/header.tsx:7-29](file://frontend/components/layout/header.tsx#L7-L29) + - [frontend/components/charts/trend-chart.tsx:22-59](file://frontend/components/charts/trend-chart.tsx#L22-L59) + +- API客户端集成 + - 统一封装fetchWithAuth,自动附加Authorization头并处理错误。 + - 在页面中调用api.citations.getStats加载统计数据。 + + **章节来源** + - [frontend/lib/api.ts:3-21](file://frontend/lib/api.ts#L3-L21) + - [frontend/lib/api.ts:46-50](file://frontend/lib/api.ts#L46-L50) + - [frontend/app/(dashboard)/dashboard/page.tsx:30-44](file://frontend/app/(dashboard)/dashboard/page.tsx#L30-L44) + +### UI组件库扩展指南 +- 新组件开发 + - 基于Radix UI与cva变体系统,定义默认样式与尺寸变体。 + - 使用Slot支持透传子节点,保持语义化与可组合性。 + + **章节来源** + - [frontend/components/ui/button.tsx:7-54](file://frontend/components/ui/button.tsx#L7-L54) + +- 样式定制与响应式设计 + - 使用Tailwind CSS变量与暗色主题适配,确保组件在深色模式下的可读性。 + - 图表组件使用ResponsiveContainer实现自适应宽高。 + + **章节来源** + - [frontend/components/charts/trend-chart.tsx:24-58](file://frontend/components/charts/trend-chart.tsx#L24-L58) + +## 依赖分析 +- 后端模块耦合 + - API层仅依赖服务层;服务层依赖模型与引擎;引擎依赖平台适配器。 + - 数据库索引覆盖常用查询条件,提升统计与分页性能。 + + ```mermaid +graph LR +API["API 层
backend/app/api/*"] --> SVC["服务层
backend/app/services/*"] +SVC --> MODELS["模型层
backend/app/models/*"] +SVC --> ENGINE["引擎层
backend/app/workers/citation_engine.py"] +ENGINE --> ADAPTERS["平台适配器
backend/app/workers/platforms/*"] +``` + +**图表来源** +- [backend/app/api/citations.py:15-19](file://backend/app/api/citations.py#L15-L19) +- [backend/app/services/citation.py:9-12](file://backend/app/services/citation.py#L9-L12) +- [backend/app/workers/citation_engine.py:148-157](file://backend/app/workers/citation_engine.py#L148-L157) +- [backend/app/workers/platforms/base.py:4-18](file://backend/app/workers/platforms/base.py#L4-L18) + +**章节来源** +- [backend/app/models/query.py:50-54](file://backend/app/models/query.py#L50-L54) +- [backend/app/models/citation_record.py:37-41](file://backend/app/models/citation_record.py#L37-L41) + +## 性能考虑 +- 数据库查询优化 + - 为高频过滤字段建立索引(如用户ID、状态、下次查询时间、查询ID、查询时间等)。 + - 使用SQL聚合与分组减少Python侧计算开销。 +- 引擎执行策略 + - 平台查询采用指数退避与稳定性检测,避免频繁失败与重复请求。 + - 将统计聚合与导出逻辑异步化,避免阻塞主流程。 +- 前端渲染优化 + - 图表组件使用ResponsiveContainer与轻量级数据结构,降低重排成本。 + - 页面按需加载与错误兜底,提升用户体验。 + +**章节来源** +- [backend/app/models/query.py:50-54](file://backend/app/models/query.py#L50-L54) +- [backend/app/models/citation_record.py:37-41](file://backend/app/models/citation_record.py#L37-L41) +- [backend/app/workers/platforms/kimi.py:126-197](file://backend/app/workers/platforms/kimi.py#L126-L197) +- [frontend/components/charts/trend-chart.tsx:24-58](file://frontend/components/charts/trend-chart.tsx#L24-L58) + +## 故障排查指南 +- 引擎执行失败 + - 平台适配器抛出异常时,记录错误信息并创建一条cited=False的占位记录,便于追踪。 + - 关闭适配器资源,避免句柄泄漏。 + + **章节来源** + - [backend/app/workers/citation_engine.py:211-228](file://backend/app/workers/citation_engine.py#L211-L228) + - [backend/app/workers/citation_engine.py:302-309](file://backend/app/workers/citation_engine.py#L302-L309) + +- API调用失败 + - 前端API客户端统一处理HTTP错误与JSON解析,抛出可读错误信息。 + - 页面中捕获错误并提供重试入口。 + + **章节来源** + - [frontend/lib/api.ts:16-21](file://frontend/lib/api.ts#L16-L21) + - [frontend/app/(dashboard)/dashboard/page.tsx:54-66](file://frontend/app/(dashboard)/dashboard/page.tsx#L54-L66) + +- 权限与数据隔离 + - 服务层在查询与导出前验证查询归属,防止越权访问。 + + **章节来源** + - [backend/app/services/citation.py:14-22](file://backend/app/services/citation.py#L14-L22) + - [backend/app/services/citation.py:242-244](file://backend/app/services/citation.py#L242-L244) + +## 结论 +通过模块化的后端架构与清晰的前后端职责划分,GEO平台具备良好的扩展性。新增API接口与页面只需遵循现有路由、模型与服务层规范;引擎与适配器的抽象设计使得品牌匹配策略与平台能力可插拔扩展;UI组件库提供了统一的样式与交互体验。建议在扩展过程中严格遵循数据权限、错误处理与性能优化的最佳实践。 + +## 附录 +- 新增品牌匹配策略的实现要点 + - 在BrandMatcher中扩展候选词提取与相似度阈值,支持领域特定规则(如正则匹配、停用词过滤)。 + - 为不同行业维护品牌别名库,提高别名匹配准确率。 + + **章节来源** + - [backend/app/workers/citation_engine.py:19-120](file://backend/app/workers/citation_engine.py#L19-L120) + +- 竞争品牌识别算法的实现要点 + - 基于预定义品牌分类(保险、金融、科技等)进行集合比对,排除目标品牌。 + - 可引入NLP分词与实体识别进一步提升召回质量。 + + **章节来源** + - [backend/app/workers/citation_engine.py:122-146](file://backend/app/workers/citation_engine.py#L122-L146) + +- 平台适配器扩展流程 + - 继承BasePlatformAdapter,实现query方法与可选的close方法。 + - 在引擎中注册平台映射,确保execute_query能够调度新平台。 + + **章节来源** + - [backend/app/workers/platforms/base.py:4-18](file://backend/app/workers/platforms/base.py#L4-L18) + - [backend/app/workers/citation_engine.py:152-157](file://backend/app/workers/citation_engine.py#L152-L157) \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/扩展与定制/扩展与定制.md b/.qoder/repowiki/zh/content/扩展与定制/扩展与定制.md new file mode 100644 index 0000000..32f82ff --- /dev/null +++ b/.qoder/repowiki/zh/content/扩展与定制/扩展与定制.md @@ -0,0 +1,481 @@ +# 扩展与定制 + + +**本文档引用的文件** +- [backend/app/main.py](file://backend/app/main.py) +- [backend/app/config.py](file://backend/app/config.py) +- [backend/app/api/auth.py](file://backend/app/api/auth.py) +- [backend/app/api/queries.py](file://backend/app/api/queries.py) +- [backend/app/api/reports.py](file://backend/app/api/reports.py) +- [backend/app/api/citations.py](file://backend/app/api/citations.py) +- [backend/app/models/query.py](file://backend/app/models/query.py) +- [backend/app/schemas/query.py](file://backend/app/schemas/query.py) +- [backend/app/workers/scheduler.py](file://backend/app/workers/scheduler.py) +- [backend/app/workers/platforms/base.py](file://backend/app/workers/platforms/base.py) +- [backend/app/workers/platforms/kimi.py](file://backend/app/workers/platforms/kimi.py) +- [backend/app/workers/platforms/wenxin.py](file://backend/app/workers/platforms/wenxin.py) +- [backend/app/workers/citation_engine.py](file://backend/app/workers/citation_engine.py) +- [backend/app/services/auth.py](file://backend/app/services/auth.py) +- [backend/app/services/query.py](file://backend/app/services/query.py) +- [backend/app/services/citation.py](file://backend/app/services/citation.py) +- [backend/app/database.py](file://backend/app/database.py) +- [backend/app/api/deps.py](file://backend/app/api/deps.py) +- [backend/app/models/user.py](file://backend/app/models/user.py) +- [backend/app/schemas/auth.py](file://backend/app/schemas/auth.py) +- [backend/app/schemas/citation.py](file://backend/app/schemas/citation.py) +- [backend/app/schemas/query.py](file://backend/app/schemas/query.py) +- [backend/requirements.txt](file://backend/requirements.txt) +- [frontend/app/layout.tsx](file://frontend/app/layout.tsx) +- [frontend/components/providers.tsx](file://frontend/components/providers.tsx) +- [frontend/lib/api.ts](file://frontend/lib/api.ts) +- [frontend/package.json](file://frontend/package.json) +- [docker-compose.yml](file://docker-compose.yml) +- [backend/Dockerfile](file://backend/Dockerfile) +- [frontend/Dockerfile](file://frontend/Dockerfile) +- [backend/alembic/versions/488d0bd5ab01_initial_migration.py](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py) + + +## 目录 +1. [简介](#简介) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖分析](#依赖分析) +7. [性能考虑](#性能考虑) +8. [故障排查指南](#故障排查指南) +9. [结论](#结论) +10. [附录](#附录) + +## 简介 +本文件面向需要对 GEO 平台进行扩展与定制的工程师与产品团队,系统性阐述后端 API 扩展、前端页面扩展、数据模型扩展、配置定制、第三方集成(AI 平台、数据库、认证)以及插件化扩展的最佳实践。文档同时提供可落地的实施建议与案例研究,帮助快速实现业务定制化目标。 + +## 项目结构 +GEO 采用前后端分离架构: +- 后端基于 FastAPI,提供 REST API;通过 Alembic 管理数据库迁移;使用 SQLAlchemy ORM 定义模型;APScheduler 实现定时任务;Playwright 支持 AI 平台网页抓取。 +- 前端基于 Next.js 14,使用 TypeScript、TailwindCSS、Radix UI 组件库;通过自定义 API 封装层与后端交互;NextAuth v4 提供会话管理。 + +```mermaid +graph TB +subgraph "前端" +FE_APP["Next.js 应用
路由与页面"] +FE_LIB["API 封装
lib/api.ts"] +FE_PROV["会话提供者
components/providers.tsx"] +end +subgraph "后端" +BE_MAIN["FastAPI 应用
app/main.py"] +BE_ROUTER_AUTH["认证路由
app/api/auth.py"] +BE_ROUTER_QUERIES["查询路由
app/api/queries.py"] +BE_ROUTER_CITATIONS["引用路由
app/api/citations.py"] +BE_ROUTER_REPORTS["报告路由
app/api/reports.py"] +BE_SCHED["调度器
app/workers/scheduler.py"] +BE_PLAT_BASE["平台适配器基类
app/workers/platforms/base.py"] +BE_PLAT_KIMI["Kimi 适配器
app/workers/platforms/kimi.py"] +BE_PLAT_WENXIN["文心一言适配器
app/workers/platforms/wenxin.py"] +BE_DB["数据库与模型
app/database.py + models/*"] +end +FE_APP --> FE_LIB +FE_LIB --> BE_MAIN +BE_MAIN --> BE_ROUTER_AUTH +BE_MAIN --> BE_ROUTER_QUERIES +BE_MAIN --> BE_ROUTER_CITATIONS +BE_MAIN --> BE_ROUTER_REPORTS +BE_MAIN --> BE_SCHED +BE_SCHED --> BE_PLAT_BASE +BE_PLAT_BASE --> BE_PLAT_KIMI +BE_PLAT_BASE --> BE_PLAT_WENXIN +BE_MAIN --> BE_DB +``` + +图表来源 +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [backend/app/workers/platforms/base.py:1-18](file://backend/app/workers/platforms/base.py#L1-L18) +- [backend/app/workers/platforms/kimi.py:1-206](file://backend/app/workers/platforms/kimi.py#L1-L206) +- [backend/app/workers/platforms/wenxin.py:1-205](file://backend/app/workers/platforms/wenxin.py#L1-L205) +- [backend/app/database.py](file://backend/app/database.py) + +章节来源 +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [frontend/app/layout.tsx:1-37](file://frontend/app/layout.tsx#L1-L37) +- [frontend/components/providers.tsx:1-9](file://frontend/components/providers.tsx#L1-L9) + +## 核心组件 +- API 层:认证、查询词、引用数据、报告导出等模块化路由,统一挂载于主应用。 +- 服务层:封装业务逻辑,如用户认证、查询 CRUD、引用处理等。 +- 数据层:SQLAlchemy 模型与 Pydantic Schema,定义实体与请求/响应结构。 +- 工作器与调度:APScheduler 驱动定时任务,CitationEngine 协调平台适配器执行查询。 +- 前端:Next.js 页面与组件,通过 lib/api.ts 统一访问后端接口;NextAuth 提供会话状态。 + +章节来源 +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/app/api/queries.py:1-86](file://backend/app/api/queries.py#L1-L86) +- [backend/app/api/citations.py](file://backend/app/api/citations.py) +- [backend/app/api/reports.py](file://backend/app/api/reports.py) +- [backend/app/services/auth.py](file://backend/app/services/auth.py) +- [backend/app/services/query.py](file://backend/app/services/query.py) +- [backend/app/services/citation.py](file://backend/app/services/citation.py) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [backend/app/schemas/query.py:1-94](file://backend/app/schemas/query.py#L1-L94) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) + +## 架构总览 +下图展示从浏览器到后端 API、数据库与外部 AI 平台的完整链路: + +```mermaid +sequenceDiagram +participant Browser as "浏览器" +participant Frontend as "前端 Next.js" +participant API as "后端 FastAPI" +participant Svc as "服务层" +participant DB as "数据库" +participant Scheduler as "调度器" +participant Engine as "CitationEngine" +participant Plat as "平台适配器" +Browser->>Frontend : 用户操作 +Frontend->>API : 发起 HTTP 请求 +API->>Svc : 路由分发与校验 +Svc->>DB : 读写数据 +DB-->>Svc : 返回结果 +Svc-->>API : 业务结果 +API-->>Frontend : JSON 响应 +Note over Scheduler,Engine : 定时触发查询执行 +Scheduler->>Engine : 触发执行 +Engine->>Plat : 调用具体平台适配器 +Plat-->>Engine : 返回原始响应 +Engine->>DB : 写入引用记录 +``` + +图表来源 +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [backend/app/workers/platforms/base.py:1-18](file://backend/app/workers/platforms/base.py#L1-L18) +- [backend/app/workers/platforms/kimi.py:1-206](file://backend/app/workers/platforms/kimi.py#L1-L206) +- [backend/app/workers/platforms/wenxin.py:1-205](file://backend/app/workers/platforms/wenxin.py#L1-L205) + +## 详细组件分析 + +### 新增 API 接口扩展指南 +- 路由扩展步骤 + - 在后端 app/api 下新增模块(如 app/api/new_feature.py),定义 APIRouter 并编写路由函数。 + - 在 app/main.py 中引入并挂载路由,指定前缀与标签。 + - 在 app/services 下新增对应服务函数,封装业务逻辑。 + - 在 app/schemas 下新增请求/响应模型,确保字段校验与默认值。 + - 在 app/models 下新增模型(如需持久化)。 + - 在 app/database.py 中注册模型,并在 alembic 迁移中生成/更新表结构。 +- 示例参考路径 + - [认证路由示例:1-43](file://backend/app/api/auth.py#L1-L43) + - [查询路由示例:1-86](file://backend/app/api/queries.py#L1-L86) + - [主应用挂载示例:38-42](file://backend/app/main.py#L38-L42) + - [服务层示例](file://backend/app/services/query.py) + - [Schema 示例:1-94](file://backend/app/schemas/query.py#L1-L94) + - [模型示例:1-55](file://backend/app/models/query.py#L1-L55) + +```mermaid +flowchart TD +Start(["开始"]) --> CreateRouter["创建路由模块
app/api/new_feature.py"] +CreateRouter --> MountRouter["在 app/main.py 挂载路由"] +MountRouter --> AddService["在 app/services 添加服务函数"] +AddService --> AddSchema["在 app/schemas 添加模型"] +AddSchema --> AddModel["在 app/models 添加模型如需"] +AddModel --> Alembic["生成/更新数据库迁移"] +Alembic --> Test["编写单元测试"] +Test --> End(["完成"]) +``` + +章节来源 +- [backend/app/main.py:38-42](file://backend/app/main.py#L38-L42) +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/app/api/queries.py:1-86](file://backend/app/api/queries.py#L1-L86) +- [backend/app/schemas/query.py:1-94](file://backend/app/schemas/query.py#L1-L94) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) + +### 前端页面扩展指南 +- 页面与布局 + - 在 frontend/app/(dashboard)/(或新的组) 下新增页面目录与 page.tsx。 + - 在 frontend/app/layout.tsx 中组织根布局与 Providers。 + - 在 components/ui 下新增或复用 UI 组件,保持一致的设计语言。 +- API 调用 + - 在 frontend/lib/api.ts 中新增方法,遵循现有命名与错误处理模式。 + - 在页面中通过 hooks 或直接调用 api.* 方法获取数据。 +- 会话与权限 + - 使用 frontend/components/providers.tsx 包裹应用,确保 NextAuth 会话可用。 + - 在页面中根据用户状态控制渲染与交互。 + +```mermaid +flowchart TD +NewPage["新增页面
frontend/app/(group)/new/page.tsx"] --> Layout["布局与 Providers
frontend/app/layout.tsx"] +Layout --> UI["UI 组件
frontend/components/ui/*"] +UI --> API["API 封装
frontend/lib/api.ts"] +API --> Backend["后端 API
backend/app/api/*"] +``` + +章节来源 +- [frontend/app/layout.tsx:1-37](file://frontend/app/layout.tsx#L1-L37) +- [frontend/components/providers.tsx:1-9](file://frontend/components/providers.tsx#L1-L9) +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) + +### 数据模型扩展指南 +- 字段与约束 + - 在 app/models 下新增或修改模型,使用 SQLAlchemy 类型与索引策略。 + - 在 app/schemas 下同步新增/变更 Pydantic 模型,确保序列化与校验。 + - 在 app/api 下更新路由的响应模型。 +- 迁移与版本控制 + - 使用 Alembic 生成迁移脚本,维护数据库演进历史。 + - 在生产环境执行迁移,避免破坏性变更。 +- 性能与一致性 + - 对高频查询字段建立索引;合理拆分表与外键关系。 + - 通过服务层统一数据访问,避免绕过校验。 + +```mermaid +erDiagram +QUERIES { +uuid id PK +uuid user_id FK +string keyword +string target_brand +jsonb brand_aliases +jsonb platforms +string frequency +string status +timestamp last_queried_at +timestamp next_query_at +timestamp created_at +timestamp updated_at +} +USERS { +uuid id PK +string email UK +string name +string hashed_password +boolean is_active +timestamp created_at +timestamp updated_at +} +CITATION_RECORDS { +uuid id PK +uuid query_id FK +string platform +text raw_response +timestamp created_at +} +QUERIES ||--o{ CITATION_RECORDS : "包含" +USERS ||--o{ QUERIES : "拥有" +``` + +图表来源 +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [backend/alembic/versions/488d0bd5ab01_initial_migration.py](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py) + +章节来源 +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [backend/app/schemas/query.py:1-94](file://backend/app/schemas/query.py#L1-L94) +- [backend/alembic/versions/488d0bd5ab01_initial_migration.py](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py) + +### 配置定制选项 +- 环境变量 + - 通过 app/config.py 的 Settings 类集中管理,支持 .env 文件覆盖。 + - 关键配置项包括数据库连接、Redis、JWT 密钥与过期时间、Playwright 浏览器路径、第三方平台密钥等。 +- 功能开关与性能参数 + - 平台列表、频率策略、状态枚举在 Schema 中集中校验,便于扩展与限制。 + - 调度周期(每小时)可在 app/workers/scheduler.py 中调整。 + - 前端 NEXT_PUBLIC_API_URL 控制后端域名,lib/api.ts 中统一拼接。 +- 建议 + - 生产环境务必替换默认密钥与数据库密码。 + - 将敏感信息放入 .env 并加入 .gitignore。 + +章节来源 +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [backend/app/schemas/query.py:6-8](file://backend/app/schemas/query.py#L6-L8) +- [backend/app/workers/scheduler.py:32-38](file://backend/app/workers/scheduler.py#L32-L38) +- [frontend/lib/api.ts:1](file://frontend/lib/api.ts#L1) + +### 第三方集成扩展指南 + +#### 新 AI 平台接入 +- 适配器开发 + - 继承 app/workers/platforms/base.py,实现 query 与可选的 close。 + - 在 app/workers/platforms 下新增适配器文件,按现有 Kimi/Wenxin 模式实现页面交互与稳定性检测。 +- 注册与调度 + - 在 app/schemas/query.py 的 VALID_PLATFORMS 中添加新平台枚举值。 + - 在 CitationEngine 中注册新适配器映射,或通过工厂动态加载。 +- 错误与重试 + - 参考现有指数退避与超时处理策略,保证鲁棒性。 + +```mermaid +classDiagram +class BasePlatformAdapter { ++string platform_name ++string platform_url ++query(keyword) str ++close() +} +class KimiAdapter { ++query(keyword) str ++close() +} +class WenxinAdapter { ++query(keyword) str ++close() +} +BasePlatformAdapter <|-- KimiAdapter +BasePlatformAdapter <|-- WenxinAdapter +``` + +图表来源 +- [backend/app/workers/platforms/base.py:1-18](file://backend/app/workers/platforms/base.py#L1-L18) +- [backend/app/workers/platforms/kimi.py:1-206](file://backend/app/workers/platforms/kimi.py#L1-L206) +- [backend/app/workers/platforms/wenxin.py:1-205](file://backend/app/workers/platforms/wenxin.py#L1-L205) + +章节来源 +- [backend/app/workers/platforms/base.py:1-18](file://backend/app/workers/platforms/base.py#L1-L18) +- [backend/app/workers/platforms/kimi.py:1-206](file://backend/app/workers/platforms/kimi.py#L1-L206) +- [backend/app/workers/platforms/wenxin.py:1-205](file://backend/app/workers/platforms/wenxin.py#L1-L205) +- [backend/app/schemas/query.py:6](file://backend/app/schemas/query.py#L6) + +#### 新数据库支持 +- 当前使用 PostgreSQL + asyncpg,若需更换: + - 在 app/database.py 中切换引擎与方言。 + - 更新 app/config.py 中 DATABASE_URL。 + - 在 requirements.txt 中替换驱动包。 + - 重新生成/更新 Alembic 迁移以适配新方言。 + +章节来源 +- [backend/app/database.py](file://backend/app/database.py) +- [backend/app/config.py:7](file://backend/app/config.py#L7) +- [backend/requirements.txt:5-8](file://backend/requirements.txt#L5-L8) + +#### 新认证方式集成 +- 当前使用 JWT 令牌与 NextAuth 会话: + - 后端:app/services/auth.py 生成与校验 JWT。 + - 前端:components/providers.tsx 提供 SessionProvider。 + - 若需 OAuth/SSO:在 NextAuth 配置中新增提供方;后端保持 JWT 令牌发放与校验逻辑不变。 +- 注意事项 + - 保持 Authorization 头格式与后端解析一致。 + - 在 app/api/deps.py 中的依赖注入中校验用户身份。 + +章节来源 +- [backend/app/services/auth.py](file://backend/app/services/auth.py) +- [frontend/components/providers.tsx:1-9](file://frontend/components/providers.tsx#L1-L9) +- [frontend/lib/api.ts:3-21](file://frontend/lib/api.ts#L3-L21) +- [backend/app/api/deps.py](file://backend/app/api/deps.py) + +### 插件系统使用指南与最佳实践 +- 插件化思路 + - 平台适配器采用“插件”式扩展:通过继承基类与工厂/映射注册,实现多平台并行。 + - 调度器与 CitationEngine 作为“核心引擎”,通过适配器接口解耦平台差异。 +- 最佳实践 + - 明确职责边界:路由负责协议与鉴权,服务层负责业务规则,模型负责数据结构。 + - 统一错误处理:前端统一捕获 HTTP 错误并提示;后端抛出明确异常码与消息。 + - 可观测性:为关键流程增加日志与指标,便于定位问题。 + - 安全:严格校验输入、最小权限原则、HTTPS 传输、密钥轮换。 + +章节来源 +- [backend/app/workers/platforms/base.py:1-18](file://backend/app/workers/platforms/base.py#L1-L18) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [frontend/lib/api.ts:16-21](file://frontend/lib/api.ts#L16-L21) + +## 依赖分析 +- 后端依赖 + - Web 框架与 ASGI 服务器:FastAPI + Uvicorn + - 数据库与迁移:SQLAlchemy + Alembic + asyncpg + - 配置与校验:Pydantic + pydantic-settings + python-dotenv + - 认证与安全:python-jose + passlib + multipart + - 任务调度:APScheduler + - 浏览器自动化:Playwright + - HTTP 客户端:httpx +- 前端依赖 + - 框架与 UI:Next.js + Radix UI + TailwindCSS + - 认证:NextAuth v4 + - 图表:Recharts + - 开发工具:TypeScript + ESLint + TailwindCSS + +```mermaid +graph LR +subgraph "后端依赖" +F["FastAPI"] +S["SQLAlchemy/Alembic"] +P["Pydantic/pydantic-settings"] +J["python-jose/passlib"] +R["Redis/APScheduler"] +PW["Playwright"] +H["httpx/python-dotenv"] +end +subgraph "前端依赖" +N["Next.js"] +NA["NextAuth"] +UI["Radix UI/TailwindCSS"] +RC["Recharts"] +end +``` + +图表来源 +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) +- [frontend/package.json:11-27](file://frontend/package.json#L11-L27) + +章节来源 +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) +- [frontend/package.json:11-27](file://frontend/package.json#L11-L27) + +## 性能考虑 +- 数据库 + - 为高频查询字段建立索引;避免 N+1 查询;使用分页参数限制单页规模。 +- API + - 合理设置分页参数(skip/limit),避免一次性返回大量数据。 + - 对热点接口启用缓存(如 Redis)减少重复计算。 +- 定时任务 + - 调度周期可根据业务需求调整;在高负载时降低频率或增加并发控制。 +- 浏览器自动化 + - Playwright 启动成本较高,尽量复用上下文;失败重试与超时控制要合理设置。 +- 前端 + - 按需加载页面与组件;减少不必要的 re-render;利用浏览器缓存与静态资源优化。 + +## 故障排查指南 +- 常见问题定位 + - 后端健康检查:访问 /health 确认服务可用。 + - CORS:确认 app/main.py 中允许的源与方法。 + - 数据库连接:检查 DATABASE_URL 与网络连通性。 + - Playwright:确保已安装浏览器二进制;查看适配器初始化日志。 +- 日志与监控 + - 调度器与平台适配器均输出详细日志,定位失败原因。 + - 前端统一错误处理:lib/api.ts 在请求失败时抛出错误,便于 UI 提示。 +- 快速恢复 + - 重启后端服务与前端构建;检查 .env 配置是否正确;核对迁移是否执行。 + +章节来源 +- [backend/app/main.py:45-47](file://backend/app/main.py#L45-L47) +- [backend/app/main.py:30-36](file://backend/app/main.py#L30-L36) +- [backend/app/config.py:7](file://backend/app/config.py#L7) +- [backend/app/workers/platforms/kimi.py:23-32](file://backend/app/workers/platforms/kimi.py#L23-L32) +- [frontend/lib/api.ts:16-21](file://frontend/lib/api.ts#L16-L21) + +## 结论 +GEO 平台提供了清晰的分层架构与可扩展点:路由层、服务层、数据层与工作器层相互解耦,配合配置中心与前端统一 API 封装,能够高效支撑业务扩展。通过平台适配器插件化、Schema/模型标准化、调度器与任务队列机制,团队可以快速接入新 AI 平台、扩展前端页面与数据模型,并在生产环境中保持稳定与可观测。 + +## 附录 + +### 系统定制化案例研究与实施建议 +- 案例一:接入新 AI 平台 + - 步骤:新增适配器类 → 在 Schema 中注册平台枚举 → 在 CitationEngine 中注册映射 → 编写测试 → 部署与灰度。 + - 建议:先在本地模拟页面交互,再逐步对接真实站点;为不同页面结构准备多套选择器策略。 +- 案例二:新增查询词字段 + - 步骤:在模型与 Schema 中新增字段 → 生成迁移 → 更新路由与服务层逻辑 → 前端页面与表单适配。 + - 建议:使用默认值与非空约束,确保向后兼容。 +- 案例三:前端新增报表页面 + - 步骤:新增页面与路由 → 引入图表组件 → 调用后端报表接口 → 权限控制与数据可视化。 + - 建议:复用现有 UI 组件库,保持设计一致性。 + +### 部署与运行要点 +- 使用 Docker Compose 启动后端与前端服务,确保端口映射与网络互通。 +- 后端 Dockerfile 与 requirements.txt 已配置,注意镜像构建缓存与依赖锁定。 +- 前端 Dockerfile 与 Next.js 版本已固定,构建产物由 Next.js 管理。 + +章节来源 +- [docker-compose.yml](file://docker-compose.yml) +- [backend/Dockerfile](file://backend/Dockerfile) +- [frontend/Dockerfile](file://frontend/Dockerfile) +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) +- [frontend/package.json:11-27](file://frontend/package.json#L11-L27) \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/扩展与定制/第三方集成.md b/.qoder/repowiki/zh/content/扩展与定制/第三方集成.md new file mode 100644 index 0000000..357452b --- /dev/null +++ b/.qoder/repowiki/zh/content/扩展与定制/第三方集成.md @@ -0,0 +1,510 @@ +# 第三方集成 + + +**本文档引用的文件** +- [backend/app/workers/platforms/base.py](file://backend/app/workers/platforms/base.py) +- [backend/app/workers/platforms/kimi.py](file://backend/app/workers/platforms/kimi.py) +- [backend/app/workers/platforms/wenxin.py](file://backend/app/workers/platforms/wenxin.py) +- [backend/app/workers/citation_engine.py](file://backend/app/workers/citation_engine.py) +- [backend/app/workers/scheduler.py](file://backend/app/workers/scheduler.py) +- [backend/app/api/auth.py](file://backend/app/api/auth.py) +- [backend/app/services/auth.py](file://backend/app/services/auth.py) +- [backend/app/api/queries.py](file://backend/app/api/queries.py) +- [backend/app/models/user.py](file://backend/app/models/user.py) +- [backend/app/models/query.py](file://backend/app/models/query.py) +- [backend/app/models/query_task.py](file://backend/app/models/query_task.py) +- [backend/app/config.py](file://backend/app/config.py) +- [backend/app/database.py](file://backend/app/database.py) +- [backend/alembic/versions/488d0bd5ab01_initial_migration.py](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py) +- [backend/app/schemas/auth.py](file://backend/app/schemas/auth.py) + + +## 目录 +1. [简介](#简介) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖分析](#依赖分析) +7. [性能考虑](#性能考虑) +8. [故障排查指南](#故障排查指南) +9. [结论](#结论) +10. [附录](#附录) + +## 简介 +本指南面向需要在 GEO 平台上集成新的第三方能力的开发者,覆盖以下主题: +- 新 AI 平台接入:适配器接口实现、平台认证配置与查询逻辑适配 +- 新数据库支持:SQLAlchemy 模型扩展、迁移脚本编写与连接配置 +- 新认证方式集成:OAuth 提供商配置、JWT 令牌处理与权限系统扩展 +- 插件系统使用:插件注册机制、生命周期管理与错误处理 +- 实战示例与常见问题解决方案 + +## 项目结构 +后端采用 FastAPI + SQLAlchemy Async + APScheduler 的架构,核心模块如下: +- workers:爬虫式 AI 平台适配器与引用检测引擎、定时调度器 +- api:FastAPI 路由层,负责请求入口与权限校验 +- services:业务服务层,封装认证、查询等核心逻辑 +- models/schemas:数据模型与请求/响应模型 +- database/config:数据库连接与配置 +- alembic:数据库迁移工具 + +```mermaid +graph TB +subgraph "API 层" +RAuth["路由: 认证 /api/auth/*"] +RQueries["路由: 查询 /api/queries/*"] +end +subgraph "服务层" +SAuth["服务: 认证"] +SQuery["服务: 查询"] +end +subgraph "工作器" +CE["引擎: 引用检测"] +SCH["调度器: 定时任务"] +ADP["适配器: AI 平台"] +end +subgraph "数据层" +DB["数据库: Async Engine"] +MUser["模型: User"] +MQuery["模型: Query"] +MTask["模型: QueryTask"] +end +RAuth --> SAuth +RQueries --> SQuery +SQuery --> CE +SCH --> CE +CE --> ADP +SAuth --> DB +SQuery --> DB +CE --> DB +DB --> MUser +DB --> MQuery +DB --> MTask +``` + +图表来源 +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/app/api/queries.py:1-86](file://backend/app/api/queries.py#L1-L86) +- [backend/app/services/auth.py:1-69](file://backend/app/services/auth.py#L1-L69) +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/models/user.py:1-41](file://backend/app/models/user.py#L1-L41) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [backend/app/models/query_task.py:1-39](file://backend/app/models/query_task.py#L1-L39) + +章节来源 +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/app/api/queries.py:1-86](file://backend/app/api/queries.py#L1-L86) +- [backend/app/services/auth.py:1-69](file://backend/app/services/auth.py#L1-L69) +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/models/user.py:1-41](file://backend/app/models/user.py#L1-L41) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [backend/app/models/query_task.py:1-39](file://backend/app/models/query_task.py#L1-L39) + +## 核心组件 +- 适配器接口与具体实现:统一的 AI 平台查询接口,内置 Kimi 与文心一言适配器 +- 引用检测引擎:对平台返回内容进行品牌引用检测与竞争品牌识别 +- 定时调度器:基于 APScheduler 的周期性查询任务调度 +- 认证与权限:基于 JWT 的用户认证与权限控制 +- 数据模型与迁移:基于 SQLAlchemy Async 的模型与 Alembic 迁移 + +章节来源 +- [backend/app/workers/platforms/base.py:1-18](file://backend/app/workers/platforms/base.py#L1-L18) +- [backend/app/workers/platforms/kimi.py:1-206](file://backend/app/workers/platforms/kimi.py#L1-L206) +- [backend/app/workers/platforms/wenxin.py:1-205](file://backend/app/workers/platforms/wenxin.py#L1-L205) +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [backend/app/services/auth.py:1-69](file://backend/app/services/auth.py#L1-L69) +- [backend/app/models/user.py:1-41](file://backend/app/models/user.py#L1-L41) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [backend/app/models/query_task.py:1-39](file://backend/app/models/query_task.py#L1-L39) +- [backend/alembic/versions/488d0bd5ab01_initial_migration.py:1-128](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L1-L128) + +## 架构总览 +下图展示从 API 请求到定时任务执行、再到 AI 平台查询与结果入库的整体流程。 + +```mermaid +sequenceDiagram +participant Client as "客户端" +participant API as "FastAPI 路由" +participant Service as "业务服务" +participant Scheduler as "定时调度器" +participant Engine as "引用检测引擎" +participant Adapter as "AI 平台适配器" +participant DB as "数据库" +Client->>API : "登录/注册/查询请求" +API->>Service : "认证/授权/业务处理" +Service->>DB : "读写模型数据" +Note over Scheduler : "定时触发" +Scheduler->>Engine : "执行到期查询" +Engine->>Adapter : "调用平台查询" +Adapter-->>Engine : "返回原始响应" +Engine->>DB : "写入引用记录/任务状态" +Engine-->>Scheduler : "更新查询时间" +``` + +图表来源 +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/app/api/queries.py:1-86](file://backend/app/api/queries.py#L1-L86) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) +- [backend/app/workers/platforms/base.py:1-18](file://backend/app/workers/platforms/base.py#L1-L18) + +## 详细组件分析 + +### 组件A:AI 平台适配器体系 +- 接口设计:抽象基类定义统一的平台名称、URL 与查询接口 +- 具体实现:Kimi 与文心一言适配器均继承基类,实现稳定的页面交互与响应等待逻辑 +- 错误处理:重试机制、指数退避、超时处理与资源清理 +- 扩展建议:新增平台时,遵循相同接口与错误处理模式 + +```mermaid +classDiagram +class BasePlatformAdapter { ++string platform_name ++string platform_url ++query(keyword) str ++close() void +} +class KimiAdapter { ++platform_name = "kimi" ++platform_url = "https : //kimi.moonshot.cn" ++query(keyword) str ++close() void +} +class WenxinAdapter { ++platform_name = "wenxin" ++platform_url = "https : //yiyan.baidu.com" ++query(keyword) str ++close() void +} +BasePlatformAdapter <|-- KimiAdapter +BasePlatformAdapter <|-- WenxinAdapter +``` + +图表来源 +- [backend/app/workers/platforms/base.py:1-18](file://backend/app/workers/platforms/base.py#L1-L18) +- [backend/app/workers/platforms/kimi.py:1-206](file://backend/app/workers/platforms/kimi.py#L1-L206) +- [backend/app/workers/platforms/wenxin.py:1-205](file://backend/app/workers/platforms/wenxin.py#L1-L205) + +章节来源 +- [backend/app/workers/platforms/base.py:1-18](file://backend/app/workers/platforms/base.py#L1-L18) +- [backend/app/workers/platforms/kimi.py:1-206](file://backend/app/workers/platforms/kimi.py#L1-L206) +- [backend/app/workers/platforms/wenxin.py:1-205](file://backend/app/workers/platforms/wenxin.py#L1-L205) + +### 组件B:引用检测引擎与查询任务 +- 引擎职责:遍历查询配置的平台,执行查询与品牌匹配,记录结果与任务状态 +- 品牌匹配:精确/别名/模糊匹配,并提取置信度与上下文片段 +- 竞争品牌检测:基于预设品牌库识别文本中的竞品 +- 任务管理:为每个查询与平台维护独立的任务记录,支持失败重试与状态追踪 + +```mermaid +flowchart TD +Start(["开始执行查询"]) --> Init["初始化 BrandMatcher 与平台列表"] +Init --> Loop{"遍历平台"} +Loop --> |执行| Single["执行单平台查询"] +Single --> Detect["品牌匹配与竞品检测"] +Detect --> Record["写入引用记录与任务状态"] +Record --> Next{"还有平台?"} +Next --> |是| Loop +Next --> |否| Update["更新查询下次执行时间"] +Update --> End(["结束"]) +``` + +图表来源 +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) + +章节来源 +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [backend/app/models/query_task.py:1-39](file://backend/app/models/query_task.py#L1-L39) + +### 组件C:定时调度器 +- 触发策略:每小时检查一次到期查询 +- 执行流程:定位状态为 active 且 next_query_at 已到达的查询,委派引擎执行 +- 生命周期:启动/关闭时清理资源,优雅退出 + +```mermaid +sequenceDiagram +participant Timer as "APScheduler" +participant Scheduler as "QueryScheduler" +participant DB as "数据库" +participant Engine as "CitationEngine" +Timer->>Scheduler : "定时触发" +Scheduler->>DB : "查询到期的 Query" +DB-->>Scheduler : "返回待执行查询集合" +loop "逐条执行" +Scheduler->>Engine : "execute_query(query)" +Engine-->>Scheduler : "返回引用记录" +end +``` + +图表来源 +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) + +章节来源 +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) + +### 组件D:认证与权限系统 +- 用户模型:包含邮箱、密码哈希、计划与配额等字段 +- 登录/注册:服务层进行密码哈希与校验,生成 JWT +- 路由保护:通过依赖注入获取当前用户,实现基于角色的访问控制 +- 权限扩展:可基于用户计划与配额限制查询频率与数量 + +```mermaid +sequenceDiagram +participant Client as "客户端" +participant API as "认证路由" +participant Service as "认证服务" +participant DB as "数据库" +Client->>API : "POST /api/auth/login" +API->>Service : "authenticate_user(email, password)" +Service->>DB : "查询用户并校验密码" +DB-->>Service : "返回用户信息" +Service-->>API : "生成 JWT" +API-->>Client : "返回 TokenResponse" +``` + +图表来源 +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/app/services/auth.py:1-69](file://backend/app/services/auth.py#L1-L69) +- [backend/app/models/user.py:1-41](file://backend/app/models/user.py#L1-L41) +- [backend/app/schemas/auth.py:1-34](file://backend/app/schemas/auth.py#L1-L34) + +章节来源 +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/app/services/auth.py:1-69](file://backend/app/services/auth.py#L1-L69) +- [backend/app/models/user.py:1-41](file://backend/app/models/user.py#L1-L41) +- [backend/app/schemas/auth.py:1-34](file://backend/app/schemas/auth.py#L1-L34) + +### 组件E:数据库模型与迁移 +- 模型关系:User 与 Query、Subscription;Query 与 CitationRecord、QueryTask +- 字段设计:JSONB 存储平台列表与别名,UUID 主键,索引优化查询 +- 迁移脚本:初始版本包含 users、queries、citation_records、query_tasks、subscriptions 表及索引 + +```mermaid +erDiagram +USERS { +uuid id PK +string email UK +string password_hash +string name +string plan +int max_queries +boolean is_active +timestamp created_at +timestamp updated_at +} +QUERIES { +uuid id PK +uuid user_id FK +string keyword +string target_brand +jsonb brand_aliases +jsonb platforms +string frequency +string status +timestamp last_queried_at +timestamp next_query_at +timestamp created_at +timestamp updated_at +} +QUERY_TASKS { +uuid id PK +uuid query_id FK +string platform +string status +text error_message +timestamp scheduled_at +timestamp started_at +timestamp completed_at +} +CITATION_RECORDS { +uuid id PK +uuid query_id FK +string platform +boolean cited +int citation_position +text citation_text +jsonb competitor_brands +text raw_response +timestamp queried_at +} +SUBSCRIPTIONS { +uuid id PK +uuid user_id FK +string plan +string status +date start_date +date end_date +numeric amount +string payment_method +string payment_id +timestamp created_at +} +USERS ||--o{ QUERIES : "拥有" +USERS ||--o{ SUBSCRIPTIONS : "拥有" +QUERIES ||--o{ QUERY_TASKS : "包含" +QUERIES ||--o{ CITATION_RECORDS : "产生" +``` + +图表来源 +- [backend/app/models/user.py:1-41](file://backend/app/models/user.py#L1-L41) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [backend/app/models/query_task.py:1-39](file://backend/app/models/query_task.py#L1-L39) +- [backend/alembic/versions/488d0bd5ab01_initial_migration.py:21-128](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L21-L128) + +章节来源 +- [backend/app/models/user.py:1-41](file://backend/app/models/user.py#L1-L41) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [backend/app/models/query_task.py:1-39](file://backend/app/models/query_task.py#L1-L39) +- [backend/alembic/versions/488d0bd5ab01_initial_migration.py:1-128](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L1-L128) + +## 依赖分析 +- 组件耦合:引擎依赖适配器接口;调度器依赖引擎;API 依赖服务层;服务层依赖数据库 +- 外部依赖:FastAPI、SQLAlchemy Async、APScheduler、Pydantic、JWTS、Passlib、Playwright +- 配置集中:数据库 URL、Redis URL、JWT 秘钥与过期时间、平台 API Key 等集中于配置类 + +```mermaid +graph LR +APIAuth["API: 认证"] --> SAuth["服务: 认证"] +APIQueries["API: 查询"] --> SQuery["服务: 查询"] +SQuery --> CE["引擎: 引用检测"] +SCH["调度器: 定时任务"] --> CE +CE --> ADP["适配器: AI 平台"] +SAuth --> DB["数据库: Async Engine"] +SQuery --> DB +CE --> DB +DB --> CFG["配置: Settings"] +``` + +图表来源 +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/app/api/queries.py:1-86](file://backend/app/api/queries.py#L1-L86) +- [backend/app/services/auth.py:1-69](file://backend/app/services/auth.py#L1-L69) +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) + +章节来源 +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) + +## 性能考虑 +- 引擎并发:适配器查询采用异步与重试策略,避免阻塞;建议在高并发场景下增加适配器池与限流 +- 数据库索引:对查询表的关键字段建立索引,减少调度器扫描成本 +- 缓存策略:可引入 Redis 缓存热点查询结果与任务状态 +- 超时与退避:适配器已内置超时与指数退避,建议结合平台限速策略调整重试参数 + +## 故障排查指南 +- 适配器启动失败(Playwright 浏览器未安装) + - 现象:启动浏览器时报错,提示需安装 Chromium + - 处理:按照日志提示安装浏览器镜像 + - 参考路径:[backend/app/workers/platforms/kimi.py:21-32](file://backend/app/workers/platforms/kimi.py#L21-L32)、[backend/app/workers/platforms/wenxin.py:21-32](file://backend/app/workers/platforms/wenxin.py#L21-L32) +- 页面元素找不到导致查询失败 + - 现象:无法定位输入框或发送按钮 + - 处理:检查页面选择器策略,必要时更新选择器集合 + - 参考路径:[backend/app/workers/platforms/kimi.py:67-111](file://backend/app/workers/platforms/kimi.py#L67-L111)、[backend/app/workers/platforms/wenxin.py:67-112](file://backend/app/workers/platforms/wenxin.py#L67-L112) +- 引擎执行异常 + - 现象:平台查询抛出异常,任务状态标记为 failed + - 处理:查看任务错误消息,确认网络与平台可用性 + - 参考路径:[backend/app/workers/citation_engine.py:211-228](file://backend/app/workers/citation_engine.py#L211-L228) +- 认证失败 + - 现象:登录返回 401,提示邮箱或密码错误 + - 处理:确认用户存在且密码正确,检查 JWT 秘钥与过期时间 + - 参考路径:[backend/app/api/auth.py:22-37](file://backend/app/api/auth.py#L22-L37)、[backend/app/services/auth.py:55-69](file://backend/app/services/auth.py#L55-L69) +- 数据库连接异常 + - 现象:连接字符串错误或数据库不可达 + - 处理:核对 DATABASE_URL,确保数据库服务正常 + - 参考路径:[backend/app/config.py](file://backend/app/config.py#L7)、[backend/app/database.py:6-10](file://backend/app/database.py#L6-L10) + +章节来源 +- [backend/app/workers/platforms/kimi.py:21-32](file://backend/app/workers/platforms/kimi.py#L21-L32) +- [backend/app/workers/platforms/wenxin.py:21-32](file://backend/app/workers/platforms/wenxin.py#L21-L32) +- [backend/app/workers/citation_engine.py:211-228](file://backend/app/workers/citation_engine.py#L211-L228) +- [backend/app/api/auth.py:22-37](file://backend/app/api/auth.py#L22-L37) +- [backend/app/services/auth.py:55-69](file://backend/app/services/auth.py#L55-L69) +- [backend/app/config.py](file://backend/app/config.py#L7) +- [backend/app/database.py:6-10](file://backend/app/database.py#L6-L10) + +## 结论 +本指南提供了 GEO 平台集成第三方能力的系统化方法论与实践路径。通过适配器接口与引擎解耦、定时调度与任务状态管理、认证与权限控制以及数据库模型与迁移规范,开发者可以快速、安全地扩展平台能力。建议在生产环境中结合缓存、限流与监控进一步完善整体稳定性与可观测性。 + +## 附录 + +### 新 AI 平台接入流程(步骤清单) +- 实现适配器 + - 继承抽象基类,定义平台名称与 URL + - 实现查询方法,包含重试、超时与资源清理 + - 参考路径:[backend/app/workers/platforms/base.py:1-18](file://backend/app/workers/platforms/base.py#L1-L18)、[backend/app/workers/platforms/kimi.py:1-206](file://backend/app/workers/platforms/kimi.py#L1-L206)、[backend/app/workers/platforms/wenxin.py:1-205](file://backend/app/workers/platforms/wenxin.py#L1-L205) +- 注册到引擎 + - 在引擎平台映射中加入新适配器实例 + - 参考路径:[backend/app/workers/citation_engine.py:152-155](file://backend/app/workers/citation_engine.py#L152-L155) +- 配置与部署 + - 确认 Playwright 浏览器可用,或按需替换为 API 方案 + - 参考路径:[backend/app/config.py](file://backend/app/config.py#L11) + +章节来源 +- [backend/app/workers/platforms/base.py:1-18](file://backend/app/workers/platforms/base.py#L1-L18) +- [backend/app/workers/platforms/kimi.py:1-206](file://backend/app/workers/platforms/kimi.py#L1-L206) +- [backend/app/workers/platforms/wenxin.py:1-205](file://backend/app/workers/platforms/wenxin.py#L1-L205) +- [backend/app/workers/citation_engine.py:152-155](file://backend/app/workers/citation_engine.py#L152-L155) +- [backend/app/config.py](file://backend/app/config.py#L11) + +### 新数据库支持(步骤清单) +- 扩展模型 + - 在 models 目录新增 SQLAlchemy 模型,继承 Base + - 参考路径:[backend/app/models/user.py:1-41](file://backend/app/models/user.py#L1-L41)、[backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55)、[backend/app/models/query_task.py:1-39](file://backend/app/models/query_task.py#L1-L39) +- 编写迁移 + - 使用 Alembic 生成并编写迁移脚本,包含索引与约束 + - 参考路径:[backend/alembic/versions/488d0bd5ab01_initial_migration.py:21-128](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L21-L128) +- 连接配置 + - 在配置类中设置 DATABASE_URL,确保连接参数正确 + - 参考路径:[backend/app/config.py](file://backend/app/config.py#L7)、[backend/app/database.py:6-10](file://backend/app/database.py#L6-L10) + +章节来源 +- [backend/app/models/user.py:1-41](file://backend/app/models/user.py#L1-L41) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [backend/app/models/query_task.py:1-39](file://backend/app/models/query_task.py#L1-L39) +- [backend/alembic/versions/488d0bd5ab01_initial_migration.py:21-128](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L21-L128) +- [backend/app/config.py](file://backend/app/config.py#L7) +- [backend/app/database.py:6-10](file://backend/app/database.py#L6-L10) + +### 新认证方式集成(步骤清单) +- OAuth 集成 + - 在认证服务中新增 OAuth 登录流程,返回用户信息与 JWT + - 参考路径:[backend/app/services/auth.py:37-52](file://backend/app/services/auth.py#L37-L52)、[backend/app/api/auth.py:13-19](file://backend/app/api/auth.py#L13-L19) +- JWT 令牌处理 + - 使用现有 JWT 工具函数生成与验证令牌 + - 参考路径:[backend/app/services/auth.py:24-34](file://backend/app/services/auth.py#L24-L34) +- 权限系统扩展 + - 在路由层通过依赖注入获取当前用户,按用户计划与配额控制访问 + - 参考路径:[backend/app/api/auth.py:40-43](file://backend/app/api/auth.py#L40-L43)、[backend/app/models/user.py:22-24](file://backend/app/models/user.py#L22-L24) + +章节来源 +- [backend/app/services/auth.py:24-34](file://backend/app/services/auth.py#L24-L34) +- [backend/app/api/auth.py:13-19](file://backend/app/api/auth.py#L13-L19) +- [backend/app/api/auth.py:40-43](file://backend/app/api/auth.py#L40-L43) +- [backend/app/models/user.py:22-24](file://backend/app/models/user.py#L22-L24) + +### 插件系统使用指南(概念性说明) +- 注册机制 + - 将插件适配器注册到引擎映射,实现动态扩展 + - 参考路径:[backend/app/workers/citation_engine.py:152-155](file://backend/app/workers/citation_engine.py#L152-L155) +- 生命周期管理 + - 在调度器启动/关闭时统一管理插件资源 + - 参考路径:[backend/app/workers/scheduler.py:30-40](file://backend/app/workers/scheduler.py#L30-L40)、[backend/app/workers/scheduler.py:86-90](file://backend/app/workers/scheduler.py#L86-L90) +- 错误处理 + - 插件异常不影响全局流程,记录错误并继续执行其他插件 + - 参考路径:[backend/app/workers/citation_engine.py:211-228](file://backend/app/workers/citation_engine.py#L211-L228) + +章节来源 +- [backend/app/workers/citation_engine.py:152-155](file://backend/app/workers/citation_engine.py#L152-L155) +- [backend/app/workers/scheduler.py:30-40](file://backend/app/workers/scheduler.py#L30-L40) +- [backend/app/workers/scheduler.py:86-90](file://backend/app/workers/scheduler.py#L86-L90) +- [backend/app/workers/citation_engine.py:211-228](file://backend/app/workers/citation_engine.py#L211-L228) \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/扩展与定制/配置定制.md b/.qoder/repowiki/zh/content/扩展与定制/配置定制.md new file mode 100644 index 0000000..d6c15ea --- /dev/null +++ b/.qoder/repowiki/zh/content/扩展与定制/配置定制.md @@ -0,0 +1,329 @@ +# 配置定制 + + +**本文档引用的文件** +- [backend/app/config.py](file://backend/app/config.py) +- [backend/app/database.py](file://backend/app/database.py) +- [backend/app/main.py](file://backend/app/main.py) +- [backend/app/workers/scheduler.py](file://backend/app/workers/scheduler.py) +- [backend/app/workers/citation_engine.py](file://backend/app/workers/citation_engine.py) +- [backend/app/workers/platforms/base.py](file://backend/app/workers/platforms/base.py) +- [backend/app/workers/platforms/kimi.py](file://backend/app/workers/platforms/kimi.py) +- [backend/app/workers/platforms/wenxin.py](file://backend/app/workers/platforms/wenxin.py) +- [backend/requirements.txt](file://backend/requirements.txt) +- [docker-compose.yml](file://docker-compose.yml) +- [frontend/tailwind.config.ts](file://frontend/tailwind.config.ts) +- [frontend/postcss.config.mjs](file://frontend/postcss.config.mjs) +- [frontend/next.config.mjs](file://frontend/next.config.mjs) +- [frontend/package.json](file://frontend/package.json) + + +## 目录 +1. [简介](#简介) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖分析](#依赖分析) +7. [性能考虑](#性能考虑) +8. [故障排查指南](#故障排查指南) +9. [结论](#结论) +10. [附录](#附录) + +## 简介 +本文件面向GEO项目的配置定制需求,系统性梳理后端环境变量配置、数据库与缓存连接、AI平台API密钥管理、功能开关与热更新机制、性能调优参数,以及前端主题定制(Tailwind CSS、颜色方案、响应式断点)。同时给出生产环境最佳实践与安全配置建议,帮助团队在不同部署环境中快速、安全地完成配置落地。 + +## 项目结构 +- 后端采用FastAPI + SQLAlchemy异步ORM + APScheduler异步调度,配置通过Pydantic Settings集中管理,支持.env文件注入。 +- 前端基于Next.js 14 + Tailwind CSS,PostCSS负责Tailwind集成,主题变量通过CSS自定义属性扩展。 +- Docker Compose编排数据库(PostgreSQL)、缓存(Redis)与前后端服务,统一挂载.env以注入环境变量。 + +```mermaid +graph TB +subgraph "后端" +CFG["配置模块
Settings"] +DB["数据库引擎
AsyncEngine"] +SCH["调度器
AsyncIOScheduler"] +CE["引用引擎
CitationEngine"] +KIMI["Kimi适配器"] +WENXIN["文心一言适配器"] +end +subgraph "前端" +TW["Tailwind配置
tailwind.config.ts"] +PC["PostCSS配置
postcss.config.mjs"] +NEXT["Next配置
next.config.mjs"] +end +subgraph "基础设施" +PG["PostgreSQL"] +RD["Redis"] +end +CFG --> DB +CFG --> RD +CFG --> CE +CE --> KIMI +CE --> WENXIN +SCH --> CE +DB --> PG +RD -.-> 应用 +TW --> 应用 +PC --> TW +NEXT --> 应用 +``` + +图表来源 +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) +- [backend/app/workers/platforms/kimi.py:1-206](file://backend/app/workers/platforms/kimi.py#L1-L206) +- [backend/app/workers/platforms/wenxin.py:1-205](file://backend/app/workers/platforms/wenxin.py#L1-L205) +- [frontend/tailwind.config.ts:1-57](file://frontend/tailwind.config.ts#L1-L57) +- [frontend/postcss.config.mjs:1-9](file://frontend/postcss.config.mjs#L1-L9) +- [frontend/next.config.mjs:1-5](file://frontend/next.config.mjs#L1-L5) + +章节来源 +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [frontend/tailwind.config.ts:1-57](file://frontend/tailwind.config.ts#L1-L57) +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) + +## 核心组件 +- 配置中心(Settings):集中声明数据库、缓存、JWT、浏览器路径、AI平台密钥等关键参数,并通过.env文件注入。 +- 数据库层:基于SQLAlchemy异步引擎,提供会话工厂与基础模型元数据。 +- 引擎与调度:CitationEngine负责跨平台查询与品牌匹配;QueryScheduler周期性触发任务。 +- 前端主题:Tailwind CSS通过CSS变量扩展颜色与圆角体系,PostCSS集成Tailwind,Next配置保持默认。 + +章节来源 +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) +- [backend/app/workers/scheduler.py:25-95](file://backend/app/workers/scheduler.py#L25-L95) +- [frontend/tailwind.config.ts:10-54](file://frontend/tailwind.config.ts#L10-L54) + +## 架构总览 +下图展示配置如何贯穿应用生命周期:从.env注入Settings,再到数据库与缓存连接、AI平台适配器初始化、调度器启动与任务执行。 + +```mermaid +sequenceDiagram +participant ENV as ".env" +participant S as "Settings" +participant DB as "数据库引擎" +participant RED as "Redis" +participant CE as "CitationEngine" +participant SCH as "QueryScheduler" +participant K as "Kimi适配器" +participant W as "文心一言适配器" +ENV-->>S : 注入键值对 +S-->>DB : 提供DATABASE_URL +S-->>RED : 提供REDIS_URL +S-->>CE : 提供API密钥(可选) +SCH->>CE : 触发任务执行 +CE->>K : query(keyword) +CE->>W : query(keyword) +K-->>CE : 原始响应文本 +W-->>CE : 原始响应文本 +CE-->>DB : 写入引用记录 +CE-->>RED : 可选缓存写入 +``` + +图表来源 +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) +- [backend/app/workers/platforms/kimi.py:33-125](file://backend/app/workers/platforms/kimi.py#L33-L125) +- [backend/app/workers/platforms/wenxin.py:33-124](file://backend/app/workers/platforms/wenxin.py#L33-L124) + +## 详细组件分析 + +### 环境变量与配置管理 +- 配置来源:Settings通过.env文件注入,支持额外键忽略策略,便于本地与CI/CD环境解耦。 +- 关键参数: + - 数据库连接:DATABASE_URL + - 缓存连接:REDIS_URL + - 安全相关:JWT_SECRET、JWT_EXPIRE_HOURS + - 浏览器自动化:PLAYWRIGHT_BROWSERS_PATH + - AI平台密钥:ZHIPU_API_KEY、TONGYI_API_KEY(当前为空,需在生产环境注入) + +```mermaid +classDiagram +class Settings { ++DATABASE_URL : string ++REDIS_URL : string ++JWT_SECRET : string ++JWT_EXPIRE_HOURS : number ++PLAYWRIGHT_BROWSERS_PATH : string ++ZHIPU_API_KEY : string ++TONGYI_API_KEY : string +} +``` + +图表来源 +- [backend/app/config.py:4-16](file://backend/app/config.py#L4-L16) + +章节来源 +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [docker-compose.yml:42-43](file://docker-compose.yml#L42-L43) + +### 数据库连接配置 +- 引擎创建:使用异步引擎,echo关闭,future开启。 +- 会话工厂:设置过期与自动提交/刷洗策略,保证事务一致性。 +- 依赖注入:通过依赖函数提供会话,确保请求生命周期内正确释放。 + +```mermaid +flowchart TD +Start(["应用启动"]) --> LoadCfg["读取Settings.DATABASE_URL"] +LoadCfg --> CreateEngine["创建异步引擎"] +CreateEngine --> SessionFactory["创建会话工厂"] +SessionFactory --> ProvideDB["依赖注入会话"] +ProvideDB --> End(["请求处理完成,关闭会话"]) +``` + +图表来源 +- [backend/app/database.py:6-28](file://backend/app/database.py#L6-L28) + +章节来源 +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) + +### Redis缓存配置 +- 连接字符串:REDIS_URL由Settings提供。 +- 使用场景:可作为任务状态、查询结果或会话存储的缓存层(当前代码未直接使用Redis,可在业务层扩展)。 +- 建议:生产环境务必独立Redis实例,设置密码与网络隔离。 + +章节来源 +- [backend/app/config.py:8](file://backend/app/config.py#L8) +- [backend/requirements.txt:21](file://backend/requirements.txt#L21) + +### AI平台API密钥管理 +- 当前实现:Settings声明密钥字段,但默认为空;引用引擎在执行平台查询时依赖适配器封装的内部逻辑。 +- 建议:在生产环境通过.env注入密钥,或通过机密管理服务挂载至容器。 + +章节来源 +- [backend/app/config.py:12-13](file://backend/app/config.py#L12-L13) +- [backend/app/workers/citation_engine.py:244-246](file://backend/app/workers/citation_engine.py#L244-L246) + +### 功能开关与动态配置 +- 现状:代码未实现运行时功能开关与配置热更新。 +- 建议实现方式: + - 引入配置中心(如Consul、etcd或PostgreSQL表),定期拉取最新配置。 + - 使用装饰器或中间件在请求入口处进行功能开关判断。 + - 结合信号或健康检查接口触发重载,避免重启服务。 + +章节来源 +- [backend/app/main.py:13-21](file://backend/app/main.py#L13-L21) + +### 性能调优参数 +- 数据库连接池:可通过异步引擎参数调整最大连接数、空闲连接数、连接超时等(当前未显式配置,使用默认值)。 +- 异步任务并发:调度器周期固定为1小时;平台查询采用指数退避与单次任务串行化,避免过度并发。 +- 缓存策略:建议在业务层引入Redis缓存热点查询结果,降低重复抓取成本。 + +章节来源 +- [backend/app/workers/scheduler.py:32-38](file://backend/app/workers/scheduler.py#L32-L38) +- [backend/app/workers/platforms/kimi.py:38-48](file://backend/app/workers/platforms/kimi.py#L38-L48) +- [backend/app/workers/platforms/wenxin.py:38-48](file://backend/app/workers/platforms/wenxin.py#L38-L48) + +### 前端主题定制指南 +- Tailwind CSS配置:通过CSS变量扩展colors与borderRadius,darkMode使用class模式。 +- PostCSS:仅启用tailwindcss插件,简化构建流程。 +- Next配置:默认配置,无需特殊改动。 +- 定制步骤: + - 颜色方案:在tailwind.config.ts的theme.extend.colors中新增或覆盖HSL变量。 + - 圆角体系:在theme.extend.borderRadius中扩展半径变量。 + - 响应式断点:在theme.extend.screens中添加自定义断点名称与宽度。 + - 主题切换:通过在html或body上切换class实现明暗主题切换。 + +```mermaid +flowchart TD +A["修改 tailwind.config.ts"] --> B["扩展 colors 或 borderRadius"] +B --> C["构建并预览"] +C --> D["在组件中使用新变量"] +D --> E["发布"] +``` + +图表来源 +- [frontend/tailwind.config.ts:10-54](file://frontend/tailwind.config.ts#L10-L54) +- [frontend/postcss.config.mjs:1-9](file://frontend/postcss.config.mjs#L1-L9) +- [frontend/next.config.mjs:1-5](file://frontend/next.config.mjs#L1-L5) + +章节来源 +- [frontend/tailwind.config.ts:1-57](file://frontend/tailwind.config.ts#L1-L57) +- [frontend/postcss.config.mjs:1-9](file://frontend/postcss.config.mjs#L1-L9) +- [frontend/next.config.mjs:1-5](file://frontend/next.config.mjs#L1-L5) +- [frontend/package.json:11-38](file://frontend/package.json#L11-L38) + +### 生产环境配置最佳实践 +- 环境变量: + - 使用独立.env文件,禁止提交到版本库;通过CI/CD注入。 + - 设置JWT_SECRET为强随机字符串,JWT_EXPIRE_HOURS按业务安全策略设定。 + - DATABASE_URL与REDIS_URL指向独立实例,启用TLS与访问控制。 +- 安全加固: + - 限制CORS白名单,仅允许受信域名。 + - 启用HTTPS与安全头(由网关或反向代理层处理)。 + - 定期轮换密钥与证书。 +- 可靠性: + - 数据库与Redis增加健康检查与自动重启策略。 + - 后端服务设置优雅停机与进程监控。 + +章节来源 +- [backend/app/main.py:30-36](file://backend/app/main.py#L30-L36) +- [docker-compose.yml:8-20](file://docker-compose.yml#L8-L20) +- [docker-compose.yml:25-34](file://docker-compose.yml#L25-L34) +- [docker-compose.yml:46-51](file://docker-compose.yml#L46-L51) + +## 依赖分析 +- 后端依赖:FastAPI、SQLAlchemy异步、Pydantic/Settings、Redis、APScheduler、Playwright。 +- 前端依赖:Next.js、Tailwind CSS、Radix UI组件、Recharts等。 + +```mermaid +graph LR +R["requirements.txt"] --> F["FastAPI"] +R --> SA["SQLAlchemy异步"] +R --> PS["Pydantic/Settings"] +R --> RD["Redis"] +R --> AP["APScheduler"] +R --> PW["Playwright"] +P["package.json"] --> NX["Next.js"] +P --> TW["Tailwind CSS"] +P --> UI["@radix-ui/*"] +P --> RC["Recharts"] +``` + +图表来源 +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) +- [frontend/package.json:11-38](file://frontend/package.json#L11-L38) + +章节来源 +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) +- [frontend/package.json:11-38](file://frontend/package.json#L11-L38) + +## 性能考虑 +- 数据库:根据QPS与查询复杂度调整连接池大小;开启连接复用与超时控制。 +- 缓存:对热点查询结果进行缓存,设置合理TTL;避免缓存穿透与雪崩。 +- 并发:平台查询采用指数退避与串行化,避免平台限流;必要时引入队列与限速器。 +- 前端:Tailwind按需扫描内容文件,减少未使用样式体积;生产构建启用压缩与Tree Shaking。 + +## 故障排查指南 +- 数据库连接失败:检查DATABASE_URL格式与可达性;确认容器网络与卷挂载。 +- Redis连接失败:检查REDIS_URL与网络策略;确认容器健康检查状态。 +- Playwright浏览器问题:确认PLAYWRIGHT_BROWSERS_PATH与浏览器安装;查看日志中的超时与选择器匹配失败信息。 +- 调度器未启动:确认lifespan钩子与main.py中include_router顺序;查看日志输出。 +- CORS错误:核对allow_origins与凭证设置;确保前端请求头一致。 + +章节来源 +- [backend/app/database.py:6-10](file://backend/app/database.py#L6-L10) +- [backend/app/config.py:11](file://backend/app/config.py#L11) +- [backend/app/workers/platforms/kimi.py:21-32](file://backend/app/workers/platforms/kimi.py#L21-L32) +- [backend/app/workers/platforms/wenxin.py:21-32](file://backend/app/workers/platforms/wenxin.py#L21-L32) +- [backend/app/main.py:13-21](file://backend/app/main.py#L13-L21) +- [backend/app/main.py:30-36](file://backend/app/main.py#L30-L36) + +## 结论 +本文档提供了GEO项目配置定制的完整指南,涵盖后端环境变量、数据库与缓存、AI平台密钥、前端主题定制与生产安全实践。当前代码未实现运行时功能开关与配置热更新,建议在后续迭代中引入配置中心与热重载机制,以提升系统的可运维性与安全性。 + +## 附录 +- Docker Compose编排:数据库、缓存与前后端服务均通过.env注入环境变量,建议在生产环境使用独立网络与只读根文件系统。 +- 版本与依赖:后端与前端均采用稳定版本,升级前请进行兼容性测试。 + +章节来源 +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) +- [frontend/package.json:11-38](file://frontend/package.json#L11-L38) \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/故障排除与FAQ.md b/.qoder/repowiki/zh/content/故障排除与FAQ.md new file mode 100644 index 0000000..87488e7 --- /dev/null +++ b/.qoder/repowiki/zh/content/故障排除与FAQ.md @@ -0,0 +1,480 @@ +# 故障排除与FAQ + + +**本文引用的文件** +- [backend/app/main.py](file://backend/app/main.py) +- [backend/app/config.py](file://backend/app/config.py) +- [backend/app/database.py](file://backend/app/database.py) +- [backend/app/api/auth.py](file://backend/app/api/auth.py) +- [backend/app/services/auth.py](file://backend/app/services/auth.py) +- [backend/app/models/user.py](file://backend/app/models/user.py) +- [backend/app/schemas/auth.py](file://backend/app/schemas/auth.py) +- [backend/app/api/deps.py](file://backend/app/api/deps.py) +- [backend/app/api/queries.py](file://backend/app/api/queries.py) +- [backend/app/models/query.py](file://backend/app/models/query.py) +- [backend/app/workers/scheduler.py](file://backend/app/workers/scheduler.py) +- [frontend/app/api/auth/[...nextauth]/route.ts](file://frontend/app/api/auth/[...nextauth]/route.ts) +- [frontend/lib/auth.ts](file://frontend/lib/auth.ts) +- [docker-compose.yml](file://docker-compose.yml) +- [backend/requirements.txt](file://backend/requirements.txt) + + +## 目录 +1. [简介](#简介) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖分析](#依赖分析) +7. [性能考虑](#性能考虑) +8. [故障排除指南](#故障排除指南) +9. [结论](#结论) +10. [附录](#附录) + +## 简介 +本文件面向GEO平台的运维与开发人员,提供系统化的故障排除与常见问题解答(FAQ)。内容覆盖认证问题、数据库连接问题、API调用问题、性能问题的定位与修复;同时给出系统监控与诊断方法(健康检查、错误日志分析、性能指标监控)、调试技巧与工具使用(开发/生产环境),以及常见错误码含义与预防性维护最佳实践,并提供紧急故障恢复流程与应急处理方案。 + +## 项目结构 +- 后端采用FastAPI + SQLAlchemy异步ORM + Redis/APScheduler任务调度,通过Docker Compose编排PostgreSQL、Redis、后端与前端服务。 +- 前端使用Next.js + NextAuth进行会话管理,通过NextAuth路由对接后端认证接口。 +- 关键路径:前端登录请求 → NextAuth → 后端认证接口 → 数据库用户校验 → JWT签发 → 前端会话存储。 + +```mermaid +graph TB +FE["前端应用
Next.js + NextAuth"] --> NA["NextAuth路由
/api/auth/[...nextauth]"] +NA --> BA["后端认证接口
/api/v1/auth/*"] +BA --> DB["数据库
PostgreSQL"] +BA --> CFG["配置中心
settings/DATABASE_URL/JWT等"] +BA --> SCH["任务调度器
APScheduler"] +BE["后端服务
FastAPI"] --> DB +BE --> REDIS["缓存/队列
Redis"] +``` + +图表来源 +- [backend/app/main.py:24-47](file://backend/app/main.py#L24-L47) +- [frontend/app/api/auth/[...nextauth]/route.ts](file://frontend/app/api/auth/[...nextauth]/route.ts#L1-L7) +- [backend/app/config.py:7-13](file://backend/app/config.py#L7-L13) +- [backend/app/database.py:6-18](file://backend/app/database.py#L6-L18) +- [backend/app/workers/scheduler.py:25-95](file://backend/app/workers/scheduler.py#L25-L95) + +章节来源 +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) + +## 核心组件 +- 应用入口与生命周期:定义FastAPI应用、CORS中间件、路由注册与健康检查端点。 +- 配置管理:集中读取DATABASE_URL、REDIS_URL、JWT密钥、API密钥等。 +- 数据库层:异步引擎与会话工厂,提供依赖注入式数据库会话。 +- 认证模块:注册/登录/当前用户信息,JWT生成与校验,OAuth2 Bearer令牌解析。 +- 查询模块:查询任务的增删改查,权限校验与分页参数控制。 +- 任务调度:基于APScheduler的异步调度器,周期性扫描并执行到期查询任务。 +- 前端认证:NextAuth凭据提供者对接后端登录接口,JWT会话策略。 + +章节来源 +- [backend/app/main.py:13-47](file://backend/app/main.py#L13-L47) +- [backend/app/config.py:4-16](file://backend/app/config.py#L4-L16) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/app/services/auth.py:1-69](file://backend/app/services/auth.py#L1-L69) +- [backend/app/api/queries.py:1-86](file://backend/app/api/queries.py#L1-L86) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [frontend/lib/auth.ts:1-56](file://frontend/lib/auth.ts#L1-L56) + +## 架构总览 +下图展示从浏览器到后端、数据库与外部平台的关键交互路径,以及认证与任务调度的运行时关系。 + +```mermaid +sequenceDiagram +participant U as "用户浏览器" +participant F as "前端NextAuth" +participant A as "后端认证接口" +participant D as "数据库" +participant J as "JWT令牌" +participant S as "任务调度器" +U->>F : "提交登录表单" +F->>A : "POST /api/v1/auth/login" +A->>D : "按邮箱查询用户" +D-->>A : "返回用户记录" +A->>A : "校验密码" +A->>J : "签发访问令牌" +A-->>F : "返回令牌与用户信息" +F-->>U : "建立会话并跳转" +Note over S,D : "后台定时扫描到期查询并执行" +``` + +图表来源 +- [frontend/lib/auth.ts:13-31](file://frontend/lib/auth.ts#L13-L31) +- [backend/app/api/auth.py:22-37](file://backend/app/api/auth.py#L22-L37) +- [backend/app/services/auth.py:55-69](file://backend/app/services/auth.py#L55-L69) +- [backend/app/models/user.py:11-41](file://backend/app/models/user.py#L11-L41) +- [backend/app/workers/scheduler.py:51-84](file://backend/app/workers/scheduler.py#L51-L84) + +## 详细组件分析 + +### 认证组件分析 +- 登录流程要点 + - 前端NextAuth凭据提供者收集邮箱/密码,调用后端登录接口。 + - 后端根据邮箱查询用户,校验密码,成功则签发JWT。 + - 前端以JWT策略存储会话,后续受保护路由自动携带Bearer令牌。 +- 常见问题与定位 + - 用户名或密码错误:后端返回未授权错误,需检查输入格式与用户是否存在。 + - JWT过期或密钥不一致:令牌解析失败导致“凭据无效”,需核对JWT_SECRET与过期时间。 + - CORS跨域:若前端端口变更但后端允许源未更新,会出现跨域错误。 +- 安全建议 + - 生产环境必须更换默认JWT密钥,设置合理过期时间。 + - 密码使用哈希存储,传输层启用HTTPS。 + +```mermaid +sequenceDiagram +participant C as "客户端" +participant N as "NextAuth" +participant R as "认证路由" +participant M as "模型/服务" +participant T as "JWT" +C->>N : "凭据提交" +N->>R : "POST /api/v1/auth/login" +R->>M : "查询用户并校验密码" +M-->>R : "返回用户或空" +alt "用户存在且密码正确" +R->>T : "生成访问令牌" +R-->>N : "返回令牌与用户" +N-->>C : "会话建立" +else "用户不存在或密码错误" +R-->>N : "401 未授权" +N-->>C : "登录失败" +end +``` + +图表来源 +- [frontend/lib/auth.ts:13-31](file://frontend/lib/auth.ts#L13-L31) +- [backend/app/api/auth.py:22-37](file://backend/app/api/auth.py#L22-L37) +- [backend/app/services/auth.py:37-69](file://backend/app/services/auth.py#L37-L69) +- [backend/app/models/user.py:11-41](file://backend/app/models/user.py#L11-L41) + +章节来源 +- [frontend/app/api/auth/[...nextauth]/route.ts](file://frontend/app/api/auth/[...nextauth]/route.ts#L1-L7) +- [frontend/lib/auth.ts:1-56](file://frontend/lib/auth.ts#L1-L56) +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/app/services/auth.py:1-69](file://backend/app/services/auth.py#L1-L69) +- [backend/app/api/deps.py:16-43](file://backend/app/api/deps.py#L16-L43) +- [backend/app/schemas/auth.py:1-34](file://backend/app/schemas/auth.py#L1-L34) +- [backend/app/models/user.py:1-41](file://backend/app/models/user.py#L1-L41) + +### 数据库与会话组件分析 +- 连接与依赖 + - 异步引擎与会话工厂统一由配置驱动,依赖注入在每个请求内创建会话并自动关闭。 + - 数据库URL来自环境变量,容器化部署通过compose的env_file注入。 +- 常见问题 + - 连接字符串错误:确认主机、端口、数据库名、用户名与密码。 + - 权限不足:确保用户具备数据库访问权限。 + - 连接池耗尽:检查并发量与超时设置,必要时调整连接数。 +- 诊断步骤 + - 在后端容器内使用psql连接数据库验证连通性。 + - 查看数据库慢查询日志与锁等待情况。 + +```mermaid +flowchart TD +Start(["请求进入"]) --> GetCfg["读取配置
DATABASE_URL"] +GetCfg --> BuildEngine["创建异步引擎"] +BuildEngine --> Session["创建会话工厂"] +Session --> UseDB["依赖注入获取会话"] +UseDB --> Exec["执行SQL操作"] +Exec --> Close["关闭会话"] +Close --> End(["请求结束"]) +``` + +图表来源 +- [backend/app/config.py:7-13](file://backend/app/config.py#L7-L13) +- [backend/app/database.py:6-29](file://backend/app/database.py#L6-L29) + +章节来源 +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [docker-compose.yml:4-21](file://docker-compose.yml#L4-L21) + +### 查询与任务调度组件分析 +- 查询管理 + - 支持分页列表、创建、查询、更新、删除;创建时进行权限校验与配额检查。 + - 查询状态与下次执行时间字段用于调度决策。 +- 任务调度 + - 每小时扫描一次到期查询,逐条执行并记录日志;异常不影响整体调度。 + - 调度器随应用生命周期启动与关闭。 + +```mermaid +flowchart TD +Tick["每小时触发"] --> Scan["扫描到期查询"] +Scan --> Found{"找到待执行项?"} +Found -- "否" --> Sleep["等待下次触发"] +Found -- "是" --> Exec["逐条执行查询"] +Exec --> Log["记录执行结果/异常"] +Log --> Sleep +``` + +图表来源 +- [backend/app/workers/scheduler.py:30-90](file://backend/app/workers/scheduler.py#L30-L90) +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) + +章节来源 +- [backend/app/api/queries.py:1-86](file://backend/app/api/queries.py#L1-L86) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) + +## 依赖分析 +- 组件耦合 + - 认证路由依赖数据库与JWT服务;依赖注入贯穿API层与服务层。 + - 查询API依赖当前用户上下文与数据库会话。 + - 调度器依赖数据库会话与引用引擎,日志输出用于可观测性。 +- 外部依赖 + - 数据库:PostgreSQL(异步驱动) + - 缓存/队列:Redis + - 任务调度:APScheduler + - 浏览览器自动化:Playwright + - 认证:JWT、BCrypt + +```mermaid +graph LR +AUTH["认证路由"] --> DEPS["依赖注入"] +AUTH --> JWT["JWT服务"] +AUTH --> DB["数据库"] +QUERIES["查询路由"] --> DEPS +QUERIES --> DB +SCHED["调度器"] --> DB +SCHED --> CE["引用引擎"] +MAIN["应用入口"] --> AUTH +MAIN --> QUERIES +MAIN --> SCHED +``` + +图表来源 +- [backend/app/main.py:6-42](file://backend/app/main.py#L6-L42) +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/app/api/queries.py:1-86](file://backend/app/api/queries.py#L1-L86) +- [backend/app/workers/scheduler.py:18-95](file://backend/app/workers/scheduler.py#L18-L95) + +章节来源 +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) + +## 性能考虑 +- 数据库层 + - 使用异步ORM减少阻塞;为高频查询字段建立索引(如查询表的用户ID、状态、下次执行时间)。 + - 控制事务范围,避免长事务;批量写入时合并提交。 +- API层 + - 合理设置分页参数上限,避免一次性返回过多数据。 + - 对热点接口增加缓存(如用户信息、平台配置)。 +- 调度与并发 + - 调度间隔与任务粒度平衡;单次任务失败不应阻塞其他任务。 + - 监控任务执行时长与积压情况,必要时拆分任务或引入队列。 +- 前端 + - 减少不必要的重渲染,使用稳定的数据结构;对长列表使用虚拟滚动。 +- 观测性 + - 记录关键链路耗时与错误率;结合日志与指标进行根因分析。 + +[本节为通用指导,无需列出章节来源] + +## 故障排除指南 + +### 认证相关问题 +- 症状:登录返回401未授权 + - 可能原因 + - 邮箱不存在或密码错误 + - JWT密钥不匹配或过期 + - CORS限制导致预检失败 + - 排查步骤 + - 确认用户是否已注册且密码正确 + - 检查后端JWT_SECRET与前端会话策略 + - 核对后端CORS允许源是否包含前端地址 + - 解决方案 + - 修正凭据或重置密码 + - 更新JWT密钥并重启服务 + - 调整CORS配置 + +- 症状:受保护接口返回401凭据无效 + - 可能原因 + - 令牌缺失或格式错误 + - 令牌解析失败(算法或密钥不一致) + - 用户被禁用或不存在 + - 排查步骤 + - 检查请求头是否包含Bearer令牌 + - 校验JWT_SECRET与过期时间 + - 确认用户仍存在于数据库且处于激活状态 + - 解决方案 + - 重新登录获取新令牌 + - 修复配置并刷新令牌 + +章节来源 +- [backend/app/api/auth.py:22-37](file://backend/app/api/auth.py#L22-L37) +- [backend/app/services/auth.py:24-34](file://backend/app/services/auth.py#L24-L34) +- [backend/app/api/deps.py:16-43](file://backend/app/api/deps.py#L16-L43) +- [backend/app/main.py:30-36](file://backend/app/main.py#L30-L36) + +### 数据库连接问题 +- 症状:应用启动时报数据库连接失败 + - 可能原因 + - DATABASE_URL配置错误 + - PostgreSQL未就绪或端口未映射 + - 用户/密码/数据库名不正确 + - 排查步骤 + - 在后端容器内使用psql连接验证 + - 检查compose健康检查与依赖顺序 + - 确认防火墙与网络策略放行 + - 解决方案 + - 修正.env中的DATABASE_URL + - 等待数据库健康检查通过后再启动后端 + - 修正凭据与数据库名称 + +- 症状:运行中频繁出现连接超时或连接池耗尽 + - 可能原因 + - 并发过高或事务过长 + - 连接池参数过小 + - 排查步骤 + - 查看数据库连接数与慢查询 + - 检查后端连接池配置 + - 解决方案 + - 优化查询与事务边界 + - 调整连接池大小与超时参数 + +章节来源 +- [backend/app/config.py:7-13](file://backend/app/config.py#L7-L13) +- [backend/app/database.py:6-18](file://backend/app/database.py#L6-L18) +- [docker-compose.yml:46-51](file://docker-compose.yml#L46-L51) + +### API调用问题 +- 症状:查询列表/详情/创建/更新/删除返回404 + - 可能原因 + - 资源不存在或ID格式错误 + - 权限不足(非本人资源) + - 排查步骤 + - 校验UUID格式与资源归属 + - 检查当前用户上下文 + - 解决方案 + - 使用正确的用户与ID + - 确保仅操作本人资源 + +- 症状:创建查询返回403 + - 可能原因 + - 配额不足或计划限制 + - 排查步骤 + - 检查用户计划与最大查询数 + - 解决方案 + - 升级计划或清理历史任务释放额度 + +章节来源 +- [backend/app/api/queries.py:26-85](file://backend/app/api/queries.py#L26-L85) + +### 性能问题 +- 症状:查询列表加载缓慢 + - 可能原因 + - 分页参数过大 + - 缺少索引导致排序/过滤慢 + - 排查步骤 + - 降低limit并观察响应时间 + - 分析数据库执行计划 + - 解决方案 + - 合理设置分页上限 + - 为常用查询字段添加索引 + +- 症状:定时任务执行延迟或堆积 + - 可能原因 + - 单次任务耗时过长 + - 调度器未正确启动或关闭 + - 排查步骤 + - 查看调度器日志与任务耗时 + - 检查后端生命周期钩子 + - 解决方案 + - 将长任务拆分为多个短任务 + - 确保调度器随应用启动/停止 + +章节来源 +- [backend/app/workers/scheduler.py:30-90](file://backend/app/workers/scheduler.py#L30-L90) +- [backend/app/main.py:13-21](file://backend/app/main.py#L13-L21) + +### 健康检查与日志分析 +- 健康检查 + - 后端健康端点:GET /health + - compose健康检查:PostgreSQL与Redis分别提供ping/ready检测 +- 日志分析 + - 后端日志:关注认证失败、数据库异常、任务执行错误 + - 前端日志:关注NextAuth回调与网络错误 +- 性能指标 + - 记录关键API耗时、数据库查询耗时、任务执行耗时 + - 监控连接池使用率与队列长度 + +章节来源 +- [backend/app/main.py:45-47](file://backend/app/main.py#L45-L47) +- [docker-compose.yml:16-34](file://docker-compose.yml#L16-L34) +- [backend/app/workers/scheduler.py:22-90](file://backend/app/workers/scheduler.py#L22-L90) + +### 调试技巧与工具使用 +- 开发环境调试 + - 启用后端reload与详细日志 + - 使用curl或Postman测试认证与查询接口 + - 在前端浏览器开发者工具查看Network与Console +- 生产环境排查 + - 通过日志聚合与告警系统定位异常 + - 快速回滚至上一版本以隔离变更 + - 使用只读副本进行慢查询分析 +- 性能分析 + - 使用数据库性能分析工具识别慢查询 + - 对热点接口进行压力测试与容量评估 + +[本节为通用指导,无需列出章节来源] + +### 常见错误码与含义 +- 400 Bad Request + - 场景:注册时邮箱已存在、密码长度不足 + - 处理:修正输入并重试 +- 401 Unauthorized + - 场景:登录失败、令牌无效、凭据无法验证 + - 处理:重新登录或检查JWT配置 +- 403 Forbidden + - 场景:创建查询时配额不足 + - 处理:升级计划或清理历史任务 +- 404 Not Found + - 场景:查询资源不存在或非本人资源 + - 处理:确认ID与权限 + +章节来源 +- [backend/app/api/auth.py:17-19](file://backend/app/api/auth.py#L17-L19) +- [backend/app/api/queries.py:34-38](file://backend/app/api/queries.py#L34-L38) +- [backend/app/api/queries.py:49-53](file://backend/app/api/queries.py#L49-L53) + +### 预防性维护最佳实践 +- 定期备份数据库与Redis +- 保持依赖版本更新与安全补丁 +- 设置合理的日志保留与轮转策略 +- 对关键接口与任务设置告警阈值 +- 制定发布前的回归测试清单 + +[本节为通用指导,无需列出章节来源] + +### 紧急故障恢复流程与应急处理 +- 立即行动 + - 降级非关键功能,优先保障认证与核心查询 + - 回滚最近一次变更 +- 诊断与修复 + - 快速定位日志与指标异常点 + - 修复配置错误、扩容资源或修复慢查询 +- 验证与复盘 + - 发布后持续监控关键指标 + - 形成故障报告与改进措施 + +[本节为通用指导,无需列出章节来源] + +## 结论 +通过明确的组件职责、完善的健康检查与日志体系、规范的调试与性能分析流程,以及针对认证、数据库、API与调度的专项排障清单,可以显著提升GEO平台的稳定性与可维护性。建议将本指南纳入团队知识库,并定期演练应急流程以提升响应效率。 + +## 附录 +- 快速检查清单 + - 数据库连通性与权限 + - JWT密钥与过期时间 + - CORS允许源配置 + - 调度器状态与日志 + - 健康端点与compose健康检查 +- 参考文件 + - 后端入口与路由:[backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) + - 配置与数据库:[backend/app/config.py:1-17](file://backend/app/config.py#L1-L17)、[backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) + - 认证与用户模型:[backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43)、[backend/app/services/auth.py:1-69](file://backend/app/services/auth.py#L1-L69)、[backend/app/models/user.py:1-41](file://backend/app/models/user.py#L1-L41) + - 查询与调度:[backend/app/api/queries.py:1-86](file://backend/app/api/queries.py#L1-L86)、[backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55)、[backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) + - 前端认证:[frontend/lib/auth.ts:1-56](file://frontend/lib/auth.ts#L1-L56)、[frontend/app/api/auth/[...nextauth]/route.ts](file://frontend/app/api/auth/[...nextauth]/route.ts#L1-L7) + - 编排与依赖:[docker-compose.yml:1-71](file://docker-compose.yml#L1-L71)、[backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/数据库设计/数据库架构.md b/.qoder/repowiki/zh/content/数据库设计/数据库架构.md new file mode 100644 index 0000000..3c59cda --- /dev/null +++ b/.qoder/repowiki/zh/content/数据库设计/数据库架构.md @@ -0,0 +1,436 @@ +# 数据库架构 + + +**本文引用的文件** +- [backend/app/database.py](file://backend/app/database.py) +- [backend/app/config.py](file://backend/app/config.py) +- [backend/alembic/env.py](file://backend/alembic/env.py) +- [backend/alembic/versions/488d0bd5ab01_initial_migration.py](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py) +- [backend/requirements.txt](file://backend/requirements.txt) +- [backend/app/models/user.py](file://backend/app/models/user.py) +- [backend/app/models/query.py](file://backend/app/models/query.py) +- [backend/app/models/citation_record.py](file://backend/app/models/citation_record.py) +- [backend/app/models/query_task.py](file://backend/app/models/query_task.py) +- [backend/app/models/subscription.py](file://backend/app/models/subscription.py) +- [backend/app/models/__init__.py](file://backend/app/models/__init__.py) +- [backend/app/api/deps.py](file://backend/app/api/deps.py) +- [backend/app/api/auth.py](file://backend/app/api/auth.py) +- [backend/app/main.py](file://backend/app/main.py) +- [docker-compose.yml](file://docker-compose.yml) + + +## 目录 +1. [简介](#简介) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖分析](#依赖分析) +7. [性能考量](#性能考量) +8. [故障排查指南](#故障排查指南) +9. [结论](#结论) +10. [附录](#附录) + +## 简介 +本文件系统性梳理 GEO 平台的数据库架构,重点覆盖以下方面: +- 基于 PostgreSQL 的异步数据库连接配置:SQLAlchemy 异步引擎、连接池与会话生命周期管理 +- 连接字符串配置、环境变量管理与安全注意事项 +- 异步数据库操作的优势、性能特征与最佳实践 +- 连接监控、错误处理与故障恢复机制 +- 生产环境配置建议与性能调优指南 + +## 项目结构 +后端采用 FastAPI + SQLAlchemy 2.x 异步 ORM 架构,数据库层由异步引擎与会话工厂构成,配合 Alembic 进行迁移管理;模型定义位于 models 子包中,并通过 API 层注入依赖进行使用。 + +```mermaid +graph TB +subgraph "后端应用" +CFG["配置模块
app/config.py"] +DB["数据库引擎与会话
app/database.py"] +MODELS["ORM 模型
app/models/*"] +ALEMBIC["迁移环境
alembic/env.py"] +MIGRATE["初始迁移脚本
alembic/versions/*"] +API_DEPS["API 依赖注入
app/api/deps.py"] +API_AUTH["认证路由
app/api/auth.py"] +MAIN["应用入口与生命周期
app/main.py"] +end +subgraph "外部服务" +PG["PostgreSQL 实例"] +REDIS["Redis 实例"] +end +CFG --> DB +DB --> MODELS +ALEMBIC --> MIGRATE +API_DEPS --> DB +API_AUTH --> DB +MAIN --> API_AUTH +MAIN --> API_DEPS +DB --> PG +REDIS -. 缓存/任务队列 .-> MAIN +``` + +图表来源 +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [backend/alembic/env.py:1-89](file://backend/alembic/env.py#L1-L89) +- [backend/alembic/versions/488d0bd5ab01_initial_migration.py:1-128](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L1-L128) +- [backend/app/api/deps.py:1-43](file://backend/app/api/deps.py#L1-L43) +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) + +章节来源 +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [backend/alembic/env.py:1-89](file://backend/alembic/env.py#L1-L89) +- [backend/alembic/versions/488d0bd5ab01_initial_migration.py:1-128](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L1-L128) +- [backend/app/api/deps.py:1-43](file://backend/app/api/deps.py#L1-L43) +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) + +## 核心组件 +- 异步引擎与会话工厂 + - 使用 SQLAlchemy 异步引擎创建数据库连接,关闭 echo,启用 future 模式 + - 会话工厂配置为非自动提交、非自动刷新、关闭提交时过期,确保事务边界清晰 + - 提供异步上下文管理的会话生成器,保证在请求结束时正确关闭会话 +- 配置与连接字符串 + - 通过 Pydantic Settings 从 .env 文件加载配置,包含 DATABASE_URL、REDIS_URL、JWT_SECRET 等 + - 默认 DATABASE_URL 指向本地或容器内 PostgreSQL 实例 +- 模型与索引 + - 用户、查询、引用记录、查询任务、订阅等核心实体,均采用 PostgreSQL UUID 主键与 JSONB 字段 + - 在高频查询字段上建立复合索引,提升读取性能 +- 迁移与初始化 + - Alembic 环境支持离线与在线迁移,使用异步引擎执行迁移 + - 初始迁移脚本创建表结构与索引,定义外键约束与默认值 + +章节来源 +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [backend/app/models/user.py:1-41](file://backend/app/models/user.py#L1-L41) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [backend/app/models/citation_record.py:1-42](file://backend/app/models/citation_record.py#L1-L42) +- [backend/app/models/query_task.py:1-39](file://backend/app/models/query_task.py#L1-L39) +- [backend/app/models/subscription.py:1-37](file://backend/app/models/subscription.py#L1-L37) +- [backend/alembic/env.py:64-88](file://backend/alembic/env.py#L64-L88) +- [backend/alembic/versions/488d0bd5ab01_initial_migration.py:21-128](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L21-L128) + +## 架构总览 +下图展示异步数据库连接在应用中的整体流转:FastAPI 路由通过依赖注入获取 AsyncSession,执行数据库操作后自动释放;迁移通过 Alembic 异步引擎执行;配置从 .env 加载 DATABASE_URL。 + +```mermaid +sequenceDiagram +participant Client as "客户端" +participant API as "FastAPI 路由" +participant Deps as "依赖注入(get_db)" +participant Session as "AsyncSession" +participant Engine as "异步引擎" +participant PG as "PostgreSQL" +Client->>API : "HTTP 请求" +API->>Deps : "解析依赖(获取会话)" +Deps->>Session : "创建/获取会话" +Session->>Engine : "执行 SQL" +Engine->>PG : "发送查询" +PG-->>Engine : "返回结果" +Engine-->>Session : "映射为模型" +Session-->>API : "业务结果" +API-->>Client : "响应" +API->>Deps : "请求结束" +Deps->>Session : "关闭会话" +``` + +图表来源 +- [backend/app/api/auth.py:13-42](file://backend/app/api/auth.py#L13-L42) +- [backend/app/api/deps.py:16-42](file://backend/app/api/deps.py#L16-L42) +- [backend/app/database.py:23-28](file://backend/app/database.py#L23-L28) + +## 详细组件分析 + +### 异步引擎与会话生命周期 +- 引擎创建 + - 使用异步引擎,echo 关闭,future 启用,避免冗余日志与兼容性问题 +- 会话工厂 + - 非自动提交、非自动刷新、关闭提交时过期,便于显式控制事务 +- 会话生成器 + - 通过上下文管理器确保异常时也能正确关闭会话,避免连接泄漏 + +```mermaid +flowchart TD +Start(["进入 get_db"]) --> NewSession["创建 AsyncSessionLocal()"] +NewSession --> TryBlock["进入 try 块并 yield 会话"] +TryBlock --> FinallyBlock["finally 块中关闭会话"] +FinallyBlock --> End(["退出 get_db"]) +``` + +图表来源 +- [backend/app/database.py:23-28](file://backend/app/database.py#L23-L28) + +章节来源 +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) + +### 连接字符串与环境变量管理 +- 连接字符串 + - 默认 DATABASE_URL 指向 PostgreSQL 实例,可在 .env 中覆盖 +- 环境变量加载 + - 通过 Pydantic SettingsConfigDict 从 .env 加载,忽略未知字段 +- 安全注意事项 + - 生产环境务必使用强密钥与加密传输 + - 不要在代码中硬编码敏感信息,优先使用环境变量与密钥管理服务 + +章节来源 +- [backend/app/config.py:4-16](file://backend/app/config.py#L4-L16) + +### 数据模型与索引策略 +- 用户模型 + - UUID 主键、邮箱唯一、密码哈希、计划与配额字段 +- 查询模型 + - 外键关联用户、JSONB 存储平台与别名、多字段索引优化 +- 引用记录模型 + - JSONB 存储竞品品牌与原始响应,按查询与时间、平台建立索引 +- 查询任务模型 + - 状态字段索引,便于任务调度与状态统计 +- 订阅模型 + - 金额与支付信息存储,日期范围与状态管理 + +```mermaid +erDiagram +USERS { +uuid id PK +string email UK +string password_hash +string name +string plan +int max_queries +bool is_active +timestamptz created_at +timestamptz updated_at +} +QUERIES { +uuid id PK +uuid user_id FK +string keyword +string target_brand +jsonb brand_aliases +jsonb platforms +string frequency +string status +timestamptz last_queried_at +timestamptz next_query_at +timestamptz created_at +timestamptz updated_at +} +CITATION_RECORDS { +uuid id PK +uuid query_id FK +string platform +bool cited +int citation_position +text citation_text +jsonb competitor_brands +text raw_response +timestamptz queried_at +} +QUERY_TASKS { +uuid id PK +uuid query_id FK +string platform +string status +text error_message +timestamptz scheduled_at +timestamptz started_at +timestamptz completed_at +} +SUBSCRIPTIONS { +uuid id PK +uuid user_id FK +string plan +string status +date start_date +date end_date +numeric amount +string payment_method +string payment_id +timestamptz created_at +} +USERS ||--o{ QUERIES : "拥有" +QUERIES ||--o{ CITATION_RECORDS : "产生" +QUERIES ||--o{ QUERY_TASKS : "驱动" +USERS ||--o{ SUBSCRIPTIONS : "订阅" +``` + +图表来源 +- [backend/app/models/user.py:11-41](file://backend/app/models/user.py#L11-L41) +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [backend/app/models/citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) +- [backend/app/models/query_task.py:11-39](file://backend/app/models/query_task.py#L11-L39) +- [backend/app/models/subscription.py:11-37](file://backend/app/models/subscription.py#L11-L37) + +章节来源 +- [backend/app/models/user.py:1-41](file://backend/app/models/user.py#L1-L41) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [backend/app/models/citation_record.py:1-42](file://backend/app/models/citation_record.py#L1-L42) +- [backend/app/models/query_task.py:1-39](file://backend/app/models/query_task.py#L1-L39) +- [backend/app/models/subscription.py:1-37](file://backend/app/models/subscription.py#L1-L37) + +### 迁移与初始化 +- 离线/在线迁移 + - 离线模式直接使用 DATABASE_URL;在线模式通过异步引擎建立连接并执行迁移 + - 使用 NullPool 避免额外连接池开销 +- 初始迁移 + - 创建 users、queries、citation_records、query_tasks、subscriptions 表 + - 添加必要的外键与索引,确保查询性能与数据一致性 + +```mermaid +sequenceDiagram +participant CLI as "Alembic CLI" +participant Env as "env.py" +participant Engine as "异步引擎" +participant Conn as "连接" +participant DB as "PostgreSQL" +CLI->>Env : "run_migrations_online()" +Env->>Engine : "create_async_engine(DATABASE_URL, poolclass=NullPool)" +Engine->>Conn : "connect()" +Conn->>DB : "执行迁移" +DB-->>Conn : "返回迁移结果" +Conn-->>Engine : "完成迁移" +Engine-->>Env : "dispose()" +``` + +图表来源 +- [backend/alembic/env.py:64-88](file://backend/alembic/env.py#L64-L88) +- [backend/alembic/versions/488d0bd5ab01_initial_migration.py:21-128](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L21-L128) + +章节来源 +- [backend/alembic/env.py:1-89](file://backend/alembic/env.py#L1-L89) +- [backend/alembic/versions/488d0bd5ab01_initial_migration.py:1-128](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L1-L128) + +### API 依赖注入与会话使用 +- 依赖注入 + - OAuth2 密钥流用于令牌校验;通过 get_db 获取 AsyncSession +- 典型流程 + - 注册/登录接口使用 AsyncSession 执行写入与查询 + - 当前用户接口通过令牌解析与数据库查询返回用户信息 + +```mermaid +sequenceDiagram +participant Client as "客户端" +participant Auth as "认证路由" +participant Deps as "get_current_user" +participant DB as "AsyncSession" +participant Model as "User 模型" +Client->>Auth : "POST /api/v1/auth/login" +Auth->>DB : "执行认证查询" +DB-->>Auth : "返回用户对象" +Auth-->>Client : "返回访问令牌与用户信息" +Client->>Auth : "GET /api/v1/auth/me" +Auth->>Deps : "校验令牌并解析用户ID" +Deps->>DB : "查询用户" +DB-->>Deps : "返回用户" +Deps-->>Client : "返回当前用户" +``` + +图表来源 +- [backend/app/api/auth.py:13-42](file://backend/app/api/auth.py#L13-L42) +- [backend/app/api/deps.py:16-42](file://backend/app/api/deps.py#L16-L42) + +章节来源 +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/app/api/deps.py:1-43](file://backend/app/api/deps.py#L1-L43) + +## 依赖分析 +- 组件耦合 + - API 层通过依赖注入使用数据库会话,降低耦合度 + - 模型与数据库层解耦,仅依赖 Base 与类型注解 +- 外部依赖 + - SQLAlchemy 2.x 异步 ORM、asyncpg、Alembic + - FastAPI、Pydantic Settings、Redis(缓存/任务) + +```mermaid +graph LR +API_AUTH["app/api/auth.py"] --> DEPS["app/api/deps.py"] +DEPS --> DB["app/database.py"] +API_AUTH --> DB +DB --> CONFIG["app/config.py"] +DB --> MODELS["app/models/*"] +ALEMBIC_ENV["alembic/env.py"] --> CONFIG +ALEMBIC_ENV --> MODELS +ALEMBIC_MIG["alembic/versions/*"] --> MODELS +``` + +图表来源 +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/app/api/deps.py:1-43](file://backend/app/api/deps.py#L1-L43) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [backend/alembic/env.py:1-89](file://backend/alembic/env.py#L1-L89) +- [backend/alembic/versions/488d0bd5ab01_initial_migration.py:1-128](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L1-L128) + +章节来源 +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) + +## 性能考量 +- 异步优势 + - 高并发场景下减少阻塞,提升吞吐量 + - 与 FastAPI 协同,充分利用事件循环 +- 连接池与会话 + - 默认未显式配置连接池参数,建议在生产中根据 QPS 与实例规格调整最大连接数、空闲连接数与超时 +- 索引与查询 + - 已在高频查询字段建立索引,避免全表扫描 + - 对 JSONB 字段的查询建议结合 GIN 索引(如需) +- 写入优化 + - 批量写入与事务合并可显著降低写入延迟 +- 监控与观测 + - 建议集成数据库性能视图与慢查询日志 + - 结合应用指标(请求耗时、错误率)定位瓶颈 + +## 故障排查指南 +- 连接失败 + - 检查 DATABASE_URL 是否正确,网络连通性与防火墙 + - 确认 PostgreSQL 实例健康状态与认证凭据 +- 迁移失败 + - 离线/在线模式分别检查配置与权限 + - 查看 Alembic 输出与 PostgreSQL 日志 +- 会话泄漏 + - 确保所有分支均能到达 finally 块,避免异常中断导致会话未关闭 +- 错误处理 + - API 层对认证失败与数据库异常进行标准化响应 + - 建议增加重试与熔断策略(如适用) + +章节来源 +- [backend/app/database.py:23-28](file://backend/app/database.py#L23-L28) +- [backend/alembic/env.py:64-88](file://backend/alembic/env.py#L64-L88) +- [backend/app/api/auth.py:13-42](file://backend/app/api/auth.py#L13-L42) +- [backend/app/api/deps.py:16-42](file://backend/app/api/deps.py#L16-L42) + +## 结论 +GEO 项目采用 SQLAlchemy 异步 ORM 与 FastAPI 构建了高并发、可维护的数据库层。通过合理的模型设计、索引策略与依赖注入,实现了清晰的职责分离与良好的扩展性。建议在生产环境中完善连接池参数、监控体系与安全策略,持续优化查询与写入路径。 + +## 附录 + +### 生产环境配置建议 +- 连接字符串与环境变量 + - 使用独立 .env 文件管理 DATABASE_URL,避免硬编码 + - 在容器编排中通过环境注入,确保不同环境隔离 +- 连接池与会话 + - 显式配置最大连接数、空闲连接数、连接超时与回收策略 + - 控制会话生命周期,避免长事务与长时间持有连接 +- 安全 + - 使用强口令与 TLS 加密传输 + - 限制数据库用户权限,最小化授权 +- 监控与可观测性 + - 集成数据库性能视图与慢查询日志 + - 应用层记录关键指标(QPS、P95/P99、错误率) +- 备份与恢复 + - 定期备份策略与演练,确保可快速恢复 + +### 性能调优清单 +- 索引优化 + - 对高频过滤与排序字段建立合适索引 + - 对 JSONB 字段查询考虑 GIN 索引 +- 查询优化 + - 减少 N+1 查询,使用 select_in_load 或 joinedload + - 合理分页与投影,避免 SELECT * +- 写入优化 + - 批量插入与事务合并 + - 写入热点拆分与读写分离(如需要) +- 连接与资源 + - 合理设置连接池大小,避免过度占用 + - 及时关闭会话与释放资源 \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/数据库设计/数据库设计.md b/.qoder/repowiki/zh/content/数据库设计/数据库设计.md new file mode 100644 index 0000000..86bc2bc --- /dev/null +++ b/.qoder/repowiki/zh/content/数据库设计/数据库设计.md @@ -0,0 +1,521 @@ +# 数据库设计 + + +**本文档引用的文件** +- [backend/app/database.py](file://backend/app/database.py) +- [backend/app/models/user.py](file://backend/app/models/user.py) +- [backend/app/models/query.py](file://backend/app/models/query.py) +- [backend/app/models/citation_record.py](file://backend/app/models/citation_record.py) +- [backend/app/models/query_task.py](file://backend/app/models/query_task.py) +- [backend/app/models/subscription.py](file://backend/app/models/subscription.py) +- [backend/app/models/__init__.py](file://backend/app/models/__init__.py) +- [backend/alembic/env.py](file://backend/alembic/env.py) +- [backend/alembic/versions/488d0bd5ab01_initial_migration.py](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py) +- [backend/alembic.ini](file://backend/alembic.ini) +- [backend/app/config.py](file://backend/app/config.py) +- [docker-compose.yml](file://docker-compose.yml) +- [backend/app/services/query.py](file://backend/app/services/query.py) +- [backend/app/services/citation.py](file://backend/app/services/citation.py) + + +## 目录 +1. [简介](#简介) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖分析](#依赖分析) +7. [性能考虑](#性能考虑) +8. [故障排查指南](#故障排查指南) +9. [结论](#结论) +10. [附录](#附录) + +## 简介 +本文件为 GEOF 智能检索与引用监测平台的数据库设计文档,聚焦于基于 PostgreSQL 的关系型数据库架构,涵盖: +- 表结构设计与实体关系映射(ER) +- 索引策略与查询优化 +- SQLAlchemy ORM 模型实现(模型定义、关系配置、查询封装) +- 数据库迁移管理、版本控制与部署策略 +- 数据完整性约束、事务处理与并发控制 +- 性能优化、查询分析与缓存策略 +- 备份、恢复与维护最佳实践 + +## 项目结构 +后端采用 FastAPI + SQLAlchemy Async + Alembic 迁移的典型分层架构: +- 配置层:读取环境变量,提供数据库连接字符串 +- 数据库引擎与会话:异步连接池与会话工厂 +- ORM 模型层:用户、查询、引用记录、任务、订阅 +- 迁移层:Alembic 初始化迁移脚本 +- 服务层:业务查询封装,统一事务边界 +- 容器编排:Docker Compose 启动 Postgres 与 Redis + +```mermaid +graph TB +subgraph "应用层" +SvcQ["服务: 查询
backend/app/services/query.py"] +SvcC["服务: 引用
backend/app/services/citation.py"] +end +subgraph "ORM 层" +MUser["模型: 用户
backend/app/models/user.py"] +MQuery["模型: 查询
backend/app/models/query.py"] +MCit["模型: 引用记录
backend/app/models/citation_record.py"] +MTask["模型: 查询任务
backend/app/models/query_task.py"] +MSub["模型: 订阅
backend/app/models/subscription.py"] +end +subgraph "基础设施" +DB["PostgreSQL 数据库"] +RD["Redis 缓存"] +end +Cfg["配置: DATABASE_URL
backend/app/config.py"] +Eng["引擎与会话
backend/app/database.py"] +Alembic["迁移: Alembic
backend/alembic/*"] +Cfg --> Eng +Eng --> DB +SvcQ --> Eng +SvcC --> Eng +MUser --> Eng +MQuery --> Eng +MCit --> Eng +MTask --> Eng +MSub --> Eng +Alembic --> DB +SvcQ --> RD +SvcC --> RD +``` + +图表来源 +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/models/user.py:1-41](file://backend/app/models/user.py#L1-L41) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [backend/app/models/citation_record.py:1-42](file://backend/app/models/citation_record.py#L1-L42) +- [backend/app/models/query_task.py:1-39](file://backend/app/models/query_task.py#L1-L39) +- [backend/app/models/subscription.py:1-37](file://backend/app/models/subscription.py#L1-L37) +- [backend/alembic/env.py:1-89](file://backend/alembic/env.py#L1-L89) +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) + +章节来源 +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/alembic/env.py:1-89](file://backend/alembic/env.py#L1-L89) +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) + +## 核心组件 +- 数据库引擎与会话 + - 使用异步引擎与会话工厂,开启自动提交、自动刷新、自动回滚等参数,确保事务一致性与资源释放。 + - 提供依赖注入式 get_db 生成器,保证每个请求生命周期内复用同一会话。 +- ORM 模型 + - 所有模型继承自统一的 Base,表名与字段类型均在模型中显式声明。 + - 关系通过 relationship 显式配置,支持级联删除与孤儿对象清理。 +- 迁移与版本控制 + - Alembic 环境集成 SQLAlchemy Base 元数据,支持离线/在线迁移。 + - 初始迁移脚本定义了用户、查询、引用记录、查询任务、订阅五张表及必要索引。 +- 服务层封装 + - 查询与引用统计、导出等业务逻辑封装在服务层,统一执行 SQL 并返回结果。 + - 对外暴露清晰的查询接口,内部进行权限校验与计数限制。 + +章节来源 +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/models/__init__.py:1-14](file://backend/app/models/__init__.py#L1-L14) +- [backend/alembic/versions/488d0bd5ab01_initial_migration.py:1-128](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L1-L128) +- [backend/app/services/query.py:1-130](file://backend/app/services/query.py#L1-L130) +- [backend/app/services/citation.py:1-269](file://backend/app/services/citation.py#L1-L269) + +## 架构总览 +下图展示数据库层与应用层的交互关系,以及迁移与容器编排对数据库的影响。 + +```mermaid +sequenceDiagram +participant Client as "客户端" +participant API as "FastAPI 应用" +participant SvcQ as "查询服务" +participant SvcC as "引用服务" +participant DB as "PostgreSQL" +participant Alembic as "迁移工具" +Client->>API : 请求 /queries 或 /citations +API->>SvcQ : 调用查询服务 +SvcQ->>DB : 执行查询/计数/插入 +API->>SvcC : 调用引用服务 +SvcC->>DB : 执行 JOIN 统计/导出 +DB-->>SvcC : 返回统计结果 +DB-->>SvcQ : 返回查询结果 +SvcC-->>API : 返回 CSV/统计 +SvcQ-->>API : 返回查询列表/详情 +API-->>Client : 响应数据 +Note over Alembic,DB : 首次启动或升级时执行迁移 +``` + +图表来源 +- [backend/app/services/query.py:1-130](file://backend/app/services/query.py#L1-L130) +- [backend/app/services/citation.py:1-269](file://backend/app/services/citation.py#L1-L269) +- [backend/alembic/env.py:1-89](file://backend/alembic/env.py#L1-L89) + +## 详细组件分析 + +### 实体关系映射(ER) +- 用户(users):主键 id,唯一邮箱,计划与配额字段,活跃状态,时间戳。 +- 查询(queries):外键 user_id,关键词、目标品牌、别名列表、平台列表、频率、状态、下次查询时间,时间戳。 +- 引用记录(citation_records):外键 query_id,平台、是否引用、引用位置、引用文本、竞争品牌列表、原始响应、查询时间。 +- 查询任务(query_tasks):外键 query_id,平台、状态、错误信息、调度/开始/完成时间。 +- 订阅(subscriptions):外键 user_id,计划、状态、起止日期、金额、支付方式与流水号,时间戳。 + +```mermaid +erDiagram +USERS { +uuid id PK +string email UK +string password_hash +string name +string plan +integer max_queries +boolean is_active +timestamptz created_at +timestamptz updated_at +} +QUERIES { +uuid id PK +uuid user_id FK +string keyword +string target_brand +jsonb brand_aliases +jsonb platforms +string frequency +string status +timestamptz last_queried_at +timestamptz next_query_at +timestamptz created_at +timestamptz updated_at +} +CITATION_RECORDS { +uuid id PK +uuid query_id FK +string platform +boolean cited +integer citation_position +text citation_text +jsonb competitor_brands +text raw_response +timestamptz queried_at +} +QUERY_TASKS { +uuid id PK +uuid query_id FK +string platform +string status +text error_message +timestamptz scheduled_at +timestamptz started_at +timestamptz completed_at +} +SUBSCRIPTIONS { +uuid id PK +uuid user_id FK +string plan +string status +date start_date +date end_date +numeric amount +string payment_method +string payment_id +timestamptz created_at +} +USERS ||--o{ QUERIES : "拥有" +QUERIES ||--o{ CITATION_RECORDS : "产生" +QUERIES ||--o{ QUERY_TASKS : "触发" +USERS ||--o{ SUBSCRIPTIONS : "订阅" +``` + +图表来源 +- [backend/app/models/user.py:1-41](file://backend/app/models/user.py#L1-L41) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [backend/app/models/citation_record.py:1-42](file://backend/app/models/citation_record.py#L1-L42) +- [backend/app/models/query_task.py:1-39](file://backend/app/models/query_task.py#L1-L39) +- [backend/app/models/subscription.py:1-37](file://backend/app/models/subscription.py#L1-L37) + +章节来源 +- [backend/app/models/user.py:1-41](file://backend/app/models/user.py#L1-L41) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [backend/app/models/citation_record.py:1-42](file://backend/app/models/citation_record.py#L1-L42) +- [backend/app/models/query_task.py:1-39](file://backend/app/models/query_task.py#L1-L39) +- [backend/app/models/subscription.py:1-37](file://backend/app/models/subscription.py#L1-L37) + +### 索引策略 +- 查询表(queries) + - idx_queries_user_id:按用户过滤 + - idx_queries_status:按状态过滤 + - idx_queries_next_query_at:按下次查询时间调度 +- 引用记录表(citation_records) + - idx_citation_records_query_id:按查询聚合 + - idx_citation_records_queried_at:按时间排序/范围 + - idx_citation_records_platform:按平台统计 +- 查询任务表(query_tasks) + - idx_query_tasks_status:按状态调度 + +这些索引覆盖了常见查询路径与统计场景,有助于提升分页、过滤、排序与聚合的性能。 + +章节来源 +- [backend/alembic/versions/488d0bd5ab01_initial_migration.py:57-94](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L57-L94) +- [backend/app/models/query.py:50-54](file://backend/app/models/query.py#L50-L54) +- [backend/app/models/citation_record.py:37-41](file://backend/app/models/citation_record.py#L37-L41) +- [backend/app/models/query_task.py:36-38](file://backend/app/models/query_task.py#L36-L38) + +### SQLAlchemy ORM 模型实现 +- 字段类型与默认值 + - UUID 主键与外键,JSONB 存储数组/字典,布尔、整数、文本、数值、时间戳等。 + - 默认值通过 server_default/onupdate 设置,减少应用层重复逻辑。 +- 关系配置 + - 用户与查询、订阅为一对多;查询与引用记录、任务为一对多。 + - 级联删除与孤儿对象清理(delete-orphan),避免悬挂数据。 +- 查询封装 + - 服务层使用 select + func + join 封装复杂查询,统一处理分页与计数。 + - 权限校验:仅允许访问当前用户的资源,防止越权。 + +```mermaid +classDiagram +class User { ++id : uuid ++email : string ++password_hash : string ++name : string ++plan : string ++max_queries : int ++is_active : bool ++created_at : datetime ++updated_at : datetime ++queries ++subscriptions +} +class Query { ++id : uuid ++user_id : uuid ++keyword : string ++target_brand : string ++brand_aliases : list ++platforms : list ++frequency : string ++status : string ++last_queried_at : datetime ++next_query_at : datetime ++created_at : datetime ++updated_at : datetime ++user ++citation_records ++query_tasks +} +class CitationRecord { ++id : uuid ++query_id : uuid ++platform : string ++cited : bool ++citation_position : int ++citation_text : text ++competitor_brands : list ++raw_response : text ++queried_at : datetime ++query +} +class QueryTask { ++id : uuid ++query_id : uuid ++platform : string ++status : string ++error_message : text ++scheduled_at : datetime ++started_at : datetime ++completed_at : datetime ++query +} +class Subscription { ++id : uuid ++user_id : uuid ++plan : string ++status : string ++start_date : date ++end_date : date ++amount : float ++payment_method : string ++payment_id : string ++created_at : datetime ++user +} +User "1" <-- "many" Query : "拥有" +Query "1" <-- "many" CitationRecord : "产生" +Query "1" <-- "many" QueryTask : "触发" +User "1" <-- "many" Subscription : "订阅" +``` + +图表来源 +- [backend/app/models/user.py:11-41](file://backend/app/models/user.py#L11-L41) +- [backend/app/models/query.py:11-48](file://backend/app/models/query.py#L11-L48) +- [backend/app/models/citation_record.py:11-35](file://backend/app/models/citation_record.py#L11-L35) +- [backend/app/models/query_task.py:11-34](file://backend/app/models/query_task.py#L11-L34) +- [backend/app/models/subscription.py:11-36](file://backend/app/models/subscription.py#L11-L36) + +章节来源 +- [backend/app/models/user.py:1-41](file://backend/app/models/user.py#L1-L41) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [backend/app/models/citation_record.py:1-42](file://backend/app/models/citation_record.py#L1-L42) +- [backend/app/models/query_task.py:1-39](file://backend/app/models/query_task.py#L1-L39) +- [backend/app/models/subscription.py:1-37](file://backend/app/models/subscription.py#L1-L37) + +### 数据库迁移管理、版本控制与部署策略 +- 迁移入口 + - Alembic 环境加载 Base 元数据,支持离线/在线迁移。 + - 在线迁移通过异步引擎连接数据库,避免阻塞。 +- 初始迁移 + - 创建 users、queries、citation_records、query_tasks、subscriptions 表,并建立必要索引。 + - 外键约束与级联删除策略明确,确保数据一致性。 +- 部署策略 + - Docker Compose 启动 PostgreSQL 与 Redis,应用容器依赖数据库健康检查。 + - 生产环境建议将数据库与缓存分离,使用独立卷持久化数据。 + +```mermaid +flowchart TD +Start(["开始"]) --> CheckEnv["检查 DATABASE_URL"] +CheckEnv --> LoadBase["加载 Base 元数据"] +LoadBase --> Mode{"运行模式"} +Mode --> |离线| Offline["配置 URL 与元数据"] +Mode --> |在线| Online["创建异步引擎并连接"] +Offline --> RunMigs["执行迁移"] +Online --> RunMigs +RunMigs --> Done(["完成"]) +``` + +图表来源 +- [backend/alembic/env.py:33-88](file://backend/alembic/env.py#L33-L88) +- [backend/alembic/versions/488d0bd5ab01_initial_migration.py:21-128](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L21-L128) +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) + +章节来源 +- [backend/alembic/env.py:1-89](file://backend/alembic/env.py#L1-L89) +- [backend/alembic/versions/488d0bd5ab01_initial_migration.py:1-128](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L1-L128) +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) + +### 数据完整性约束、事务处理与并发控制 +- 完整性约束 + - 唯一约束:用户邮箱唯一 + - 外键约束:查询、任务、订阅均对用户做级联删除 + - JSONB 字段默认值:空数组/字典,避免 NULL 导致的条件判断复杂化 +- 事务处理 + - 服务层方法在单个事务内执行插入/更新/删除,提交后刷新对象状态 + - 会话工厂设置 expire_on_commit=False,减少后续查询的额外开销 +- 并发控制 + - 异步连接池与会话隔离,避免阻塞 + - 服务层在执行前进行权限校验与配额检查,降低并发冲突概率 + +章节来源 +- [backend/alembic/versions/488d0bd5ab01_initial_migration.py:36-111](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L36-L111) +- [backend/app/services/query.py:45-81](file://backend/app/services/query.py#L45-L81) +- [backend/app/database.py:12-18](file://backend/app/database.py#L12-L18) + +### 查询流程与优化要点 +- 查询列表与计数 + - 分页查询与计数分离,避免重复扫描全表 + - 使用索引覆盖 user_id 与 created_at 排序 +- 引用统计 + - 使用 JOIN 查询限定到用户所属的查询 + - 按平台分组统计,利用索引加速 queried_at 与 platform +- 导出 CSV + - 以查询为维度导出,先验证所有权再执行导出 + +```mermaid +sequenceDiagram +participant API as "API" +participant Svc as "服务" +participant DB as "数据库" +API->>Svc : get_citations(user_id, query_id?, platform?, dates?) +Svc->>DB : SELECT ... FROM citation_records JOIN queries WHERE ... +DB-->>Svc : 记录集 +Svc->>DB : COUNT ... FROM citation_records JOIN queries WHERE ... +DB-->>Svc : 总数 +Svc-->>API : 结果与总数 +``` + +图表来源 +- [backend/app/services/citation.py:24-73](file://backend/app/services/citation.py#L24-L73) + +章节来源 +- [backend/app/services/query.py:12-32](file://backend/app/services/query.py#L12-L32) +- [backend/app/services/citation.py:24-73](file://backend/app/services/citation.py#L24-L73) + +## 依赖分析 +- 模块耦合 + - 模型层仅依赖 Base 与 SQLAlchemy 类型,低耦合 + - 服务层依赖模型与会话,职责清晰 + - Alembic 依赖 Base 与配置,迁移脚本与模型同步演进 +- 外部依赖 + - PostgreSQL 异步驱动(asyncpg) + - Redis(用于缓存,如需要) + +```mermaid +graph LR +Cfg["配置
config.py"] --> DB["数据库引擎
database.py"] +DB --> Models["ORM 模型
models/*"] +Models --> Services["服务层
services/*"] +Alembic["迁移
alembic/*"] --> DB +Docker["编排
docker-compose.yml"] --> DB +``` + +图表来源 +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/models/__init__.py:1-14](file://backend/app/models/__init__.py#L1-L14) +- [backend/alembic/env.py:1-89](file://backend/alembic/env.py#L1-L89) +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) + +章节来源 +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/models/__init__.py:1-14](file://backend/app/models/__init__.py#L1-L14) +- [backend/alembic/env.py:1-89](file://backend/alembic/env.py#L1-L89) +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) + +## 性能考虑 +- 索引优化 + - 为高频过滤字段建立单列索引(user_id/status/next_query_at/platform/queried_at) + - 对 JSONB 字段可考虑 GIN 索引(如需复杂查询),当前迁移脚本未启用 +- 查询优化 + - 分页与计数分离,避免重复扫描 + - 使用 JOIN 限定用户范围,减少全表扫描 + - 时间范围查询使用索引覆盖 +- 缓存策略 + - 引用统计与趋势数据可缓存至 Redis,设置合理过期时间 + - 导出 CSV 可缓存热点查询结果,降低数据库压力 +- 连接与并发 + - 使用异步连接池,避免阻塞 + - 控制并发度,避免大量写入导致锁争用 + +[本节为通用指导,不直接分析具体文件] + +## 故障排查指南 +- 迁移失败 + - 检查 DATABASE_URL 是否正确,Alembic 配置与环境变量一致 + - 确认数据库已初始化且用户具备权限 +- 查询异常 + - 核对服务层权限校验逻辑,确认 user_id 与查询归属一致 + - 检查索引是否存在,必要时重建索引 +- 导出失败 + - 确认查询所有权校验通过 + - 检查 CSV 写入逻辑与字符编码 + +章节来源 +- [backend/alembic/env.py:33-88](file://backend/alembic/env.py#L33-L88) +- [backend/app/services/citation.py:14-22](file://backend/app/services/citation.py#L14-L22) + +## 结论 +本数据库设计围绕用户、查询、引用记录、任务与订阅五大实体展开,采用 PostgreSQL + SQLAlchemy Async + Alembic 的成熟技术栈,具备良好的扩展性与可维护性。通过合理的索引策略、事务边界与服务层封装,能够满足日常查询、统计与导出需求。建议在生产环境中进一步引入缓存与监控,持续优化查询路径与索引覆盖。 + +[本节为总结性内容,不直接分析具体文件] + +## 附录 +- 部署与运维 + - 使用 Docker Compose 启动数据库与应用,确保数据库健康检查通过后再启动应用 + - 生产环境建议使用独立数据库实例与只读副本,配合连接池与慢查询日志 +- 备份与恢复 + - 使用 pg_dump/pg_restore 进行逻辑备份与恢复 + - 对关键表定期增量备份,结合 WAL 归档实现点-in-time 恢复 +- 监控与告警 + - 监控连接数、查询延迟、索引命中率与慢查询 + - 对迁移脚本变更进行版本化管理与回滚演练 + +[本节为通用指导,不直接分析具体文件] \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/数据库设计/数据库迁移.md b/.qoder/repowiki/zh/content/数据库设计/数据库迁移.md new file mode 100644 index 0000000..02145b4 --- /dev/null +++ b/.qoder/repowiki/zh/content/数据库设计/数据库迁移.md @@ -0,0 +1,319 @@ +# 数据库迁移 + + +**本文引用的文件** +- [backend/alembic/versions/488d0bd5ab01_initial_migration.py](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py) +- [backend/alembic/env.py](file://backend/alembic/env.py) +- [backend/alembic.ini](file://backend/alembic.ini) +- [backend/alembic/script.py.mako](file://backend/alembic/script.py.mako) +- [backend/app/database.py](file://backend/app/database.py) +- [backend/app/models/__init__.py](file://backend/app/models/__init__.py) +- [backend/app/models/user.py](file://backend/app/models/user.py) +- [backend/app/models/query.py](file://backend/app/models/query.py) +- [backend/app/models/citation_record.py](file://backend/app/models/citation_record.py) +- [backend/app/models/query_task.py](file://backend/app/models/query_task.py) +- [backend/app/models/subscription.py](file://backend/app/models/subscription.py) +- [backend/app/config.py](file://backend/app/config.py) + + +## 目录 +1. [简介](#简介) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖分析](#依赖分析) +7. [性能考虑](#性能考虑) +8. [故障排查指南](#故障排查指南) +9. [结论](#结论) +10. [附录](#附录) + +## 简介 +本文件面向GEO项目的数据库迁移与版本管理,系统性说明基于Alembic的迁移框架配置与使用方式,覆盖迁移脚本生成、版本管理、数据库升级策略、初始迁移实现(含表创建、索引与约束)、迁移命令(upgrade、downgrade、autogenerate)以及生产环境最佳实践与风险控制。读者无需深入Python或SQLAlchemy即可理解并安全地执行迁移。 + +## 项目结构 +GEO后端采用异步SQLAlchemy与Alembic进行数据库迁移管理,迁移相关代码集中在backend/alembic目录中,并通过env.py与应用模型(models)建立连接。数据库连接由应用配置提供,迁移脚本位于versions子目录中,按版本顺序命名。 + +```mermaid +graph TB +subgraph "迁移配置" +A["alembic.ini
脚本位置、日志、URL等"] +B["env.py
在线/离线迁移入口"] +C["script.py.mako
迁移模板"] +end +subgraph "模型定义" +D["models/__init__.py
导出所有模型"] +E["models/user.py"] +F["models/query.py"] +G["models/citation_record.py"] +H["models/query_task.py"] +I["models/subscription.py"] +end +subgraph "数据库" +J["PostgreSQL 实例"] +end +A --> B +B --> D +D --> E +D --> F +D --> G +D --> H +D --> I +B --> J +``` + +**图表来源** +- [backend/alembic.ini:1-150](file://backend/alembic.ini#L1-L150) +- [backend/alembic/env.py:1-89](file://backend/alembic/env.py#L1-L89) +- [backend/alembic/script.py.mako:1-29](file://backend/alembic/script.py.mako#L1-L29) +- [backend/app/models/__init__.py:1-14](file://backend/app/models/__init__.py#L1-L14) +- [backend/app/models/user.py:1-41](file://backend/app/models/user.py#L1-L41) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [backend/app/models/citation_record.py:1-42](file://backend/app/models/citation_record.py#L1-L42) +- [backend/app/models/query_task.py:1-39](file://backend/app/models/query_task.py#L1-L39) +- [backend/app/models/subscription.py:1-37](file://backend/app/models/subscription.py#L1-L37) + +**章节来源** +- [backend/alembic.ini:1-150](file://backend/alembic.ini#L1-L150) +- [backend/alembic/env.py:1-89](file://backend/alembic/env.py#L1-L89) +- [backend/app/models/__init__.py:1-14](file://backend/app/models/__init__.py#L1-L14) + +## 核心组件 +- 迁移配置与入口 + - alembic.ini:定义脚本位置、日志级别、数据库URL等全局设置。 + - env.py:根据运行模式(在线/离线)加载目标元数据并执行迁移。 + - script.py.mako:迁移脚本模板,定义upgrade/downgrade骨架。 +- 模型与元数据 + - models/__init__.py:聚合导出所有ORM模型,供Alembic扫描。 + - 各模型文件:定义表结构、字段类型、索引与外键关系。 +- 数据库引擎 + - app/database.py:创建异步引擎与Base,为迁移提供元数据基础。 + - app/config.py:提供DATABASE_URL,被env.py与alembic.ini共同使用。 + +**章节来源** +- [backend/alembic.ini:1-150](file://backend/alembic.ini#L1-L150) +- [backend/alembic/env.py:1-89](file://backend/alembic/env.py#L1-L89) +- [backend/alembic/script.py.mako:1-29](file://backend/alembic/script.py.mako#L1-L29) +- [backend/app/models/__init__.py:1-14](file://backend/app/models/__init__.py#L1-L14) +- [backend/app/models/user.py:1-41](file://backend/app/models/user.py#L1-L41) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [backend/app/models/citation_record.py:1-42](file://backend/app/models/citation_record.py#L1-L42) +- [backend/app/models/query_task.py:1-39](file://backend/app/models/query_task.py#L1-L39) +- [backend/app/models/subscription.py:1-37](file://backend/app/models/subscription.py#L1-L37) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) + +## 架构总览 +下图展示从命令到数据库的迁移执行链路,包括在线与离线两种模式,以及模型元数据对迁移的影响。 + +```mermaid +sequenceDiagram +participant CLI as "命令行" +participant Alembic as "Alembic CLI" +participant Env as "env.py" +participant DB as "PostgreSQL" +CLI->>Alembic : 执行迁移命令 +Alembic->>Env : 加载配置与元数据 +Env->>Env : 判断在线/离线模式 +alt 在线模式 +Env->>DB : 异步连接并执行事务 +else 离线模式 +Env->>DB : 使用URL直接执行DDL +end +Env-->>Alembic : 返回迁移结果 +Alembic-->>CLI : 输出状态与日志 +``` + +**图表来源** +- [backend/alembic/env.py:33-89](file://backend/alembic/env.py#L33-L89) +- [backend/alembic.ini:86-89](file://backend/alembic.ini#L86-L89) + +## 详细组件分析 + +### 初始迁移脚本(488d0bd5ab01) +该脚本实现首次数据库初始化,包含以下要点: +- 表创建 + - users:用户主表,包含唯一邮箱、计划等级、配额、激活状态等字段。 + - queries:查询任务表,关联users,包含关键词、品牌、平台列表、频率、状态及时间戳。 + - citation_records:引用记录表,关联queries,包含平台、是否引用、竞品品牌、原始响应等。 + - query_tasks:查询任务执行表,关联queries,包含状态、错误信息与调度/开始/完成时间。 + - subscriptions:订阅表,关联users,包含计划、状态、起止日期、金额与支付信息。 +- 索引建立 + - queries:按user_id、status、next_query_at建立索引。 + - citation_records:按query_id、queried_at、platform建立索引。 + - query_tasks:按status建立索引。 +- 约束与默认值 + - 外键约束:各子表均对父表执行级联删除。 + - 默认值:广泛使用服务器默认值(如NOW()、JSONB空数组、字符串默认值)。 +- 回滚策略 + - downgrade按逆序删除表与索引,确保幂等与可恢复。 + +```mermaid +flowchart TD +Start(["执行初始迁移"]) --> CreateUsers["创建 users 表"] +CreateUsers --> CreateQueries["创建 queries 表
建立索引"] +CreateQueries --> CreateRecords["创建 citation_records 表
建立索引"] +CreateRecords --> CreateTasks["创建 query_tasks 表
建立索引"] +CreateTasks --> CreateSubs["创建 subscriptions 表"] +CreateSubs --> End(["完成"]) +``` + +**图表来源** +- [backend/alembic/versions/488d0bd5ab01_initial_migration.py:21-128](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L21-L128) + +**章节来源** +- [backend/alembic/versions/488d0bd5ab01_initial_migration.py:1-128](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L1-L128) + +### 迁移环境与元数据 +- 元数据来源 + - env.py通过app.database.Base.metadata提供目标元数据,确保Alembic能识别应用模型的变更。 + - models/__init__.py统一导出所有模型,避免遗漏。 +- 在线/离线模式 + - 在线模式:使用异步引擎连接数据库,适合生产与容器环境。 + - 离线模式:直接使用URL执行DDL,便于测试与快速验证。 +- 配置来源 + - DATABASE_URL来自app/config.py,同时在alembic.ini中也有默认示例URL。 + +```mermaid +graph LR +A["env.py"] --> B["Base.metadata"] +B --> C["models/*"] +A --> D["在线/离线执行器"] +D --> E["PostgreSQL"] +``` + +**图表来源** +- [backend/alembic/env.py:10-25](file://backend/alembic/env.py#L10-L25) +- [backend/app/models/__init__.py:1-14](file://backend/app/models/__init__.py#L1-L14) +- [backend/app/database.py](file://backend/app/database.py#L20) +- [backend/app/config.py](file://backend/app/config.py#L7) + +**章节来源** +- [backend/alembic/env.py:1-89](file://backend/alembic/env.py#L1-L89) +- [backend/app/models/__init__.py:1-14](file://backend/app/models/__init__.py#L1-L14) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) + +### 迁移命令与工作流 +- 常用命令 + - 升级:将数据库迁移到最新版本。 + - 降级:将数据库回退到指定版本。 + - 自动化生成:基于模型变更自动生成迁移脚本。 +- 工作流建议 + - 开发阶段:先在本地dev环境验证,再合并到测试环境。 + - 测试阶段:在隔离数据库上执行,验证数据完整性与索引有效性。 + - 生产阶段:严格遵循“只读优先”原则,先备份,再执行,最后验证。 + +```mermaid +flowchart TD +Dev["开发环境"] --> Test["测试环境"] +Test --> Prod["生产环境"] +Dev --> |dry-run/验证| Test +Test --> |备份/演练| Prod +``` + +[本节为通用流程说明,不直接分析具体文件,故无“章节来源”] + +### 数据保护、回滚与版本控制 +- 数据保护 + - 在生产环境执行前务必备份数据库。 + - 对大表操作(如重建索引)选择维护窗口,避免高峰时段。 +- 回滚机制 + - 初始迁移提供完整的downgrade路径,确保可逆。 + - 建议每次迁移仅包含单一逻辑变更,降低回滚复杂度。 +- 版本控制 + - 迁移脚本以版本号命名,保持顺序与可追溯性。 + - 将迁移脚本纳入版本控制系统,配合提交信息描述变更意图。 + +**章节来源** +- [backend/alembic/versions/488d0bd5ab01_initial_migration.py:114-128](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L114-L128) + +### 生产环境最佳实践与风险控制 +- 风险控制 + - 限制迁移窗口,避免业务高峰期。 + - 使用只读副本或独立测试库先行验证。 + - 对DDL操作进行分批执行,逐步替换索引与约束。 +- 可观测性 + - 记录迁移日志,监控执行时长与失败原因。 + - 在应用侧增加迁移状态检查,防止未完成迁移导致的数据不一致。 +- 安全与合规 + - 严格管理DATABASE_URL与访问权限。 + - 对敏感字段(如密码哈希)迁移需遵循最小暴露原则。 + +**章节来源** +- [backend/alembic.ini:115-150](file://backend/alembic.ini#L115-L150) +- [backend/alembic/env.py:33-89](file://backend/alembic/env.py#L33-L89) + +## 依赖分析 +- 组件耦合 + - env.py依赖app/config.py提供的DATABASE_URL与app/database.py的Base元数据。 + - models/__init__.py集中导出所有模型,提升扫描效率与一致性。 +- 外部依赖 + - PostgreSQL作为目标数据库,UUID、JSONB、时间戳等类型在迁移脚本中被广泛使用。 +- 潜在问题 + - 若models/__init__.py遗漏导出,可能导致autogenerate无法识别新模型。 + - 在线迁移依赖异步引擎,需确保网络与连接池配置正确。 + +```mermaid +graph TB +Env["env.py"] --> Cfg["app/config.py"] +Env --> DB["app/database.py"] +DB --> Meta["Base.metadata"] +Meta --> Models["models/*"] +``` + +**图表来源** +- [backend/alembic/env.py:10-25](file://backend/alembic/env.py#L10-L25) +- [backend/app/config.py](file://backend/app/config.py#L7) +- [backend/app/database.py](file://backend/app/database.py#L20) +- [backend/app/models/__init__.py:1-14](file://backend/app/models/__init__.py#L1-L14) + +**章节来源** +- [backend/alembic/env.py:1-89](file://backend/alembic/env.py#L1-L89) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/models/__init__.py:1-14](file://backend/app/models/__init__.py#L1-L14) + +## 性能考虑 +- 索引设计 + - 初始迁移已为高频查询字段建立索引,有助于提升查询性能。 + - 对于大表,建议在维护窗口内重建索引,避免阻塞。 +- 迁移窗口 + - 将DDL密集型操作安排在低峰时段,减少对线上服务的影响。 +- 幂等性 + - 迁移脚本应具备幂等特性,避免重复执行造成资源浪费。 + +**章节来源** +- [backend/alembic/versions/488d0bd5ab01_initial_migration.py:57-94](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L57-L94) + +## 故障排查指南 +- 常见问题 + - 迁移失败:检查DATABASE_URL是否正确,确认数据库可达。 + - 模型未识别:确保models/__init__.py导出了新增模型。 + - 在线/离线模式混淆:确认运行环境与配置文件一致。 +- 排查步骤 + - 查看日志:调整alembic.ini中的日志级别以获取更详细输出。 + - 回滚验证:使用downgrade验证回滚路径是否完整。 + - 数据核对:迁移完成后执行简单查询,核对关键索引与约束是否生效。 + +**章节来源** +- [backend/alembic.ini:115-150](file://backend/alembic.ini#L115-L150) +- [backend/alembic/env.py:33-89](file://backend/alembic/env.py#L33-L89) +- [backend/alembic/versions/488d0bd5ab01_initial_migration.py:114-128](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L114-L128) + +## 结论 +GEO项目已建立完善的Alembic迁移体系:清晰的配置、可靠的元数据扫描、完备的初始迁移脚本与回滚路径。遵循本文的命令使用方法、最佳实践与风险控制措施,可在保障数据安全的前提下高效推进数据库演进。 + +## 附录 +- 迁移命令速查 + - 升级到最新:alembic upgrade head + - 降级到上一版本:alembic downgrade -1 + - 降级到指定版本:alembic downgrade <版本号> + - 自动生成迁移:alembic revision --autogenerate -m "<描述>" +- 关键文件定位 + - 配置:backend/alembic.ini + - 环境:backend/alembic/env.py + - 模板:backend/alembic/script.py.mako + - 初始迁移:backend/alembic/versions/488d0bd5ab01_initial_migration.py + - 模型聚合:backend/app/models/__init__.py + - 数据库配置:backend/app/config.py \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/数据库设计/数据模型.md b/.qoder/repowiki/zh/content/数据库设计/数据模型.md new file mode 100644 index 0000000..fbfdc67 --- /dev/null +++ b/.qoder/repowiki/zh/content/数据库设计/数据模型.md @@ -0,0 +1,419 @@ +# 数据模型 + + +**本文引用的文件** +- [backend/app/models/__init__.py](file://backend/app/models/__init__.py) +- [backend/app/models/user.py](file://backend/app/models/user.py) +- [backend/app/models/query.py](file://backend/app/models/query.py) +- [backend/app/models/query_task.py](file://backend/app/models/query_task.py) +- [backend/app/models/citation_record.py](file://backend/app/models/citation_record.py) +- [backend/app/models/subscription.py](file://backend/app/models/subscription.py) +- [backend/app/database.py](file://backend/app/database.py) +- [backend/alembic/versions/488d0bd5ab01_initial_migration.py](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py) +- [backend/app/schemas/query.py](file://backend/app/schemas/query.py) +- [backend/app/schemas/citation.py](file://backend/app/schemas/citation.py) +- [backend/app/services/query.py](file://backend/app/services/query.py) +- [backend/app/api/queries.py](file://backend/app/api/queries.py) +- [backend/app/config.py](file://backend/app/config.py) + + +## 目录 +1. [简介](#简介) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖关系分析](#依赖关系分析) +7. [性能考量](#性能考量) +8. [故障排查指南](#故障排查指南) +9. [结论](#结论) +10. [附录](#附录) + +## 简介 +本文件系统性梳理 GEO 平台的 SQLAlchemy ORM 数据模型,覆盖模型类定义、字段映射与关系配置,解释模型间的关系映射(含级联与外键约束)、序列化/反序列化与数据验证机制、生命周期管理与事件钩子、以及使用示例与最佳实践。目标是帮助开发者快速理解并正确使用数据模型,确保在查询、任务调度、引用记录与订阅管理等场景中保持一致性与可维护性。 + +## 项目结构 +后端采用异步 SQLAlchemy(SQLAlchemy 2.x + asyncpg)与 Pydantic 模式进行数据建模与 API 层交互。数据库初始化通过 declarative_base 创建基类,迁移脚本由 Alembic 管理,模型导出统一在 models 包的 __init__.py 中聚合。 + +```mermaid +graph TB +subgraph "模型层" +U["User
用户"] +Q["Query
查询任务"] +QT["QueryTask
查询任务执行单元"] +CR["CitationRecord
引用记录"] +S["Subscription
订阅"] +end +subgraph "数据库" +DB["PostgreSQL"] +end +subgraph "服务层" +SVCQ["Query 服务"] +end +subgraph "API 层" +APIQ["Queries API"] +end +U --> Q +Q --> CR +Q --> QT +U --> S +SVCQ --> Q +APIQ --> SVCQ +Q --- DB +CR --- DB +QT --- DB +S --- DB +U --- DB +``` + +图表来源 +- [backend/app/models/user.py:11-41](file://backend/app/models/user.py#L11-L41) +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [backend/app/models/query_task.py:11-39](file://backend/app/models/query_task.py#L11-L39) +- [backend/app/models/citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) +- [backend/app/models/subscription.py:11-37](file://backend/app/models/subscription.py#L11-L37) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/services/query.py:1-130](file://backend/app/services/query.py#L1-L130) +- [backend/app/api/queries.py:1-86](file://backend/app/api/queries.py#L1-L86) + +章节来源 +- [backend/app/models/__init__.py:1-14](file://backend/app/models/__init__.py#L1-L14) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) + +## 核心组件 +本节概述各模型的职责、关键字段与关系,为后续深入分析打基础。 + +- 用户(User) + - 负责平台用户信息与配额控制,支持计划类型与最大查询数限制。 + - 关系:一对多到 Query、Subscription,删除时级联删除孤儿对象。 +- 查询(Query) + - 描述关键词、目标品牌、别名、平台集合、频率、状态及下次查询时间等。 + - 关系:多对一到 User;一对多到 CitationRecord、QueryTask;删除时级联删除孤儿对象。 +- 查询任务(QueryTask) + - 记录单次平台查询任务的状态、错误信息与时间戳。 + - 关系:多对一到 Query;删除时级联删除。 +- 引用记录(CitationRecord) + - 记录某次查询在特定平台上的引用情况、竞品品牌列表与原始响应摘要。 + - 关系:多对一到 Query;删除时级联删除。 +- 订阅(Subscription) + - 记录用户的订阅计划、有效期、支付信息与状态。 + - 关系:多对一到 User;删除时级联删除。 + +章节来源 +- [backend/app/models/user.py:11-41](file://backend/app/models/user.py#L11-L41) +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [backend/app/models/query_task.py:11-39](file://backend/app/models/query_task.py#L11-L39) +- [backend/app/models/citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) +- [backend/app/models/subscription.py:11-37](file://backend/app/models/subscription.py#L11-L37) + +## 架构总览 +下图展示模型与数据库表、索引、外键约束之间的对应关系,以及服务层与 API 层如何通过模型进行数据访问。 + +```mermaid +erDiagram +USERS { +uuid id PK +string email UK +string password_hash +string name +string plan +integer max_queries +boolean is_active +timestamptz created_at +timestamptz updated_at +} +QUERIES { +uuid id PK +uuid user_id FK +string keyword +string target_brand +jsonb brand_aliases +jsonb platforms +string frequency +string status +timestamptz last_queried_at +timestamptz next_query_at +timestamptz created_at +timestamptz updated_at +} +CITATION_RECORDS { +uuid id PK +uuid query_id FK +string platform +boolean cited +integer citation_position +text citation_text +jsonb competitor_brands +text raw_response +timestamptz queried_at +} +QUERY_TASKS { +uuid id PK +uuid query_id FK +string platform +string status +text error_message +timestamptz scheduled_at +timestamptz started_at +timestamptz completed_at +} +SUBSCRIPTIONS { +uuid id PK +uuid user_id FK +string plan +string status +date start_date +date end_date +numeric amount +string payment_method +string payment_id +timestamptz created_at +} +USERS ||--o{ QUERIES : "拥有" +USERS ||--o{ SUBSCRIPTIONS : "拥有" +QUERIES ||--o{ CITATION_RECORDS : "产生" +QUERIES ||--o{ QUERY_TASKS : "拆分执行" +``` + +图表来源 +- [backend/alembic/versions/488d0bd5ab01_initial_migration.py:21-128](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L21-L128) +- [backend/app/models/user.py:11-41](file://backend/app/models/user.py#L11-L41) +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [backend/app/models/citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) +- [backend/app/models/query_task.py:11-39](file://backend/app/models/query_task.py#L11-L39) +- [backend/app/models/subscription.py:11-37](file://backend/app/models/subscription.py#L11-L37) + +## 详细组件分析 + +### 用户模型(User) +- 表名与主键 + - 表名:users;主键:UUID 类型的 id。 +- 字段映射 + - 邮箱唯一且必填;密码哈希必填;名称可空;计划默认 free;最大查询数默认 5;激活状态默认 true;创建/更新时间自动填充。 +- 关系 + - 一对多到 Query、Subscription,删除用户时级联删除孤儿对象。 +- 生命周期与事件 + - created_at/updated_at 使用 server_default/onupdate 注入数据库侧时间戳。 +- 序列化/反序列化 + - 通过 Pydantic 模型(如查询响应)在 API 层进行序列化;ORM 对象可直接用于 FastAPI 响应模型(from_attributes)。 +- 最佳实践 + - 在创建/更新用户时避免直接修改计划或配额,建议通过专门的服务接口进行校验与审计。 + +章节来源 +- [backend/app/models/user.py:11-41](file://backend/app/models/user.py#L11-L41) +- [backend/alembic/versions/488d0bd5ab01_initial_migration.py:23-37](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L23-L37) + +### 查询模型(Query) +- 表名与主键 + - 表名:queries;主键:UUID 类型的 id。 +- 字段映射 + - 外键 user_id 指向 users.id,删除时级联;关键词与目标品牌必填;品牌别名与平台集合默认值合理;频率默认 weekly;状态默认 active;下次查询时间可空;创建/更新时间自动填充。 +- 关系 + - 多对一到 User;一对多到 CitationRecord、QueryTask,删除查询时级联删除孤儿对象。 +- 索引 + - 为 user_id、status、next_query_at 建立索引以优化查询。 +- 生命周期与事件 + - created_at/updated_at 使用 server_default/onupdate 注入数据库侧时间戳。 +- 序列化/反序列化 + - 响应模型 QueryResponse 支持 from_attributes,便于 ORM 对象直接转为 API 响应。 +- 最佳实践 + - 更新频率时同步更新 next_query_at;在创建查询前检查用户配额。 + +章节来源 +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [backend/alembic/versions/488d0bd5ab01_initial_migration.py:39-59](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L39-L59) + +### 查询任务模型(QueryTask) +- 表名与主键 + - 表名:query_tasks;主键:UUID 类型的 id。 +- 字段映射 + - 外键 query_id 指向 queries.id,删除时级联;平台必填;状态默认 pending;错误信息可空;调度/开始/完成时间可空。 +- 关系 + - 多对一到 Query;删除查询任务时级联删除。 +- 索引 + - 为 status 建立索引以支持任务调度筛选。 +- 生命周期与事件 + - scheduled_at 默认当前时间;其他时间戳按需更新。 +- 序列化/反序列化 + - 通过 Pydantic 模型进行 API 层序列化。 +- 最佳实践 + - 任务状态机:pending -> started -> completed 或 failed;失败时记录 error_message。 + +章节来源 +- [backend/app/models/query_task.py:11-39](file://backend/app/models/query_task.py#L11-L39) +- [backend/alembic/versions/488d0bd5ab01_initial_migration.py:80-94](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L80-L94) + +### 引用记录模型(CitationRecord) +- 表名与主键 + - 表名:citation_records;主键:UUID 类型的 id。 +- 字段映射 + - 外键 query_id 指向 queries.id,删除时级联;平台必填;是否引用默认 false;引用位置可空;引用文本可空;竞品品牌列表默认空数组;原始响应可空;查询时间默认当前时间。 +- 关系 + - 多对一到 Query;删除查询时级联删除。 +- 索引 + - 为 query_id、queried_at、platform 建立索引以优化统计与检索。 +- 生命周期与事件 + - queried_at 默认当前时间。 +- 序列化/反序列化 + - 通过 CitationResponse 进行 API 层序列化。 +- 最佳实践 + - 统计时按平台与日期聚合,结合索引提升性能。 + +章节来源 +- [backend/app/models/citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) +- [backend/alembic/versions/488d0bd5ab01_initial_migration.py:61-78](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L61-L78) + +### 订阅模型(Subscription) +- 表名与主键 + - 表名:subscriptions;主键:UUID 类型的 id。 +- 字段映射 + - 外键 user_id 指向 users.id,删除时级联;计划必填;状态默认 active;起止日期必填;金额可空;支付方式与支付 ID 可空;创建时间默认当前时间。 +- 关系 + - 多对一到 User;删除用户时级联删除。 +- 生命周期与事件 + - created_at 默认当前时间。 +- 序列化/反序列化 + - 通过 Pydantic 模型进行 API 层序列化。 +- 最佳实践 + - 订阅到期后应自动调整用户配额与功能权限。 + +章节来源 +- [backend/app/models/subscription.py:11-37](file://backend/app/models/subscription.py#L11-L37) +- [backend/alembic/versions/488d0bd5ab01_initial_migration.py:96-111](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L96-L111) + +### 模型关系与级联策略 +- 外键约束 + - 所有子表均设置外键指向父表主键,并在删除时采用 CASCADE,确保数据一致性。 +- 级联删除孤儿对象 + - User 的 queries、subscriptions;Query 的 citation_records、query_tasks 均配置了“all, delete-orphan”,保证删除父对象时自动清理其子对象。 +- 索引策略 + - 查询高频字段(如 user_id、status、next_query_at、queried_at、platform)建立索引,提升查询性能。 + +章节来源 +- [backend/app/models/user.py:35-40](file://backend/app/models/user.py#L35-L40) +- [backend/app/models/query.py:43-48](file://backend/app/models/query.py#L43-L48) +- [backend/app/models/query_task.py:36-38](file://backend/app/models/query_task.py#L36-L38) +- [backend/app/models/citation_record.py:37-41](file://backend/app/models/citation_record.py#L37-L41) + +### 序列化、反序列化与数据验证 +- Pydantic 验证 + - 查询创建/更新请求体包含平台集合、频率、状态等字段的严格校验,非法值会抛出异常。 +- ORM 到 API 的转换 + - 响应模型启用 from_attributes,允许直接将 ORM 对象转为 JSON 响应。 +- API 层集成 + - Queries API 将请求体绑定到 Pydantic 模型,调用服务层进行业务处理,再返回 ORM 对象或 Pydantic 响应模型。 + +章节来源 +- [backend/app/schemas/query.py:11-94](file://backend/app/schemas/query.py#L11-L94) +- [backend/app/schemas/citation.py:7-50](file://backend/app/schemas/citation.py#L7-L50) +- [backend/app/api/queries.py:1-86](file://backend/app/api/queries.py#L1-L86) + +### 生命周期管理与事件钩子 +- 时间戳管理 + - 所有模型的 created_at/updated_at 使用 server_default/onupdate 注入数据库侧时间戳,减少应用层负担。 +- 任务状态流转 + - QueryTask 的状态从 pending 到 started 再到 completed 或 failed,配合 scheduled_at/started_at/completed_at 字段记录生命周期节点。 +- 查询调度 + - 服务层根据频率计算 next_query_at,便于定时任务调度。 + +章节来源 +- [backend/app/models/user.py:25-33](file://backend/app/models/user.py#L25-L33) +- [backend/app/models/query.py:32-40](file://backend/app/models/query.py#L32-L40) +- [backend/app/models/query_task.py:27-32](file://backend/app/models/query_task.py#L27-L32) +- [backend/app/services/query.py:62-77](file://backend/app/services/query.py#L62-L77) + +### 使用示例与最佳实践 +- 创建查询 + - 步骤:校验用户配额 -> 计算 next_query_at -> 构造 Query -> 提交事务 -> 刷新对象。 + - 参考路径:[创建查询服务:45-81](file://backend/app/services/query.py#L45-L81) +- 更新查询 + - 步骤:读取查询 -> 排除未设置字段 -> 若更新频率则重算 next_query_at -> 提交事务 -> 刷新对象。 + - 参考路径:[更新查询服务:84-113](file://backend/app/services/query.py#L84-L113) +- 删除查询 + - 步骤:按用户与查询 ID 定位 -> 删除 -> 提交事务。 + - 参考路径:[删除查询服务:116-129](file://backend/app/services/query.py#L116-L129) +- API 调用 + - GET /queries -> 返回 QueryListResponse + - POST /queries -> 返回 QueryResponse + - GET /queries/{query_id} -> 返回 QueryResponse + - PUT /queries/{query_id} -> 返回 QueryResponse + - DELETE /queries/{query_id} -> 204 No Content + - 参考路径:[查询 API:15-86](file://backend/app/api/queries.py#L15-L86) + +章节来源 +- [backend/app/services/query.py:45-129](file://backend/app/services/query.py#L45-L129) +- [backend/app/api/queries.py:15-86](file://backend/app/api/queries.py#L15-L86) + +## 依赖关系分析 +- 数据库引擎与会话 + - 使用异步引擎与 async_sessionmaker,Base 作为 declarative_base 基类。 +- 模型导出 + - models/__init__.py 统一导出所有模型,便于上层模块按需导入。 +- 迁移脚本 + - Alembic 初始迁移脚本定义了表结构、索引与外键约束,与模型定义保持一致。 + +```mermaid +graph LR +CFG["配置
DATABASE_URL"] --> ENG["异步引擎"] +ENG --> SESS["AsyncSessionLocal"] +SESS --> BASE["declarative_base"] +BASE --> MODELS["模型类"] +MODELS --> DB["PostgreSQL"] +``` + +图表来源 +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/config.py](file://backend/app/config.py#L7) + +章节来源 +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) + +## 性能考量 +- 索引设计 + - queries:user_id、status、next_query_at + - citation_records:query_id、queried_at、platform + - query_tasks:status + - 建议:基于实际查询模式持续评估与补充索引。 +- 查询优化 + - 使用 select + order_by + offset + limit 实现分页与排序。 + - 使用 func.count 统计总数,避免不必要的全量加载。 +- 异步 I/O + - 使用 asyncpg 与 SQLAlchemy 异步引擎,降低并发场景下的阻塞风险。 +- 缓存策略 + - 对热点查询结果(如用户配额、订阅状态)可引入 Redis 缓存,减少数据库压力。 + +## 故障排查指南 +- 查询配额超限 + - 现象:创建查询时报错“PermissionError: Query limit exceeded” + - 处理:检查用户 max_queries 与当前查询数量,必要时升级计划或清理历史查询。 + - 参考路径:[创建查询服务:45-81](file://backend/app/services/query.py#L45-L81) +- 查询不存在 + - 现象:GET/PUT/DELETE 查询返回 404 + - 处理:确认 query_id 与当前用户匹配;检查软删除/级联删除是否生效。 + - 参考路径:[查询 API:42-85](file://backend/app/api/queries.py#L42-L85) +- 平台/频率参数非法 + - 现象:Pydantic 校验失败 + - 处理:核对平台集合是否在允许集合内,频率是否为 daily/weekly。 + - 参考路径:[查询请求体校验:18-33](file://backend/app/schemas/query.py#L18-L33) +- 任务状态异常 + - 现象:任务长时间 pending 或失败 + - 处理:检查 error_message 字段;核对平台可用性与 API 密钥配置。 + - 参考路径:[查询任务模型:11-39](file://backend/app/models/query_task.py#L11-L39) + +章节来源 +- [backend/app/services/query.py:45-129](file://backend/app/services/query.py#L45-L129) +- [backend/app/api/queries.py:42-85](file://backend/app/api/queries.py#L42-L85) +- [backend/app/schemas/query.py:18-33](file://backend/app/schemas/query.py#L18-L33) +- [backend/app/models/query_task.py:11-39](file://backend/app/models/query_task.py#L11-L39) + +## 结论 +GEO 项目的数据模型围绕用户、查询、任务、引用记录与订阅五大实体构建,采用异步 SQLAlchemy ORM 与 Alembic 迁移管理,配合 Pydantic 的输入输出验证,形成清晰的领域模型与 API 边界。通过合理的外键约束、级联策略与索引设计,既保证了数据一致性,也兼顾了查询性能。建议在生产环境中持续监控查询性能与缓存命中率,并完善事件钩子与审计日志以增强可观测性。 + +## 附录 +- 数据库连接配置 + - DATABASE_URL:PostgreSQL 异步连接字符串 + - 参考路径:[配置](file://backend/app/config.py#L7) +- 模型导出入口 + - models/__init__.py 统一导出所有模型 + - 参考路径:[模型导出:1-14](file://backend/app/models/__init__.py#L1-L14) \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/数据库设计/表结构设计.md b/.qoder/repowiki/zh/content/数据库设计/表结构设计.md new file mode 100644 index 0000000..fcbe8f8 --- /dev/null +++ b/.qoder/repowiki/zh/content/数据库设计/表结构设计.md @@ -0,0 +1,501 @@ +# 表结构设计 + + +**本文档引用的文件** +- [user.py](file://backend/app/models/user.py) +- [query.py](file://backend/app/models/query.py) +- [citation_record.py](file://backend/app/models/citation_record.py) +- [query_task.py](file://backend/app/models/query_task.py) +- [subscription.py](file://backend/app/models/subscription.py) +- [initial_migration.py](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py) +- [database.py](file://backend/app/database.py) +- [config.py](file://backend/app/config.py) + + +## 目录 +1. [简介](#简介) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构概览](#架构概览) +5. [详细组件分析](#详细组件分析) +6. [依赖分析](#依赖分析) +7. [性能考虑](#性能考虑) +8. [故障排除指南](#故障排除指南) +9. [结论](#结论) + +## 简介 + +本文件详细描述GEO项目的数据库表结构设计,涵盖用户查询监控平台的核心数据模型。该系统采用PostgreSQL作为主要数据库,使用SQLAlchemy ORM进行对象关系映射,支持异步数据库操作。系统包含五个核心表:users(用户表)、queries(查询表)、citation_records(引用记录表)、query_tasks(查询任务表)和subscriptions(订阅表),这些表通过外键关系相互关联,形成完整的业务数据模型。 + +## 项目结构 + +GEO项目的数据库层采用分层架构设计,主要包含以下组件: + +```mermaid +graph TB +subgraph "数据库层" +DB[(PostgreSQL数据库)] +Engine[异步引擎] +Session[会话管理] +end +subgraph "ORM模型层" +User[User模型] +Query[Query模型] +Citation[CitationRecord模型] +Task[QueryTask模型] +Subscription[Subscription模型] +end +subgraph "配置层" +Config[配置管理] +Alembic[迁移管理] +end +Config --> Engine +Engine --> DB +Session --> DB +User --> DB +Query --> DB +Citation --> DB +Task --> DB +Subscription --> DB +Alembic --> DB +``` + +**图表来源** +- [database.py:1-29](file://backend/app/database.py#L1-L29) +- [config.py:4-17](file://backend/app/config.py#L4-L17) + +**章节来源** +- [database.py:1-29](file://backend/app/database.py#L1-L29) +- [config.py:4-17](file://backend/app/config.py#L4-L17) + +## 核心组件 + +### 数据库连接配置 + +系统使用异步PostgreSQL连接,配置参数如下: +- 数据库URL: `postgresql+asyncpg://postgres:postgres123@db:5432/geo_platform` +- Redis连接: `redis://redis:6379/0` +- JWT密钥: `your-secret-key-change-in-production` +- Playwright浏览器路径: `/ms-playwright` + +### 异步数据库引擎 + +使用SQLAlchemy异步引擎创建数据库连接池,支持: +- 异步会话管理 +- 连接池配置 +- 自动事务处理 +- 连接生命周期管理 + +**章节来源** +- [config.py:7](file://backend/app/config.py#L7) +- [database.py:6-18](file://backend/app/database.py#L6-L18) + +## 架构概览 + +GEO系统的数据库架构采用标准的三层设计模式: + +```mermaid +erDiagram +USERS { +uuid id PK +string email UK +string password_hash +string name +string plan +integer max_queries +boolean is_active +timestamp created_at +timestamp updated_at +} +QUERIES { +uuid id PK +uuid user_id FK +string keyword +string target_brand +jsonb brand_aliases +jsonb platforms +string frequency +string status +timestamp last_queried_at +timestamp next_query_at +timestamp created_at +timestamp updated_at +} +CITATION_RECORDS { +uuid id PK +uuid query_id FK +string platform +boolean cited +integer citation_position +text citation_text +jsonb competitor_brands +text raw_response +timestamp queried_at +} +QUERY_TASKS { +uuid id PK +uuid query_id FK +string platform +string status +text error_message +timestamp scheduled_at +timestamp started_at +timestamp completed_at +} +SUBSCRIPTIONS { +uuid id PK +uuid user_id FK +string plan +string status +date start_date +date end_date +numeric amount +string payment_method +string payment_id +timestamp created_at +} +USERS ||--o{ QUERIES : "拥有" +QUERIES ||--o{ CITATION_RECORDS : "包含" +QUERIES ||--o{ QUERY_TASKS : "包含" +USERS ||--o{ SUBSCRIPTIONS : "拥有" +``` + +**图表来源** +- [initial_migration.py:24-37](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L24-L37) +- [initial_migration.py:40-56](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L40-L56) +- [initial_migration.py:62-78](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L62-L78) +- [initial_migration.py:81-94](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L81-L94) +- [initial_migration.py:97-111](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L97-L111) + +## 详细组件分析 + +### 用户表 (users) + +用户表是整个系统的核心实体,存储平台用户的基本信息和账户状态。 + +#### 字段定义 + +| 字段名 | 数据类型 | 约束条件 | 描述 | +|--------|----------|----------|------| +| id | UUID | 主键, 非空, 默认值 | 用户唯一标识符 | +| email | String(255) | 唯一, 非空 | 用户邮箱地址 | +| password_hash | String(255) | 非空 | 用户密码哈希值 | +| name | String(100) | 可空 | 用户姓名 | +| plan | String(20) | 非空, 默认值: "free" | 用户套餐类型 | +| max_queries | Integer | 非空, 默认值: 5 | 最大查询次数限制 | +| is_active | Boolean | 非空, 默认值: true | 账户激活状态 | +| created_at | Timestamp | 非空, 默认值: NOW() | 创建时间 | +| updated_at | Timestamp | 非空, 默认值: NOW() | 更新时间 | + +#### 约束和索引 + +- 主键: id (UUID) +- 唯一约束: email +- 外键: 无直接外键关系 +- 关系: 与查询表(一对多)、订阅表(一对多) + +#### 业务规则 + +- 用户必须提供唯一的邮箱地址 +- 默认免费套餐,最大查询次数为5次 +- 账户默认激活状态 +- 时间戳自动管理 + +**章节来源** +- [user.py:14-33](file://backend/app/models/user.py#L14-L33) +- [initial_migration.py:24-37](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L24-L37) + +### 查询表 (queries) + +查询表记录用户的搜索关键词和品牌监控配置。 + +#### 字段定义 + +| 字段名 | 数据类型 | 约束条件 | 描述 | +|--------|----------|----------|------| +| id | UUID | 主键, 非空, 默认值 | 查询记录唯一标识符 | +| user_id | UUID | 外键, 非空 | 关联用户ID | +| keyword | String(200) | 非空 | 搜索关键词 | +| target_brand | String(100) | 非空 | 目标品牌名称 | +| brand_aliases | JSONB | 非空, 默认值: [] | 品牌别名列表 | +| platforms | JSONB | 非空, 默认值: ["wenxin","kimi"] | 监控平台列表 | +| frequency | String(20) | 非空, 默认值: "weekly" | 查询频率 | +| status | String(20) | 非空, 默认值: "active" | 查询状态 | +| last_queried_at | Timestamp | 可空 | 最后查询时间 | +| next_query_at | Timestamp | 可空 | 下次查询时间 | +| created_at | Timestamp | 非空, 默认值: NOW() | 创建时间 | +| updated_at | Timestamp | 非空, 默认值: NOW() | 更新时间 | + +#### 约束和索引 + +- 主键: id (UUID) +- 外键: user_id → users.id (级联删除) +- 索引: idx_queries_user_id, idx_queries_status, idx_queries_next_query_at +- 关系: 与用户表(多对一)、引用记录表(一对多)、查询任务表(一对多) + +#### 业务规则 + +- 必须关联有效用户 +- 平台默认监控"wenxin"和"kimi" +- 状态默认为"active" +- 支持定时查询调度 +- 级联删除确保数据一致性 + +**章节来源** +- [query.py:14-48](file://backend/app/models/query.py#L14-L48) +- [initial_migration.py:40-56](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L40-L56) + +### 引用记录表 (citation_records) + +引用记录表存储每次查询的具体结果和竞争情报。 + +#### 字段定义 + +| 字段名 | 数据类型 | 约束条件 | 描述 | +|--------|----------|----------|------| +| id | UUID | 主键, 非空, 默认值 | 引用记录唯一标识符 | +| query_id | UUID | 外键, 非空 | 关联查询ID | +| platform | String(50) | 非空 | 查询平台名称 | +| cited | Boolean | 非空, 默认值: false | 是否被引用 | +| citation_position | Integer | 可空 | 引用位置 | +| citation_text | Text | 可空 | 引用文本内容 | +| competitor_brands | JSONB | 非空, 默认值: [] | 竞争品牌列表 | +| raw_response | Text | 可空 | 原始响应内容 | +| queried_at | Timestamp | 非空, 默认值: NOW() | 查询时间 | + +#### 约束和索引 + +- 主键: id (UUID) +- 外键: query_id → queries.id (级联删除) +- 索引: idx_citation_records_query_id, idx_citation_records_queried_at, idx_citation_records_platform +- 关系: 与查询表(多对一) + +#### 业务规则 + +- 必须关联有效查询 +- 引用状态默认未引用 +- 支持JSON格式的竞争品牌数据 +- 按平台和查询时间建立索引优化查询性能 +- 级联删除确保查询历史完整清理 + +**章节来源** +- [citation_record.py:14-35](file://backend/app/models/citation_record.py#L14-L35) +- [initial_migration.py:62-78](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L62-L78) + +### 查询任务表 (query_tasks) + +查询任务表管理异步查询任务的状态和执行信息。 + +#### 字段定义 + +| 字段名 | 数据类型 | 约束条件 | 描述 | +|--------|----------|----------|------| +| id | UUID | 主键, 非空, 默认值 | 任务唯一标识符 | +| query_id | UUID | 外键, 非空 | 关联查询ID | +| platform | String(50) | 非空 | 执行平台 | +| status | String(20) | 非空, 默认值: "pending" | 任务状态 | +| error_message | Text | 可空 | 错误信息 | +| scheduled_at | Timestamp | 非空, 默认值: NOW() | 调度时间 | +| started_at | Timestamp | 可空 | 开始执行时间 | +| completed_at | Timestamp | 可空 | 完成时间 | + +#### 约束和索引 + +- 主键: id (UUID) +- 外键: query_id → queries.id (级联删除) +- 索引: idx_query_tasks_status +- 关系: 与查询表(多对一) + +#### 业务规则 + +- 必须关联有效查询 +- 任务状态默认为"pending" +- 支持任务执行跟踪 +- 状态变更自动记录时间戳 +- 级联删除确保任务历史清理 + +**章节来源** +- [query_task.py:14-34](file://backend/app/models/query_task.py#L14-L34) +- [initial_migration.py:81-94](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L81-L94) + +### 订阅表 (subscriptions) + +订阅表管理用户的付费订阅信息和状态。 + +#### 字段定义 + +| 字段名 | 数据类型 | 约束条件 | 描述 | +|--------|----------|----------|------| +| id | UUID | 主键, 非空, 默认值 | 订阅记录唯一标识符 | +| user_id | UUID | 外键, 非空 | 关联用户ID | +| plan | String(20) | 非空 | 套餐类型 | +| status | String(20) | 非空, 默认值: "active" | 订阅状态 | +| start_date | Date | 非空 | 订阅开始日期 | +| end_date | Date | 非空 | 订阅结束日期 | +| amount | Numeric(10,2) | 可空 | 支付金额 | +| payment_method | String(50) | 可空 | 支付方式 | +| payment_id | String(255) | 可空 | 支付ID | +| created_at | Timestamp | 非空, 默认值: NOW() | 创建时间 | + +#### 约束和索引 + +- 主键: id (UUID) +- 外键: user_id → users.id (级联删除) +- 关系: 与用户表(多对一) + +#### 业务规则 + +- 必须关联有效用户 +- 状态默认为"active" +- 支持多种支付方式 +- 日期范围确保订阅有效性 +- 级联删除确保订阅历史清理 + +**章节来源** +- [subscription.py:14-36](file://backend/app/models/subscription.py#L14-L36) +- [initial_migration.py:97-111](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L97-L111) + +## 依赖分析 + +### 外键关系图 + +```mermaid +graph LR +Users["Users表
主键: id"] --> |外键: user_id| Queries["Queries表
主键: id"] +Queries --> |外键: query_id| CitationRecords["CitationRecords表
主键: id"] +Queries --> |外键: query_id| QueryTasks["QueryTasks表
主键: id"] +Users --> |外键: user_id| Subscriptions["Subscriptions表
主键: id"] +style Users fill:#e1f5fe +style Queries fill:#f3e5f5 +style CitationRecords fill:#e8f5e8 +style QueryTasks fill:#fff3e0 +style Subscriptions fill:#fce4ec +``` + +**图表来源** +- [initial_migration.py:55](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L55) +- [initial_migration.py:74](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L74) +- [initial_migration.py:92](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L92) +- [initial_migration.py:110](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L110) + +### 关系类型分析 + +#### 一对一关系 +- users ↔ subscriptions: 一个用户对应一个当前有效的订阅记录 + +#### 一对多关系 +- users → queries: 一个用户可以有多个查询记录 +- users → subscriptions: 一个用户可以有多个订阅记录 +- queries → citation_records: 一个查询可以有多个引用记录 +- queries → query_tasks: 一个查询可以有多个任务 + +#### 多对多关系 +- 通过中间表实现的多对多关系不存在于当前设计中 + +### 级联删除策略 + +所有外键关系都设置了级联删除策略: +- 删除用户时,自动删除其所有查询、订阅记录 +- 删除查询时,自动删除其所有引用记录和任务 + +**章节来源** +- [initial_migration.py:55](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L55) +- [initial_migration.py:74](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L74) +- [initial_migration.py:92](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L92) +- [initial_migration.py:110](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L110) + +## 性能考虑 + +### 索引策略 + +系统为关键查询字段建立了专门的索引: + +#### 查询表索引 +- idx_queries_user_id: 提升按用户查询的性能 +- idx_queries_status: 支持状态过滤查询 +- idx_queries_next_query_at: 优化定时任务调度 + +#### 引用记录表索引 +- idx_citation_records_query_id: 提升按查询获取记录的性能 +- idx_citation_records_queried_at: 支持时间序列查询 +- idx_citation_records_platform: 优化平台过滤 + +#### 查询任务表索引 +- idx_query_tasks_status: 支持任务状态统计和调度 + +### 数据类型优化 + +- 使用UUID作为主键,避免序列号暴露业务信息 +- JSONB类型存储动态配置,支持高效查询和更新 +- 数值类型使用Numeric精确存储金额数据 +- 时间戳使用带时区的DateTime确保时区一致性 + +### 查询优化建议 + +1. **批量操作**: 对于大量数据的插入和更新,使用批量操作减少数据库往返 +2. **分页查询**: 对于列表查询,实现分页机制避免一次性加载过多数据 +3. **缓存策略**: 结合Redis实现热点数据缓存 +4. **连接池管理**: 合理配置连接池大小以平衡性能和资源使用 + +## 故障排除指南 + +### 常见问题及解决方案 + +#### 数据库连接问题 +- **症状**: 应用启动时报数据库连接错误 +- **原因**: DATABASE_URL配置不正确或数据库服务未启动 +- **解决**: 检查.env文件中的DATABASE_URL配置,确认数据库服务正常运行 + +#### 外键约束冲突 +- **症状**: 删除用户时报外键约束错误 +- **原因**: 存在相关记录导致级联删除失败 +- **解决**: 确保级联删除策略正确配置,检查数据完整性 + +#### 索引性能问题 +- **症状**: 查询响应缓慢 +- **原因**: 缺少必要的索引或索引设计不当 +- **解决**: 分析查询计划,添加适当的索引 + +#### 数据类型转换错误 +- **症状**: JSONB字段操作报错 +- **原因**: 数据类型不匹配或格式错误 +- **解决**: 确保JSONB数据格式正确,使用适当的序列化方法 + +### 调试工具 + +1. **数据库监控**: 使用PostgreSQL内置监控工具查看查询性能 +2. **日志分析**: 启用SQLAlchemy日志输出分析查询执行情况 +3. **性能分析**: 使用EXPLAIN ANALYZE分析慢查询 + +**章节来源** +- [config.py:7](file://backend/app/config.py#L7) +- [initial_migration.py:114-127](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L114-L127) + +## 结论 + +GEO项目的数据库表结构设计体现了现代Web应用的最佳实践: + +### 设计优势 + +1. **清晰的数据模型**: 五个核心表覆盖了完整的业务场景 +2. **合理的外键关系**: 确保数据一致性和完整性 +3. **高效的索引策略**: 针对常见查询模式优化性能 +4. **异步数据库支持**: 提供良好的并发处理能力 +5. **灵活的数据类型**: JSONB支持动态配置需求 + +### 业务逻辑实现 + +- 用户权限控制通过用户表的激活状态实现 +- 查询配额控制通过用户表的max_queries字段实现 +- 订阅管理通过订阅表的日期范围和状态字段实现 +- 数据清理通过级联删除策略自动维护 + +### 扩展性考虑 + +系统设计具有良好的扩展性: +- 新增表时可复用现有的UUID主键模式 +- JSONB字段支持未来功能的动态配置 +- 异步架构支持水平扩展 +- 清晰的关系设计便于添加新的业务实体 + +这个数据模型为GEO平台提供了坚实的基础,能够支持用户查询监控、竞争情报分析和订阅管理等核心业务功能。 \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/测试策略/单元测试.md b/.qoder/repowiki/zh/content/测试策略/单元测试.md new file mode 100644 index 0000000..085ac7f --- /dev/null +++ b/.qoder/repowiki/zh/content/测试策略/单元测试.md @@ -0,0 +1,497 @@ +# 单元测试 + + +**本文档引用的文件** +- [tests/conftest.py](file://tests/conftest.py) +- [tests/test_auth.py](file://tests/test_auth.py) +- [tests/test_citation_engine.py](file://tests/test_citation_engine.py) +- [tests/test_citations.py](file://tests/test_citations.py) +- [tests/test_queries.py](file://tests/test_queries.py) +- [backend/app/main.py](file://backend/app/main.py) +- [backend/app/api/deps.py](file://backend/app/api/deps.py) +- [backend/app/api/auth.py](file://backend/app/api/auth.py) +- [backend/app/api/citations.py](file://backend/app/api/citations.py) +- [backend/app/api/queries.py](file://backend/app/api/queries.py) +- [backend/app/workers/citation_engine.py](file://backend/app/workers/citation_engine.py) +- [backend/app/services/auth.py](file://backend/app/services/auth.py) +- [backend/app/models/user.py](file://backend/app/models/user.py) +- [backend/app/models/citation_record.py](file://backend/app/models/citation_record.py) + + +## 目录 +1. [简介](#简介) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖分析](#依赖分析) +7. [性能考虑](#性能考虑) +8. [故障排查指南](#故障排查指南) +9. [结论](#结论) +10. [附录](#附录) + +## 简介 +本文件系统性梳理 GEO 项目的单元测试实现,覆盖认证模块、引用引擎、查询处理与引用数据四大模块的测试用例设计与最佳实践。文档重点说明测试夹具(fixture)的使用方法(如 mock_user、auth_token、auth_headers、override_get_current_user),断言编写方式与覆盖率建议,并提供异步函数、依赖注入与错误处理的测试示例路径与流程图。 + +## 项目结构 +测试目录位于仓库根目录 tests/,采用按功能分模块组织的方式,分别对应后端 API 层与核心业务组件: +- 认证模块测试:tests/test_auth.py +- 引用引擎测试:tests/test_citation_engine.py +- 查询处理测试:tests/test_queries.py +- 引用数据测试:tests/test_citations.py +- 测试夹具与异步客户端:tests/conftest.py +- 后端应用入口与路由:backend/app/main.py、backend/app/api/*.py +- 核心业务组件:backend/app/workers/citation_engine.py +- 服务层与模型:backend/app/services/*、backend/app/models/* + +```mermaid +graph TB +subgraph "测试层" +T1["tests/test_auth.py"] +T2["tests/test_citation_engine.py"] +T3["tests/test_queries.py"] +T4["tests/test_citations.py"] +C["tests/conftest.py"] +end +subgraph "后端应用" +M["backend/app/main.py"] +A1["backend/app/api/auth.py"] +A2["backend/app/api/queries.py"] +A3["backend/app/api/citations.py"] +D["backend/app/api/deps.py"] +S1["backend/app/services/auth.py"] +W["backend/app/workers/citation_engine.py"] +U["backend/app/models/user.py"] +CR["backend/app/models/citation_record.py"] +end +T1 --> A1 +T2 --> W +T3 --> A2 +T4 --> A3 +T1 --> D +T2 --> W +T3 --> D +T4 --> D +A1 --> S1 +A2 --> D +A3 --> D +D --> U +W --> CR +M --> A1 +M --> A2 +M --> A3 +C --> M +``` + +图表来源 +- [tests/conftest.py:1-71](file://tests/conftest.py#L1-L71) +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/app/api/queries.py:1-86](file://backend/app/api/queries.py#L1-L86) +- [backend/app/api/citations.py:1-78](file://backend/app/api/citations.py#L1-L78) +- [backend/app/api/deps.py:1-43](file://backend/app/api/deps.py#L1-L43) +- [backend/app/services/auth.py:1-69](file://backend/app/services/auth.py#L1-L69) +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) +- [backend/app/models/user.py:1-41](file://backend/app/models/user.py#L1-L41) +- [backend/app/models/citation_record.py:1-42](file://backend/app/models/citation_record.py#L1-L42) + +章节来源 +- [tests/conftest.py:1-71](file://tests/conftest.py#L1-L71) +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) + +## 核心组件 +本节概述各模块测试关注点与测试夹具的作用范围。 + +- 认证模块测试(tests/test_auth.py) + - 注册成功/重复邮箱、登录成功/密码错误、用户信息获取(含未认证场景) + - 使用夹具:mock_registered_user、async_client、patch 注入服务层行为 + - 关键断言:HTTP 状态码、响应体字段、错误详情消息 + +- 引用引擎测试(tests/test_citation_engine.py) + - 品牌匹配器精确/别名/模糊/无匹配;竞争品牌检测;引用位置与上下文提取 + - 使用夹具:直接构造类实例进行纯函数式断言,无需外部依赖 + +- 查询处理测试(tests/test_queries.py) + - 创建查询成功/超出限额、列出查询、更新查询、删除查询、查询不存在/不属于他人 + - 使用夹具:mock_query、async_client、patch 服务层返回值或异常 + +- 引用数据测试(tests/test_citations.py) + - 获取引用列表、统计信息、CSV 导出(MIME 类型、附件头、内容片段) + +- 测试夹具(tests/conftest.py) + - mock_scheduler:屏蔽后台调度器,避免真实任务影响测试 + - mock_user:模拟认证用户对象,包含 id、邮箱、计划、配额等 + - override_get_current_user:重写依赖 get_current_user,使路由自动获得认证用户 + - auth_token:基于 mock_user 生成 JWT + - auth_headers:组装 Authorization Bearer 头 + - async_client:ASGI 异步 HTTP 客户端,用于端到端测试 + +章节来源 +- [tests/test_auth.py:1-104](file://tests/test_auth.py#L1-L104) +- [tests/test_citation_engine.py:1-54](file://tests/test_citation_engine.py#L1-L54) +- [tests/test_queries.py:1-154](file://tests/test_queries.py#L1-L154) +- [tests/test_citations.py:1-93](file://tests/test_citations.py#L1-L93) +- [tests/conftest.py:19-71](file://tests/conftest.py#L19-L71) + +## 架构总览 +下图展示测试夹具与被测组件之间的交互关系,以及异步 HTTP 客户端如何驱动 FastAPI 路由与依赖注入。 + +```mermaid +sequenceDiagram +participant Test as "测试用例" +participant Fixture as "测试夹具(conftest.py)" +participant Client as "AsyncClient" +participant App as "FastAPI 应用(main.py)" +participant Dep as "依赖注入(deps.py)" +participant Service as "服务层(API 路由)" +participant Model as "模型(models)" +Test->>Fixture : 请求夹具(mock_user/override_get_current_user/...) +Fixture-->>Test : 返回夹具对象 +Test->>Client : 发起 HTTP 请求 +Client->>App : ASGI 请求 +App->>Dep : 解析 OAuth2 Bearer Token +Dep-->>App : 返回当前用户(get_current_user) +App->>Service : 调用路由处理器 +Service->>Model : 数据库读写/查询 +Service-->>App : 返回响应数据 +App-->>Client : HTTP 响应 +Client-->>Test : 断言结果 +``` + +图表来源 +- [tests/conftest.py:28-71](file://tests/conftest.py#L28-L71) +- [backend/app/main.py:38-42](file://backend/app/main.py#L38-L42) +- [backend/app/api/deps.py:16-43](file://backend/app/api/deps.py#L16-L43) +- [backend/app/api/auth.py:13-43](file://backend/app/api/auth.py#L13-L43) +- [backend/app/api/queries.py:15-86](file://backend/app/api/queries.py#L15-L86) +- [backend/app/api/citations.py:25-78](file://backend/app/api/citations.py#L25-L78) + +## 详细组件分析 + +### 认证模块测试 +- 测试要点 + - 注册接口:成功返回 201,响应包含邮箱与姓名;重复邮箱抛出 400 + - 登录接口:成功返回 200,包含 access_token 与用户信息;错误凭据返回 401 + - 用户信息接口:已认证返回 200,未认证返回 401 +- 测试夹具 + - mock_registered_user:模拟注册成功的用户对象 + - override_get_current_user:通过依赖覆盖提供认证用户 + - auth_headers:携带 Bearer Token 的请求头 + - async_client:ASGI 异步客户端 +- 断言策略 + - 状态码断言:201/200/400/401 + - 响应体字段断言:access_token、token_type、user.email 等 + - 错误详情断言:detail 中包含特定提示 +- 异步与依赖注入示例路径 + - [tests/test_auth.py:25-40](file://tests/test_auth.py#L25-L40) + - [tests/test_auth.py:62-73](file://tests/test_auth.py#L62-L73) + - [tests/test_auth.py:88-104](file://tests/test_auth.py#L88-L104) + - [tests/conftest.py:28-62](file://tests/conftest.py#L28-L62) + - [backend/app/api/auth.py:13-43](file://backend/app/api/auth.py#L13-L43) + - [backend/app/services/auth.py:37-69](file://backend/app/services/auth.py#L37-L69) + +```mermaid +sequenceDiagram +participant T as "测试用例(test_auth.py)" +participant F as "夹具(conftest.py)" +participant AC as "AsyncClient" +participant R as "路由(auth.py)" +participant S as "服务(auth.py)" +participant DB as "数据库" +T->>F : 获取 mock_registered_user/override_get_current_user/auth_headers +T->>AC : POST /api/v1/auth/register +AC->>R : 路由处理器 +R->>S : register_user(...) +S->>DB : 查询/插入用户 +S-->>R : 返回用户对象 +R-->>AC : 201 + 用户信息 +AC-->>T : 断言状态码与字段 +``` + +图表来源 +- [tests/test_auth.py:25-40](file://tests/test_auth.py#L25-L40) +- [tests/conftest.py:28-62](file://tests/conftest.py#L28-L62) +- [backend/app/api/auth.py:13-19](file://backend/app/api/auth.py#L13-L19) +- [backend/app/services/auth.py:37-52](file://backend/app/services/auth.py#L37-L52) + +章节来源 +- [tests/test_auth.py:1-104](file://tests/test_auth.py#L1-L104) +- [tests/conftest.py:28-62](file://tests/conftest.py#L28-L62) +- [backend/app/api/auth.py:13-43](file://backend/app/api/auth.py#L13-L43) +- [backend/app/services/auth.py:37-69](file://backend/app/services/auth.py#L37-L69) + +### 引用引擎测试 +- 测试要点 + - BrandMatcher:精确匹配置信度 1.0、别名匹配置信度 0.9、模糊匹配阈值 >0.4、无匹配返回 False/None/0.0 + - CompetitorDetector:在文本中识别除目标品牌外的竞争品牌 + - 引用位置与上下文:按段落定位首次出现位置并截取上下文片段 +- 测试夹具 + - 直接构造 BrandMatcher/CompetitorDetector 实例,无需外部依赖 +- 断言策略 + - 字典键断言:cited、match_type、confidence、position、citation_text + - 列表断言:competitor_brands 包含预期品牌且不含目标品牌 +- 示例路径 + - [tests/test_citation_engine.py:6-37](file://tests/test_citation_engine.py#L6-L37) + - [tests/test_citation_engine.py:39-45](file://tests/test_citation_engine.py#L39-L45) + - [tests/test_citation_engine.py:47-54](file://tests/test_citation_engine.py#L47-L54) + - [backend/app/workers/citation_engine.py:19-120](file://backend/app/workers/citation_engine.py#L19-L120) + - [backend/app/workers/citation_engine.py:122-146](file://backend/app/workers/citation_engine.py#L122-L146) + +```mermaid +classDiagram +class BrandMatcher { ++match(text) dict +-_extract_candidates(text) list +-_extract_position_and_context(text, keyword) tuple +} +class CompetitorDetector { ++detect(text, target_brand) list +} +BrandMatcher --> CompetitorDetector : "在引擎中协作" +``` + +图表来源 +- [backend/app/workers/citation_engine.py:19-146](file://backend/app/workers/citation_engine.py#L19-L146) + +章节来源 +- [tests/test_citation_engine.py:1-54](file://tests/test_citation_engine.py#L1-L54) +- [backend/app/workers/citation_engine.py:19-146](file://backend/app/workers/citation_engine.py#L19-L146) + +### 查询处理测试 +- 测试要点 + - 创建查询:成功返回 201,响应包含关键词与目标品牌;超出配额抛出 403 + - 列出查询:返回 items 与 total + - 更新查询:返回更新后的关键词与频率 + - 删除查询:返回 204 + - 查询不存在/不属于他人:返回 404 +- 测试夹具 + - mock_query:模拟查询对象,包含 id、keyword、target_brand、platforms、frequency、status 等 + - override_get_current_user、auth_headers、async_client +- 断言策略 + - 状态码断言:201/200/204/403/404 + - 响应体字段断言:keyword、frequency、detail 等 +- 示例路径 + - [tests/test_queries.py:30-48](file://tests/test_queries.py#L30-L48) + - [tests/test_queries.py:51-71](file://tests/test_queries.py#L51-L71) + - [tests/test_queries.py:74-84](file://tests/test_queries.py#L74-L84) + - [tests/test_queries.py:87-113](file://tests/test_queries.py#L87-L113) + - [tests/test_queries.py:116-125](file://tests/test_queries.py#L116-L125) + - [tests/test_queries.py:128-139](file://tests/test_queries.py#L128-L139) + - [tests/test_queries.py:143-154](file://tests/test_queries.py#L143-L154) + +```mermaid +sequenceDiagram +participant T as "测试用例(test_queries.py)" +participant F as "夹具(conftest.py)" +participant AC as "AsyncClient" +participant R as "路由(queries.py)" +participant S as "服务层" +participant DB as "数据库" +T->>F : 获取 mock_query/override_get_current_user/auth_headers +T->>AC : POST /api/v1/queries/ +AC->>R : 路由处理器 +R->>S : create_query(...) +S->>DB : 插入查询记录 +S-->>R : 返回查询对象 +R-->>AC : 201 + 查询信息 +AC-->>T : 断言状态码与字段 +``` + +图表来源 +- [tests/test_queries.py:30-48](file://tests/test_queries.py#L30-L48) +- [backend/app/api/queries.py:26-39](file://backend/app/api/queries.py#L26-L39) + +章节来源 +- [tests/test_queries.py:1-154](file://tests/test_queries.py#L1-L154) +- [backend/app/api/queries.py:15-86](file://backend/app/api/queries.py#L15-L86) + +### 引用数据测试 +- 测试要点 + - 获取引用列表:返回 items 与 total,包含 platform 与 cited 等字段 + - 统计信息:返回总量、引用率、按平台分布与趋势 + - CSV 导出:返回 text/csv 内容类型与附件头,内容包含平台信息 +- 测试夹具 + - mock_citation_record:模拟引用记录对象 + - override_get_current_user、auth_headers、async_client +- 断言策略 + - 状态码断言:200 + - 响应体断言:total、items、by_platform、trend 等 + - 响应头断言:Content-Type 以 text/csv 开头、Content-Disposition 含 attachment +- 示例路径 + - [tests/test_citations.py:24-41](file://tests/test_citations.py#L24-L41) + - [tests/test_citations.py:44-73](file://tests/test_citations.py#L44-L73) + - [tests/test_citations.py:76-93](file://tests/test_citations.py#L76-L93) + +```mermaid +sequenceDiagram +participant T as "测试用例(test_citations.py)" +participant F as "夹具(conftest.py)" +participant AC as "AsyncClient" +participant R as "路由(citations.py)" +participant S as "服务层" +participant DB as "数据库" +T->>F : 获取 mock_citation_record/override_get_current_user/auth_headers +T->>AC : GET /api/v1/citations/stats +AC->>R : 路由处理器 +R->>S : get_citation_stats(...) +S->>DB : 聚合统计 +S-->>R : 返回统计结果 +R-->>AC : 200 + 统计信息 +AC-->>T : 断言字段与头 +``` + +图表来源 +- [tests/test_citations.py:44-73](file://tests/test_citations.py#L44-L73) +- [backend/app/api/citations.py:49-56](file://backend/app/api/citations.py#L49-L56) + +章节来源 +- [tests/test_citations.py:1-93](file://tests/test_citations.py#L1-L93) +- [backend/app/api/citations.py:25-78](file://backend/app/api/citations.py#L25-L78) + +### 测试夹具详解与最佳实践 +- mock_scheduler + - 作用:屏蔽后台调度器,避免真实任务启动影响测试稳定性 + - 使用:在会话级 autouse fixture 中替换 app.main.query_scheduler 的 start/shutdown + - 参考路径:[tests/conftest.py:19-26](file://tests/conftest.py#L19-L26) +- mock_user + - 作用:提供认证用户对象,包含 id、email、name、plan、max_queries、is_active 等 + - 使用:作为 override_get_current_user 的数据源 + - 参考路径:[tests/conftest.py:29-39](file://tests/conftest.py#L29-L39) +- override_get_current_user + - 作用:重写依赖 get_current_user,使路由自动解析到 mock_user + - 使用:在测试函数参数中注入,结束后清理依赖覆盖 + - 参考路径:[tests/conftest.py:42-50](file://tests/conftest.py#L42-L50) +- auth_token + - 作用:基于 mock_user 的 id 生成 JWT + - 使用:配合 auth_headers 进行认证请求 + - 参考路径:[tests/conftest.py:54-56](file://tests/conftest.py#L54-L56) +- auth_headers + - 作用:组装 Authorization: Bearer 请求头 + - 使用:GET/POST/PUT/DELETE 等请求统一携带 + - 参考路径:[tests/conftest.py:60-62](file://tests/conftest.py#L60-L62) +- async_client + - 作用:ASGI 异步 HTTP 客户端,用于端到端测试 + - 使用:with 上下文管理器确保生命周期内正确创建与释放 + - 参考路径:[tests/conftest.py:65-71](file://tests/conftest.py#L65-L71) + +最佳实践 +- 尽量使用 patch 替换服务层或外部依赖,避免真实网络或数据库调用 +- 对于需要认证的路由,优先使用 override_get_current_user 与 auth_headers +- 对于后台任务,使用 mock_scheduler 屏蔽真实调度器 +- 对于异步函数,使用 pytest.mark.asyncio 并通过 AsyncClient 发起请求 +- 清理:在依赖覆盖后及时 pop,避免影响其他测试 + +章节来源 +- [tests/conftest.py:19-71](file://tests/conftest.py#L19-L71) + +### 异步函数、依赖注入与错误处理测试示例 +- 异步函数 + - 使用 pytest.mark.asyncio 标记测试函数 + - 使用 ASGI AsyncClient 发起请求 + - 参考路径:[tests/test_auth.py:25-40](file://tests/test_auth.py#L25-L40)、[tests/test_queries.py:30-48](file://tests/test_queries.py#L30-L48) +- 依赖注入 + - 通过 app.dependency_overrides 临时替换 get_current_user + - 参考路径:[tests/conftest.py:42-50](file://tests/conftest.py#L42-L50)、[backend/app/api/deps.py:16-43](file://backend/app/api/deps.py#L16-L43) +- 错误处理 + - 通过 patch 抛出异常或返回 None,验证路由的 HTTP 异常与错误详情 + - 参考路径:[tests/test_auth.py:43-58](file://tests/test_auth.py#L43-L58)、[tests/test_queries.py:51-71](file://tests/test_queries.py#L51-L71) + +章节来源 +- [tests/test_auth.py:25-58](file://tests/test_auth.py#L25-L58) +- [tests/test_queries.py:51-71](file://tests/test_queries.py#L51-L71) +- [tests/conftest.py:42-50](file://tests/conftest.py#L42-L50) +- [backend/app/api/deps.py:16-43](file://backend/app/api/deps.py#L16-L43) + +### 测试数据准备与清理 +- 准备 + - 使用 AsyncMock 构造模型对象(用户、查询、引用记录),设置必要字段 + - 使用 patch 模拟服务层返回值或异常 + - 使用 override_get_current_user 提供认证上下文 +- 清理 + - 测试结束后 pop 依赖覆盖,避免污染其他测试 + - 使用 with 上下文管理 async_client 生命周期 +- 参考路径 + - [tests/conftest.py:42-50](file://tests/conftest.py#L42-L50)、[tests/conftest.py:65-71](file://tests/conftest.py#L65-L71) + - [tests/test_citations.py:8-21](file://tests/test_citations.py#L8-L21)、[tests/test_queries.py:10-27](file://tests/test_queries.py#L10-L27) + +章节来源 +- [tests/conftest.py:42-50](file://tests/conftest.py#L42-L50) +- [tests/conftest.py:65-71](file://tests/conftest.py#L65-L71) +- [tests/test_citations.py:8-21](file://tests/test_citations.py#L8-L21) +- [tests/test_queries.py:10-27](file://tests/test_queries.py#L10-L27) + +## 依赖分析 +- 测试对应用层的依赖 + - 测试通过 ASGI AsyncClient 直接调用 FastAPI 路由,路由再依赖 get_current_user 解析 JWT + - get_current_user 依赖 OAuth2PasswordBearer 与 verify_token,最终查询数据库 User 表 +- 测试对服务层的依赖 + - 通过 patch 替换服务层函数(如 register_user、authenticate_user、create_query 等) +- 测试对业务组件的依赖 + - 引擎测试直接构造 BrandMatcher/CompetitorDetector,不依赖外部服务 + +```mermaid +graph LR +T["测试用例"] --> C["conftest.py 夹具"] +C --> A["FastAPI 路由"] +A --> D["依赖注入(get_current_user)"] +D --> S["服务层"] +S --> DB["数据库"] +T --> E["引用引擎(直接类实例)"] +``` + +图表来源 +- [tests/conftest.py:28-71](file://tests/conftest.py#L28-L71) +- [backend/app/api/deps.py:16-43](file://backend/app/api/deps.py#L16-L43) +- [backend/app/api/auth.py:13-43](file://backend/app/api/auth.py#L13-L43) +- [backend/app/api/queries.py:26-39](file://backend/app/api/queries.py#L26-L39) +- [backend/app/workers/citation_engine.py:19-146](file://backend/app/workers/citation_engine.py#L19-L146) + +章节来源 +- [tests/conftest.py:28-71](file://tests/conftest.py#L28-L71) +- [backend/app/api/deps.py:16-43](file://backend/app/api/deps.py#L16-L43) +- [backend/app/api/auth.py:13-43](file://backend/app/api/auth.py#L13-L43) +- [backend/app/api/queries.py:26-39](file://backend/app/api/queries.py#L26-L39) +- [backend/app/workers/citation_engine.py:19-146](file://backend/app/workers/citation_engine.py#L19-L146) + +## 性能考虑 +- 使用 ASGI AsyncClient 避免真实网络往返,提升测试速度 +- 通过 patch 替换外部服务调用,减少 IO 与等待时间 +- 使用 mock_scheduler 屏蔽后台任务,避免并发与定时任务干扰 +- 对纯函数(引擎类)直接实例化断言,避免数据库与网络依赖 + +## 故障排查指南 +- 401 未认证 + - 检查是否正确注入 override_get_current_user 与 auth_headers + - 参考路径:[tests/test_auth.py:88-104](file://tests/test_auth.py#L88-L104)、[tests/conftest.py:42-62](file://tests/conftest.py#L42-L62) +- 403 超出配额 + - 检查服务层是否抛出 PermissionError,测试中通过 patch 触发 + - 参考路径:[tests/test_queries.py:51-71](file://tests/test_queries.py#L51-L71) +- 404 不存在/不属于他人 + - 检查 get_query 返回 None 的分支,确认依赖覆盖与参数 + - 参考路径:[tests/test_queries.py:128-139](file://tests/test_queries.py#L128-L139)、[tests/test_queries.py:143-154](file://tests/test_queries.py#L143-L154) +- CSV 导出问题 + - 检查 Content-Type 与 Content-Disposition 头,以及响应体是否包含平台信息 + - 参考路径:[tests/test_citations.py:76-93](file://tests/test_citations.py#L76-L93) + +章节来源 +- [tests/test_auth.py:88-104](file://tests/test_auth.py#L88-L104) +- [tests/test_queries.py:51-71](file://tests/test_queries.py#L51-L71) +- [tests/test_queries.py:128-154](file://tests/test_queries.py#L128-L154) +- [tests/test_citations.py:76-93](file://tests/test_citations.py#L76-L93) +- [tests/conftest.py:42-62](file://tests/conftest.py#L42-L62) + +## 结论 +本测试体系通过夹具与 patch 有效隔离外部依赖,结合 ASGI 异步客户端完成端到端验证。认证、查询、引用数据三大模块均覆盖成功与错误路径,引擎模块以纯函数形式保证可测试性与高内聚。建议持续补充边界条件与并发场景测试,逐步提升整体覆盖率。 + +## 附录 +- 测试覆盖率建议 + - 路由层:100%(含错误分支) + - 服务层:100%(含异常路径) + - 引擎类:100%(精确/别名/模糊/无匹配、位置与上下文) + - 数据模型:100%(字段与索引覆盖) +- 参考文件 + - [backend/app/models/user.py:1-41](file://backend/app/models/user.py#L1-L41) + - [backend/app/models/citation_record.py:1-42](file://backend/app/models/citation_record.py#L1-L42) + - [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) + - [backend/app/api/deps.py:1-43](file://backend/app/api/deps.py#L1-L43) + - [backend/app/services/auth.py:1-69](file://backend/app/services/auth.py#L1-L69) \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/测试策略/测试最佳实践.md b/.qoder/repowiki/zh/content/测试策略/测试最佳实践.md new file mode 100644 index 0000000..c4a5a3b --- /dev/null +++ b/.qoder/repowiki/zh/content/测试策略/测试最佳实践.md @@ -0,0 +1,384 @@ +# 测试最佳实践 + + +**本文档引用的文件** +- [tests/conftest.py](file://tests/conftest.py) +- [tests/test_auth.py](file://tests/test_auth.py) +- [tests/test_citations.py](file://tests/test_citations.py) +- [tests/test_queries.py](file://tests/test_queries.py) +- [tests/test_citation_engine.py](file://tests/test_citation_engine.py) +- [backend/requirements.txt](file://backend/requirements.txt) +- [docker-compose.yml](file://docker-compose.yml) +- [backend/app/main.py](file://backend/app/main.py) +- [backend/app/api/deps.py](file://backend/app/api/deps.py) +- [backend/app/workers/citation_engine.py](file://backend/app/workers/citation_engine.py) +- [backend/app/workers/scheduler.py](file://backend/app/workers/scheduler.py) +- [backend/alembic.ini](file://backend/alembic.ini) + + +## 目录 +1. [简介](#简介) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖分析](#依赖分析) +7. [性能考虑](#性能考虑) +8. [故障排查指南](#故障排查指南) +9. [结论](#结论) +10. [附录](#附录) + +## 简介 +本指南面向GEO项目的测试工作,系统性地给出测试编写规范、覆盖率要求与测量方法、持续集成配置建议、调试技巧以及测试环境与数据管理的最佳实践。文档以仓库现有测试代码为基础,结合后端FastAPI应用、数据库与任务调度器的实际实现,帮助团队建立一致、可维护、高覆盖度的测试体系。 + +## 项目结构 +测试相关的核心位置与职责如下: +- tests 目录:集中存放pytest测试用例与通用fixture配置 +- backend/app:后端应用入口、API路由、依赖注入、模型与服务 +- backend/app/workers:引用检测引擎与任务调度器 +- docker-compose.yml:本地开发与测试所需的数据库与缓存服务编排 + +```mermaid +graph TB +subgraph "测试层" +T1["tests/conftest.py"] +T2["tests/test_auth.py"] +T3["tests/test_citations.py"] +T4["tests/test_queries.py"] +T5["tests/test_citation_engine.py"] +end +subgraph "后端应用" +A1["app/main.py"] +A2["app/api/deps.py"] +W1["workers/citation_engine.py"] +W2["workers/scheduler.py"] +end +subgraph "基础设施" +D1["docker-compose.yml"] +DB["PostgreSQL"] +R["Redis"] +end +T1 --> A1 +T2 --> A1 +T3 --> A1 +T4 --> A1 +T5 --> W1 +A1 --> W2 +W1 --> DB +W2 --> DB +D1 --> DB +D1 --> R +``` + +图表来源 +- [tests/conftest.py:1-71](file://tests/conftest.py#L1-L71) +- [tests/test_auth.py:1-104](file://tests/test_auth.py#L1-L104) +- [tests/test_citations.py:1-93](file://tests/test_citations.py#L1-L93) +- [tests/test_queries.py:1-154](file://tests/test_queries.py#L1-L154) +- [tests/test_citation_engine.py:1-54](file://tests/test_citation_engine.py#L1-L54) +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/api/deps.py:1-43](file://backend/app/api/deps.py#L1-L43) +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) + +章节来源 +- [tests/conftest.py:1-71](file://tests/conftest.py#L1-L71) +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) + +## 核心组件 +- 测试夹具与环境 + - 会话级mock:屏蔽真实任务调度器,避免后台作业影响测试稳定性 + - 认证夹具:生成模拟用户、JWT令牌与请求头,统一处理鉴权依赖 + - 异步HTTP客户端:基于ASGI传输,便于对FastAPI路由进行端到端测试 +- API测试范围 + - 认证模块:注册、登录、个人信息读取 + - 查询模块:创建、列表、更新、删除、详情与权限边界 + - 引用数据模块:查询、统计、导出CSV +- 单元测试范围 + - 引用检测引擎:品牌匹配、竞争品牌识别、引用位置与置信度 +- 测试依赖 + - pytest、pytest-asyncio、httpx、aiosqlite、unittest.mock + +章节来源 +- [tests/conftest.py:19-71](file://tests/conftest.py#L19-L71) +- [tests/test_auth.py:25-104](file://tests/test_auth.py#L25-L104) +- [tests/test_queries.py:29-154](file://tests/test_queries.py#L29-L154) +- [tests/test_citations.py:23-93](file://tests/test_citations.py#L23-L93) +- [tests/test_citation_engine.py:6-54](file://tests/test_citation_engine.py#L6-L54) +- [backend/requirements.txt:31-35](file://backend/requirements.txt#L31-L35) + +## 架构总览 +下图展示测试执行路径与关键依赖交互,体现测试如何通过夹具注入、路由访问与服务mock实现可控的测试场景。 + +```mermaid +sequenceDiagram +participant Py as "pytest" +participant Fix as "conftest夹具" +participant AC as "AsyncClient" +participant APP as "FastAPI应用" +participant DEP as "依赖注入(get_current_user)" +participant SVC as "业务服务(被patch)" +participant DB as "数据库/缓存" +Py->>Fix : "加载会话级fixture" +Fix->>APP : "注入依赖覆盖(get_current_user)" +Py->>AC : "创建异步HTTP客户端" +AC->>APP : "发送API请求" +APP->>DEP : "解析JWT并解析当前用户" +DEP-->>APP : "返回模拟用户" +APP->>SVC : "调用业务逻辑(被patch)" +SVC-->>APP : "返回测试数据/异常" +APP-->>AC : "响应JSON/状态码" +AC-->>Py : "断言结果" +APP->>DB : "读写(测试中通常隔离)" +``` + +图表来源 +- [tests/conftest.py:19-71](file://tests/conftest.py#L19-L71) +- [tests/test_auth.py:25-104](file://tests/test_auth.py#L25-L104) +- [tests/test_queries.py:29-154](file://tests/test_queries.py#L29-L154) +- [tests/test_citations.py:23-93](file://tests/test_citations.py#L23-L93) +- [backend/app/api/deps.py:16-43](file://backend/app/api/deps.py#L16-L43) +- [backend/app/main.py:13-48](file://backend/app/main.py#L13-L48) + +## 详细组件分析 + +### 测试命名约定与结构 +- 文件命名 + - 使用 test_xxx.py,如 test_auth.py、test_queries.py、test_citations.py、test_citation_engine.py +- 函数命名 + - 使用 test_xxx_pattern,如 test_register_success、test_create_query_exceeds_limit +- 夹具命名 + - 使用 fixture_xxx 或 mock_xxx,如 mock_user、auth_headers、override_get_current_user +- 断言风格 + - 明确断言状态码、响应体字段与业务语义,如“包含特定错误信息”“返回CSV头” +- 注释标准 + - 每个fixture与关键测试函数添加简要说明,解释用途与边界条件 + +章节来源 +- [tests/test_auth.py:11-22](file://tests/test_auth.py#L11-L22) +- [tests/test_queries.py:10-26](file://tests/test_queries.py#L10-L26) +- [tests/test_citations.py:8-20](file://tests/test_citations.py#L8-L20) +- [tests/test_citation_engine.py:3-5](file://tests/test_citation_engine.py#L3-L5) + +### 认证与授权测试 +- 关键点 + - 使用依赖覆盖模拟当前用户,确保受保护路由在已认证与未认证两种场景下的行为 + - 通过patch替换服务层方法,验证成功与失败路径 +- 推荐断言 + - 成功:200/201响应、返回必要字段、token类型与用户信息 + - 失败:4xx状态码、错误消息包含预期关键字 +- 安全性 + - 确保未覆盖依赖时,未认证访问返回401 + +```mermaid +sequenceDiagram +participant C as "客户端" +participant A as "认证路由" +participant D as "get_current_user" +participant S as "认证服务(被patch)" +C->>A : "POST /api/v1/auth/login" +A->>D : "解析JWT并解析用户" +D-->>A : "返回模拟用户" +A->>S : "authenticate_user(...)" +S-->>A : "返回用户或None" +A-->>C : "200/401 + JSON" +``` + +图表来源 +- [tests/test_auth.py:62-84](file://tests/test_auth.py#L62-L84) +- [tests/conftest.py:42-50](file://tests/conftest.py#L42-L50) +- [backend/app/api/deps.py:16-43](file://backend/app/api/deps.py#L16-L43) + +章节来源 +- [tests/test_auth.py:25-104](file://tests/test_auth.py#L25-L104) +- [tests/conftest.py:28-62](file://tests/conftest.py#L28-L62) +- [backend/app/api/deps.py:16-43](file://backend/app/api/deps.py#L16-L43) + +### 查询管理测试 +- 关键点 + - 使用mock对象构造典型查询实体,覆盖创建、列表、更新、删除、不存在与越权访问 + - 通过patch触发权限限制与业务异常,验证403/404响应 +- 推荐断言 + - 字段一致性、分页总数、状态转换、权限边界 + +```mermaid +flowchart TD +Start(["开始: 发送请求"]) --> Patch["patch 业务方法"] +Patch --> CallAPI["调用 /queries/* 路由"] +CallAPI --> Resp{"响应状态码"} +Resp --> |201/200| AssertOK["断言返回字段与业务语义"] +Resp --> |403/404| AssertErr["断言错误消息与权限边界"] +AssertOK --> End(["结束"]) +AssertErr --> End +``` + +图表来源 +- [tests/test_queries.py:30-70](file://tests/test_queries.py#L30-L70) +- [tests/test_queries.py:127-139](file://tests/test_queries.py#L127-L139) + +章节来源 +- [tests/test_queries.py:29-154](file://tests/test_queries.py#L29-L154) + +### 引用数据与报告测试 +- 关键点 + - mock服务返回结构化数据,验证列表、统计聚合与CSV导出的响应头与内容 +- 推荐断言 + - 统计字段存在性与合理性、CSV内容片段包含关键字段 + +章节来源 +- [tests/test_citations.py:23-93](file://tests/test_citations.py#L23-L93) + +### 引用检测引擎单元测试 +- 关键点 + - 测试品牌匹配器的精确、别名、模糊匹配与无匹配场景,以及引用位置与置信度 + - 测试竞争品牌检测器对不同行业类别的识别 +- 推荐断言 + - 匹配结果布尔值、匹配类型、置信度范围、位置编号与上下文片段 + +```mermaid +classDiagram +class BrandMatcher { ++match(text) dict +-_extract_candidates(text) list +-_extract_position_and_context(text, keyword) tuple +} +class CompetitorDetector { ++detect(text, target_brand) list +} +BrandMatcher <.. CompetitorDetector : "组合使用" +``` + +图表来源 +- [tests/test_citation_engine.py:6-54](file://tests/test_citation_engine.py#L6-L54) +- [backend/app/workers/citation_engine.py:19-146](file://backend/app/workers/citation_engine.py#L19-L146) + +章节来源 +- [tests/test_citation_engine.py:1-54](file://tests/test_citation_engine.py#L1-L54) +- [backend/app/workers/citation_engine.py:19-146](file://backend/app/workers/citation_engine.py#L19-L146) + +## 依赖分析 +- 测试与应用的耦合 + - 通过依赖覆盖与patch解耦具体实现细节,提升测试稳定性 + - 会话级mock调度器避免真实任务执行,保证测试幂等性 +- 外部依赖 + - PostgreSQL与Redis通过docker-compose提供,测试可选择独立数据库或内存数据库(如aiosqlite)以加速 +- 日志与可观测性 + - 引擎与调度器使用标准日志,便于定位问题;Alembic日志级别较低,避免噪声 + +```mermaid +graph LR +Py["pytest"] --> CF["conftest.py"] +CF --> APP["app/main.py"] +APP --> SCH["workers/scheduler.py"] +SCH --> CE["workers/citation_engine.py"] +CE --> DB["PostgreSQL/Redis"] +``` + +图表来源 +- [tests/conftest.py:19-25](file://tests/conftest.py#L19-L25) +- [backend/app/main.py:10-21](file://backend/app/main.py#L10-L21) +- [backend/app/workers/scheduler.py:25-95](file://backend/app/workers/scheduler.py#L25-L95) +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) + +章节来源 +- [tests/conftest.py:19-25](file://tests/conftest.py#L19-L25) +- [backend/app/main.py:10-21](file://backend/app/main.py#L10-L21) +- [backend/app/workers/scheduler.py:25-95](file://backend/app/workers/scheduler.py#L25-L95) +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) +- [backend/alembic.ini:115-149](file://backend/alembic.ini#L115-L149) + +## 性能考虑 +- 测试速度 + - 使用会话级fixture减少重复初始化成本 + - 将外部依赖(数据库、缓存)置于容器中,避免每次重启 +- 并发与异步 + - 使用pytest-asyncio与AsyncClient,确保异步测试稳定 +- 覆盖率 + - 建议优先达到以下目标:行覆盖率≥80%、分支覆盖率≥70%、功能覆盖率≥90% + - 对关键路径(认证、查询、引用检测)进行重点覆盖 + +## 故障排查指南 +- 常见问题 + - 未覆盖依赖导致未认证访问失败:确认在测试中正确注入依赖覆盖 + - patch目标不匹配:核对被patch的服务方法签名与调用路径 + - 调度器干扰:确保会话级mock调度器生效 +- 调试技巧 + - 使用pytest调试选项:--pdb(进入调试器)、-v(详细输出)、-s(打印日志) + - 启用更详细的日志:在测试前设置日志级别,关注引擎与调度器日志 + - 分离数据库:使用独立测试数据库或内存数据库(如aiosqlite)以避免数据污染 +- 错误排查步骤 + - 逐个缩小patch范围,确认业务方法是否被正确替换 + - 检查JWT生成与解析流程,确保令牌有效 + - 校验路由前缀与路径参数,避免404 + +章节来源 +- [tests/conftest.py:19-25](file://tests/conftest.py#L19-L25) +- [tests/test_auth.py:88-104](file://tests/test_auth.py#L88-L104) +- [backend/app/api/deps.py:16-43](file://backend/app/api/deps.py#L16-L43) + +## 结论 +通过统一的命名与结构规范、完善的夹具与mock策略、清晰的断言与覆盖率目标,以及合理的CI/CD与调试实践,GEO项目可以构建高质量、可维护的测试体系。建议在现有基础上逐步扩展覆盖率,并引入自动化CI流水线以保障质量。 + +## 附录 + +### 测试覆盖率要求与测量方法 +- 行覆盖率(Line Coverage) + - 目标:≥80% + - 方法:使用覆盖率工具(如pytest-cov)生成报告,关注未覆盖的分支与异常路径 +- 分支覆盖率(Branch Coverage) + - 目标:≥70% + - 方法:针对if/else、异常处理与权限判断路径分别设计用例 +- 功能覆盖率(Functional Coverage) + - 目标:≥90% + - 方法:以API端点与核心算法(品牌匹配、竞争品牌检测)为功能域,确保每个功能域至少有一个正向与一个反向用例 + +### 持续集成配置建议 +- 触发条件 + - push到主分支与拉取请求 +- 步骤建议 + - 安装依赖(后端requirements.txt) + - 启动PostgreSQL与Redis(可复用docker-compose) + - 运行pytest并生成覆盖率报告 + - 上传覆盖率与测试报告 +- 参考文件 + - 后端依赖清单:[backend/requirements.txt:31-35](file://backend/requirements.txt#L31-L35) + - 服务编排:[docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) + +章节来源 +- [backend/requirements.txt:31-35](file://backend/requirements.txt#L31-L35) +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) + +### 测试环境管理 +- 数据库 + - 开发:使用docker-compose提供的PostgreSQL + - 测试:可选独立测试库或内存数据库(如aiosqlite),避免跨测试污染 +- 缓存 + - Redis用于任务调度与缓存,测试中可直接使用容器版本 +- 依赖注入 + - 通过conftest中的依赖覆盖,确保测试中始终使用mock用户与禁用真实调度器 + +章节来源 +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) +- [tests/conftest.py:19-50](file://tests/conftest.py#L19-L50) + +### 测试数据管理 +- 建议 + - 使用fixture生成稳定的UUID与时间戳,确保可重复性 + - 对于复杂对象(查询、引用记录),在fixture中集中定义字段默认值 + - 对于CSV导出等场景,使用固定字符串片段进行断言,避免动态内容导致脆弱断言 + +章节来源 +- [tests/test_queries.py:10-26](file://tests/test_queries.py#L10-L26) +- [tests/test_citations.py:8-20](file://tests/test_citations.py#L8-L20) +- [tests/test_citations.py:76-93](file://tests/test_citations.py#L76-L93) + +### 测试报告生成 +- 建议 + - 使用pytest-cov生成HTML或XML报告,便于CI集成 + - 在CI中保留报告产物,便于回溯历史趋势 +- 日志配置 + - Alembic日志级别已较低,可在测试中临时提高日志级别以辅助排查 + +章节来源 +- [backend/alembic.ini:115-149](file://backend/alembic.ini#L115-L149) \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/测试策略/测试策略.md b/.qoder/repowiki/zh/content/测试策略/测试策略.md new file mode 100644 index 0000000..101b7a3 --- /dev/null +++ b/.qoder/repowiki/zh/content/测试策略/测试策略.md @@ -0,0 +1,425 @@ +# 测试策略 + + +**本文引用的文件** +- [tests/conftest.py](file://tests/conftest.py) +- [tests/test_auth.py](file://tests/test_auth.py) +- [tests/test_citation_engine.py](file://tests/test_citation_engine.py) +- [tests/test_citations.py](file://tests/test_citations.py) +- [tests/test_queries.py](file://tests/test_queries.py) +- [backend/app/main.py](file://backend/app/main.py) +- [backend/app/api/deps.py](file://backend/app/api/deps.py) +- [backend/app/services/auth.py](file://backend/app/services/auth.py) +- [backend/app/workers/citation_engine.py](file://backend/app/workers/citation_engine.py) +- [backend/app/api/auth.py](file://backend/app/api/auth.py) +- [backend/app/api/citations.py](file://backend/app/api/citations.py) +- [backend/app/api/queries.py](file://backend/app/api/queries.py) +- [backend/app/database.py](file://backend/app/database.py) +- [backend/app/config.py](file://backend/app/config.py) + + +## 目录 +1. [引言](#引言) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖分析](#依赖分析) +7. [性能考虑](#性能考虑) +8. [故障排查指南](#故障排查指南) +9. [结论](#结论) +10. [附录](#附录) + +## 引言 +本测试策略文档面向GEO项目的Pytest测试体系,覆盖单元测试与集成测试的设计与实施要点。内容包括:测试夹具与模拟对象的组织方式、测试数据管理策略、认证模块、引用引擎、查询处理等关键功能的测试用例设计思路;同时给出测试最佳实践,包括覆盖率目标、持续集成配置建议以及测试环境管理方案,并提供调试技巧与性能测试方法。 + +## 项目结构 +测试目录位于仓库根目录下的tests,采用按功能模块划分的组织方式,配合Pytest的conftest集中式夹具与模拟对象,确保测试隔离与可重复性。后端应用以FastAPI为核心,API层通过依赖注入获取当前用户与数据库会话,服务层封装业务逻辑,工作器(worker)负责异步任务与平台适配。 + +```mermaid +graph TB +subgraph "测试层" +C["tests/conftest.py"] +TA["tests/test_auth.py"] +TQ["tests/test_queries.py"] +TC["tests/test_citations.py"] +TCE["tests/test_citation_engine.py"] +end +subgraph "后端应用" +M["backend/app/main.py"] +D["backend/app/api/deps.py"] +DB["backend/app/database.py"] +CFG["backend/app/config.py"] +AUTH_API["backend/app/api/auth.py"] +QUERIES_API["backend/app/api/queries.py"] +CITATIONS_API["backend/app/api/citations.py"] +CE["backend/app/workers/citation_engine.py"] +end +C --> TA +C --> TQ +C --> TC +C --> TCE +TA --> AUTH_API +TQ --> QUERIES_API +TC --> CITATIONS_API +TCE --> CE +AUTH_API --> D +QUERIES_API --> D +CITATIONS_API --> D +D --> DB +DB --> CFG +M --> AUTH_API +M --> QUERIES_API +M --> CITATIONS_API +``` + +图表来源 +- [tests/conftest.py:1-71](file://tests/conftest.py#L1-L71) +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/api/deps.py:1-43](file://backend/app/api/deps.py#L1-L43) +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/app/api/queries.py:1-86](file://backend/app/api/queries.py#L1-L86) +- [backend/app/api/citations.py:1-78](file://backend/app/api/citations.py#L1-L78) +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) + +章节来源 +- [tests/conftest.py:1-71](file://tests/conftest.py#L1-L71) +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) + +## 核心组件 +- 测试夹具与模拟 + - 会话级调度器模拟:在测试启动时替换查询调度器,避免真实后台任务影响测试稳定性。 + - 用户与令牌:提供模拟用户对象、JWT访问令牌及请求头,便于认证相关接口测试。 + - 异步HTTP客户端:基于ASGI传输创建异步HTTP客户端,用于端到端API测试。 + - 依赖覆盖:通过依赖注入覆盖当前用户解析逻辑,简化认证流程。 +- 测试数据管理 + - 使用pytest fixture生成模拟模型对象(如查询、引用记录),保证测试数据一致性与可读性。 + - 通过patch对服务层函数进行桩替,隔离外部依赖,提升测试确定性。 +- 测试运行与并发 + - 使用pytest-asyncio标记异步测试,确保事件循环正确初始化与清理。 + +章节来源 +- [tests/conftest.py:19-71](file://tests/conftest.py#L19-L71) + +## 架构总览 +下图展示了测试与被测系统的交互关系:测试通过异步HTTP客户端直接调用FastAPI路由,路由依赖当前用户与数据库会话,服务层完成业务逻辑,工作器负责平台查询与品牌匹配。 + +```mermaid +sequenceDiagram +participant T as "测试用例" +participant AC as "异步HTTP客户端" +participant APP as "FastAPI应用" +participant R_AUTH as "认证路由" +participant R_QUERIES as "查询路由" +participant R_CIT as "引用路由" +participant DEPS as "依赖注入(get_current_user)" +participant SVC as "服务层" +participant DB as "数据库" +T->>AC : 发起HTTP请求 +AC->>APP : ASGI传输调用 +APP->>DEPS : 解析当前用户 +DEPS->>DB : 查询用户 +DB-->>DEPS : 返回用户 +APP->>R_AUTH : 认证相关端点 +APP->>R_QUERIES : 查询相关端点 +APP->>R_CIT : 引用相关端点 +R_AUTH->>SVC : 调用服务 +R_QUERIES->>SVC : 调用服务 +R_CIT->>SVC : 调用服务 +SVC->>DB : 数据持久化/查询 +DB-->>SVC : 返回结果 +SVC-->>APP : 返回响应 +APP-->>AC : 序列化响应 +AC-->>T : 断言结果 +``` + +图表来源 +- [tests/conftest.py:65-71](file://tests/conftest.py#L65-L71) +- [backend/app/main.py:38-42](file://backend/app/main.py#L38-L42) +- [backend/app/api/deps.py:16-43](file://backend/app/api/deps.py#L16-L43) +- [backend/app/api/auth.py:13-43](file://backend/app/api/auth.py#L13-L43) +- [backend/app/api/queries.py:15-86](file://backend/app/api/queries.py#L15-L86) +- [backend/app/api/citations.py:25-78](file://backend/app/api/citations.py#L25-L78) +- [backend/app/database.py:23-29](file://backend/app/database.py#L23-L29) + +## 详细组件分析 + +### 认证模块测试策略 +- 测试目标 + - 注册成功与重复邮箱错误处理 + - 登录成功与密码错误场景 + - 当前用户信息获取(已认证与未认证) +- 关键测试点 + - 使用patch对注册与登录服务函数进行桩替,控制返回值与异常 + - 通过依赖覆盖与令牌头验证路由鉴权中间件行为 +- 用例设计要点 + - 成功路径断言状态码与响应体字段 + - 失败路径断言HTTP状态码与错误详情 + - 未认证路径断言401并检查依赖覆盖清理 + +```mermaid +sequenceDiagram +participant T as "测试用例" +participant AC as "异步HTTP客户端" +participant AUTH as "认证路由" +participant SVC as "认证服务" +participant DEPS as "依赖注入(get_current_user)" +T->>AC : POST /api/v1/auth/register +AC->>AUTH : 路由处理 +AUTH->>SVC : 注册服务 +SVC-->>AUTH : 返回用户或抛出异常 +AUTH-->>AC : 201/400 +AC-->>T : 断言 +T->>AC : POST /api/v1/auth/login +AC->>AUTH : 路由处理 +AUTH->>SVC : 登录服务 +SVC-->>AUTH : 返回用户或None +AUTH-->>AC : 200/401 +AC-->>T : 断言 +T->>AC : GET /api/v1/auth/me +AC->>AUTH : 路由处理 +AUTH->>DEPS : 解析当前用户 +DEPS-->>AUTH : 返回用户或抛出401 +AUTH-->>AC : 200/401 +AC-->>T : 断言 +``` + +图表来源 +- [tests/test_auth.py:25-104](file://tests/test_auth.py#L25-L104) +- [backend/app/api/auth.py:13-43](file://backend/app/api/auth.py#L13-L43) +- [backend/app/services/auth.py:37-69](file://backend/app/services/auth.py#L37-L69) +- [backend/app/api/deps.py:16-43](file://backend/app/api/deps.py#L16-L43) + +章节来源 +- [tests/test_auth.py:1-104](file://tests/test_auth.py#L1-L104) +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/app/services/auth.py:1-69](file://backend/app/services/auth.py#L1-L69) +- [backend/app/api/deps.py:1-43](file://backend/app/api/deps.py#L1-L43) + +### 引擎与查询处理测试策略 +- 测试目标 + - 品牌匹配器:精确、别名、模糊匹配与无匹配场景 + - 竞争品牌检测器:在文本中识别除目标品牌外的竞争品牌 + - 引擎执行流程:单平台查询、任务状态更新、记录创建与异常处理 +- 关键测试点 + - 单元测试直接构造BrandMatcher与CompetitorDetector,断言匹配结果与置信度、位置等字段 + - 集成测试通过patch平台适配器返回AI响应,验证引擎整合后的综合输出 +- 用例设计要点 + - 文本分段与位置提取:验证引用出现在第几段及上下文截取 + - 平台适配器替换:确保引擎执行单平台流程时能捕获异常并写入失败记录 + +```mermaid +classDiagram +class BrandMatcher { ++match(text) dict +-_extract_candidates(text) list +-_extract_position_and_context(text, keyword) tuple +} +class CompetitorDetector { ++detect(text, target_brand) list +} +class CitationEngine { ++execute_query(query, db) list ++execute_single_platform(keyword, platform, target_brand, brand_aliases) dict +-_get_or_create_task(db, query_id, platform) QueryTask +-_calculate_next_query_at(frequency) datetime ++close() void +} +BrandMatcher <.. CitationEngine : "使用" +CompetitorDetector <.. CitationEngine : "使用" +``` + +图表来源 +- [backend/app/workers/citation_engine.py:19-120](file://backend/app/workers/citation_engine.py#L19-L120) +- [backend/app/workers/citation_engine.py:122-146](file://backend/app/workers/citation_engine.py#L122-L146) +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) + +章节来源 +- [tests/test_citation_engine.py:1-54](file://tests/test_citation_engine.py#L1-L54) +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) + +### 引用数据与报告测试策略 +- 测试目标 + - 引用列表查询与统计聚合 + - 立即运行查询任务 + - CSV导出格式与响应头校验 +- 关键测试点 + - 通过patch服务函数返回预设数据,断言分页、总数与字段 + - 校验CSV内容类型、附件头与关键字段存在性 +- 用例设计要点 + - 统计聚合断言整体指标与按平台细分 + - 导出接口断言响应头与正文内容 + +```mermaid +sequenceDiagram +participant T as "测试用例" +participant AC as "异步HTTP客户端" +participant CIT as "引用路由" +participant REP as "报告路由" +participant SVC as "服务层" +T->>AC : GET /api/v1/citations/?skip=&limit= +AC->>CIT : 路由处理 +CIT->>SVC : 获取引用列表 +SVC-->>CIT : 返回(items,total) +CIT-->>AC : 200 + JSON +AC-->>T : 断言 +T->>AC : GET /api/v1/citations/stats +AC->>CIT : 路由处理 +CIT->>SVC : 获取统计 +SVC-->>CIT : 返回统计聚合 +CIT-->>AC : 200 + JSON +AC-->>T : 断言 +T->>AC : GET /api/v1/reports/export/csv?query_id= +AC->>REP : 路由处理 +REP->>SVC : 导出CSV +SVC-->>REP : 返回CSV字符串 +REP-->>AC : 200 + text/csv + attachment +AC-->>T : 断言 +``` + +图表来源 +- [tests/test_citations.py:23-93](file://tests/test_citations.py#L23-L93) +- [backend/app/api/citations.py:25-78](file://backend/app/api/citations.py#L25-L78) + +章节来源 +- [tests/test_citations.py:1-93](file://tests/test_citations.py#L1-L93) +- [backend/app/api/citations.py:1-78](file://backend/app/api/citations.py#L1-L78) + +### 查询管理测试策略 +- 测试目标 + - 创建查询(含配额限制场景) + - 列表查询与分页 + - 更新查询(字段变更) + - 删除查询 + - 查询不存在与跨用户访问控制 +- 关键测试点 + - 通过patch服务函数返回模拟查询对象或抛出权限异常 + - 断言HTTP状态码与响应体字段 +- 用例设计要点 + - 权限异常断言403与错误详情 + - 跨用户访问断言404(模拟查询归属判定) + +```mermaid +sequenceDiagram +participant T as "测试用例" +participant AC as "异步HTTP客户端" +participant Q as "查询路由" +participant SVC as "服务层" +T->>AC : POST /api/v1/queries/ +AC->>Q : 路由处理 +Q->>SVC : 创建查询 +SVC-->>Q : 返回查询或抛出权限异常 +Q-->>AC : 201/403 +AC-->>T : 断言 +T->>AC : GET /api/v1/queries/?skip=&limit= +AC->>Q : 路由处理 +Q->>SVC : 获取查询列表 +SVC-->>Q : 返回(items,total) +Q-->>AC : 200 + JSON +AC-->>T : 断言 +T->>AC : PUT /api/v1/queries/{id} +AC->>Q : 路由处理 +Q->>SVC : 更新查询 +SVC-->>Q : 返回更新后的查询或None +Q-->>AC : 200/404 +AC-->>T : 断言 +T->>AC : DELETE /api/v1/queries/{id} +AC->>Q : 路由处理 +Q->>SVC : 删除查询 +SVC-->>Q : 返回True/False +Q-->>AC : 204/404 +AC-->>T : 断言 +``` + +图表来源 +- [tests/test_queries.py:29-154](file://tests/test_queries.py#L29-L154) +- [backend/app/api/queries.py:15-86](file://backend/app/api/queries.py#L15-L86) + +章节来源 +- [tests/test_queries.py:1-154](file://tests/test_queries.py#L1-L154) +- [backend/app/api/queries.py:1-86](file://backend/app/api/queries.py#L1-L86) + +## 依赖分析 +- 测试与被测模块耦合 + - 测试通过ASGI传输直接调用路由,避免引入额外适配层 + - 通过依赖覆盖与patch解耦服务层与数据库、第三方平台 +- 外部依赖与集成点 + - 数据库:通过异步引擎与会话管理,测试中可使用内存数据库或独立测试库 + - JWT:通过服务层令牌生成与校验,测试中直接构造令牌头 + - 平台适配器:通过patch替换,避免真实网络请求 +- 循环依赖与风险 + - 当前结构清晰,无明显循环依赖;注意在测试中避免对真实调度器的依赖 + +```mermaid +graph LR +T_AUTH["测试: 认证"] --> A_AUTH["路由: 认证"] +T_QUERIES["测试: 查询"] --> A_QUERIES["路由: 查询"] +T_CIT["测试: 引用"] --> A_CIT["路由: 引用"] +A_AUTH --> S_AUTH["服务: 认证"] +A_QUERIES --> S_QUERY["服务: 查询"] +A_CIT --> S_CIT["服务: 引用"] +S_AUTH --> DB["数据库"] +S_QUERY --> DB +S_CIT --> DB +DB --> CFG["配置"] +``` + +图表来源 +- [tests/test_auth.py:1-104](file://tests/test_auth.py#L1-L104) +- [tests/test_queries.py:1-154](file://tests/test_queries.py#L1-L154) +- [tests/test_citations.py:1-93](file://tests/test_citations.py#L1-L93) +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/app/api/queries.py:1-86](file://backend/app/api/queries.py#L1-L86) +- [backend/app/api/citations.py:1-78](file://backend/app/api/citations.py#L1-L78) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) + +章节来源 +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) + +## 性能考虑 +- 测试并发与资源 + - 使用pytest-asyncio并行执行异步测试,减少总耗时 + - 通过会话级调度器模拟避免真实后台任务带来的不稳定因素 +- 数据库与缓存 + - 建议使用独立测试数据库实例,避免与开发/生产数据冲突 + - 对高频查询场景,可在测试中模拟数据库延迟,评估路由与服务层的超时与重试策略 +- 接口响应与序列化 + - 对大列表与统计聚合接口,关注JSON序列化开销与分页参数边界 +- 平台适配器性能 + - 通过patch模拟不同响应时延与错误率,评估引擎的容错与降级策略 + +## 故障排查指南 +- 常见问题定位 + - 认证失败:检查令牌生成与头设置、依赖覆盖是否生效 + - 404查询:确认查询ID与用户归属,检查服务层查询逻辑 + - 403配额:检查服务层权限异常抛出与HTTP状态映射 +- 调试技巧 + - 在conftest中临时打印依赖解析过程,定位get_current_user解析失败原因 + - 使用pytest的-v与-s选项查看详细输出,结合patch的side_effect观察异常传播 + - 对数据库相关测试,开启SQLAlchemy echo以查看生成的SQL +- 性能与稳定性 + - 对于长时间运行的异步测试,确保事件循环正确关闭 + - 对需要真实网络请求的场景,优先使用patch模拟,必要时增加超时与重试 + +## 结论 +本测试策略以Pytest为核心,结合会话级调度器模拟、依赖覆盖与patch技术,实现了对认证、查询、引用与引擎模块的全面覆盖。通过明确的夹具与测试数据管理,确保测试的可维护性与可重复性。建议在CI中启用并行执行与覆盖率统计,并为数据库与平台适配器建立稳定的模拟层,持续提升测试效率与质量。 + +## 附录 +- 测试覆盖率要求建议 + - 语句覆盖率:≥80% + - 分支覆盖率:≥70% + - 行覆盖率:≥80% + - 函数/方法覆盖率:≥90% +- 持续集成配置建议 + - 使用GitHub Actions或GitLab CI,包含Python版本矩阵、依赖安装、数据库准备、pytest执行与覆盖率上传 + - 将测试与lint、类型检查并行,确保主干分支质量 +- 测试环境管理 + - 使用独立测试数据库与Redis实例,避免污染 + - 通过环境变量切换测试配置,确保敏感信息不泄露 +- 性能测试方法 + - 使用pytest-benchmark或locust对高频路由进行基准测试 + - 对引擎执行流程进行压力测试,评估平台适配器与数据库写入瓶颈 \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/测试策略/集成测试.md b/.qoder/repowiki/zh/content/测试策略/集成测试.md new file mode 100644 index 0000000..d419459 --- /dev/null +++ b/.qoder/repowiki/zh/content/测试策略/集成测试.md @@ -0,0 +1,365 @@ +# 集成测试 + + +**本文引用的文件** +- [tests/conftest.py](file://tests/conftest.py) +- [backend/app/main.py](file://backend/app/main.py) +- [backend/app/database.py](file://backend/app/database.py) +- [backend/app/config.py](file://backend/app/config.py) +- [docker-compose.yml](file://docker-compose.yml) +- [tests/test_auth.py](file://tests/test_auth.py) +- [tests/test_queries.py](file://tests/test_queries.py) +- [tests/test_citations.py](file://tests/test_citations.py) +- [backend/app/api/deps.py](file://backend/app/api/deps.py) +- [backend/app/api/auth.py](file://backend/app/api/auth.py) +- [backend/app/api/queries.py](file://backend/app/api/queries.py) +- [backend/app/api/citations.py](file://backend/app/api/citations.py) +- [backend/app/workers/scheduler.py](file://backend/app/workers/scheduler.py) +- [backend/app/services/auth.py](file://backend/app/services/auth.py) + + +## 目录 +1. [简介](#简介) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖分析](#依赖分析) +7. [性能考虑](#性能考虑) +8. [故障排查指南](#故障排查指南) +9. [结论](#结论) +10. [附录](#附录) + +## 简介 +本文件面向GEO项目的集成测试,系统性阐述端到端测试的实现方法与最佳实践,覆盖以下方面: +- FastAPI应用测试:通过ASGI传输层在内存中启动应用,使用异步HTTP客户端进行端到端请求验证。 +- 数据库连接测试:基于异步SQLAlchemy引擎与会话管理,确保依赖注入与数据库交互正确。 +- 外部服务集成测试:通过依赖注入覆盖与模拟(patch)实现对外部平台(如wenxin、kimi)的模拟调用。 +- 测试环境配置:测试数据库、异步客户端、依赖注入覆盖与后台调度器的处理策略。 +- 完整用户工作流:从认证到查询创建、执行到结果返回的端到端流程验证。 +- 数据库事务与测试数据隔离:通过依赖注入覆盖与会话生命周期管理保障隔离性。 +- 性能与负载测试:提供可扩展的测试方法与建议。 + +## 项目结构 +GEO采用前后端分离架构,后端使用FastAPI + SQLAlchemy异步ORM,测试位于tests目录,通过pytest与pytest-asyncio驱动异步测试。数据库与缓存服务通过Docker Compose编排,便于本地与CI环境一致化。 + +```mermaid +graph TB +subgraph "测试环境" +TC["测试客户端
AsyncClient(ASGI)"] +CFG["测试配置
Settings(.env)"] +DC["Docker Compose
db, redis, backend, frontend"] +end +subgraph "后端" +APP["FastAPI 应用
app.main"] +DEPS["依赖注入
api.deps"] +AUTH["认证路由
api.auth"] +QUERIES["查询路由
api.queries"] +CITATIONS["引用路由
api.citations"] +SCHED["调度器
workers.scheduler"] +DB["数据库引擎/会话
database.py"] +end +DC --> APP +CFG --> APP +TC --> APP +APP --> AUTH +APP --> QUERIES +APP --> CITATIONS +APP --> SCHED +AUTH --> DB +QUERIES --> DB +CITATIONS --> DB +DEPS --> DB +``` + +图表来源 +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/api/deps.py:1-43](file://backend/app/api/deps.py#L1-L43) +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/app/api/queries.py:1-86](file://backend/app/api/queries.py#L1-L86) +- [backend/app/api/citations.py:1-78](file://backend/app/api/citations.py#L1-L78) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) + +章节来源 +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) + +## 核心组件 +- 异步HTTP客户端与ASGI传输层:通过ASGITransport在内存中启动FastAPI应用,避免真实网络开销,提升测试速度与稳定性。 +- 依赖注入覆盖:使用app.dependency_overrides在测试中替换真实依赖(如认证用户),以模拟不同业务场景。 +- 认证与令牌:通过create_access_token生成JWT,配合OAuth2PasswordBearer在路由中校验令牌。 +- 数据库引擎与会话:异步SQLAlchemy引擎与会话工厂,确保测试中数据库交互的可控性与隔离性。 +- 后台调度器:QueryScheduler负责周期性检查并触发查询任务,测试中通过mock屏蔽真实调度,防止后台任务干扰测试。 + +章节来源 +- [tests/conftest.py:19-71](file://tests/conftest.py#L19-L71) +- [backend/app/api/deps.py:16-43](file://backend/app/api/deps.py#L16-L43) +- [backend/app/services/auth.py:24-34](file://backend/app/services/auth.py#L24-L34) +- [backend/app/database.py:6-29](file://backend/app/database.py#L6-L29) +- [backend/app/workers/scheduler.py:25-95](file://backend/app/workers/scheduler.py#L25-L95) + +## 架构总览 +下图展示测试视角下的端到端调用链路,从异步客户端发起请求,经由FastAPI路由、依赖注入、服务层,最终访问数据库或外部平台(通过模拟)。 + +```mermaid +sequenceDiagram +participant Test as "测试客户端" +participant App as "FastAPI 应用" +participant Dep as "依赖注入
get_current_user" +participant Auth as "认证路由
/api/v1/auth/*" +participant Queries as "查询路由
/api/v1/queries/*" +participant Citations as "引用路由
/api/v1/citations/*" +participant DB as "数据库引擎/会话" +Test->>App : "POST /api/v1/auth/register" +App->>Dep : "解析令牌/校验用户" +App->>Auth : "注册逻辑" +Auth->>DB : "写入用户" +DB-->>Auth : "提交成功" +Auth-->>Test : "201 用户信息" +Test->>App : "POST /api/v1/auth/login" +App->>Auth : "登录校验" +Auth-->>Test : "200 {access_token}" +Test->>App : "POST /api/v1/queries/" +App->>Dep : "校验令牌并解析用户" +App->>Queries : "创建查询" +Queries->>DB : "插入查询记录" +DB-->>Queries : "提交成功" +Queries-->>Test : "201 查询详情" +Test->>App : "GET /api/v1/citations/?query_id=..." +App->>Dep : "校验令牌并解析用户" +App->>Citations : "查询引用数据" +Citations->>DB : "读取引用记录" +DB-->>Citations : "返回数据" +Citations-->>Test : "200 引用列表" +``` + +图表来源 +- [tests/test_auth.py:25-104](file://tests/test_auth.py#L25-L104) +- [tests/test_queries.py:29-154](file://tests/test_queries.py#L29-L154) +- [tests/test_citations.py:23-93](file://tests/test_citations.py#L23-L93) +- [backend/app/api/auth.py:13-43](file://backend/app/api/auth.py#L13-L43) +- [backend/app/api/queries.py:15-86](file://backend/app/api/queries.py#L15-L86) +- [backend/app/api/citations.py:25-78](file://backend/app/api/citations.py#L25-L78) +- [backend/app/api/deps.py:16-43](file://backend/app/api/deps.py#L16-L43) +- [backend/app/database.py:23-29](file://backend/app/database.py#L23-L29) + +## 详细组件分析 + +### 测试环境与配置 +- 测试数据库与外部服务:通过Docker Compose启动PostgreSQL与Redis,确保测试与生产环境一致;数据库URL与Redis URL在配置类中定义,便于在测试中覆盖。 +- 异步客户端:使用ASGITransport在内存中启动FastAPI应用,避免网络抖动对测试的影响。 +- 依赖注入覆盖:在conftest中定义override_get_current_user,使所有需要认证的路由在测试中自动获得模拟用户。 +- 调度器屏蔽:通过mock_scheduler在测试期间禁用真实调度器,防止后台任务影响测试稳定性。 + +章节来源 +- [docker-compose.yml:4-51](file://docker-compose.yml#L4-L51) +- [backend/app/config.py:7-14](file://backend/app/config.py#L7-L14) +- [tests/conftest.py:19-71](file://tests/conftest.py#L19-L71) + +### 认证与用户工作流测试 +- 注册与登录:通过patch模拟注册与登录服务,断言状态码与响应字段,确保认证流程正确。 +- 当前用户信息:在依赖注入覆盖下,调用/me接口返回当前用户信息。 +- 未认证访问:移除依赖注入覆盖后,访问受保护路由应返回401。 + +```mermaid +sequenceDiagram +participant C as "测试客户端" +participant A as "认证路由" +participant S as "认证服务" +participant D as "数据库" +C->>A : "POST /api/v1/auth/register" +A->>S : "register_user()" +S->>D : "查询邮箱是否存在" +S->>D : "插入新用户" +D-->>S : "提交成功" +S-->>A : "返回用户" +A-->>C : "201 Created" +C->>A : "POST /api/v1/auth/login" +A->>S : "authenticate_user()" +S->>D : "查询用户并校验密码" +D-->>S : "返回用户" +S-->>A : "生成JWT" +A-->>C : "200 {access_token}" +``` + +图表来源 +- [tests/test_auth.py:25-104](file://tests/test_auth.py#L25-L104) +- [backend/app/api/auth.py:13-43](file://backend/app/api/auth.py#L13-L43) +- [backend/app/services/auth.py:37-69](file://backend/app/services/auth.py#L37-L69) + +章节来源 +- [tests/test_auth.py:25-104](file://tests/test_auth.py#L25-L104) +- [backend/app/api/auth.py:13-43](file://backend/app/api/auth.py#L13-L43) +- [backend/app/services/auth.py:37-69](file://backend/app/services/auth.py#L37-L69) + +### 查询管理端到端测试 +- 创建查询:在依赖注入覆盖与令牌头下,调用创建接口,断言返回字段与状态码。 +- 列表与详情:通过patch模拟服务层返回值,断言分页与字段一致性。 +- 更新与删除:断言更新后的字段与删除后的状态码。 +- 权限限制:当超过配额时,断言403错误与错误信息。 +- 资源不存在:断言404错误与错误信息。 + +```mermaid +sequenceDiagram +participant C as "测试客户端" +participant Q as "查询路由" +participant S as "查询服务" +participant D as "数据库" +C->>Q : "POST /api/v1/queries/" +Q->>S : "create_query()" +S->>D : "插入查询记录" +D-->>S : "提交成功" +S-->>Q : "返回查询" +Q-->>C : "201 Created" +C->>Q : "GET /api/v1/queries/?skip=&limit=" +Q->>S : "get_queries()" +S->>D : "查询记录" +D-->>S : "返回列表" +S-->>Q : "返回分页" +Q-->>C : "200 OK" +C->>Q : "PUT /api/v1/queries/{id}" +Q->>S : "update_query()" +S->>D : "更新记录" +D-->>S : "提交成功" +S-->>Q : "返回更新后记录" +Q-->>C : "200 OK" +C->>Q : "DELETE /api/v1/queries/{id}" +Q->>S : "delete_query()" +S->>D : "删除记录" +D-->>S : "提交成功" +S-->>Q : "返回True" +Q-->>C : "204 No Content" +``` + +图表来源 +- [tests/test_queries.py:29-154](file://tests/test_queries.py#L29-L154) +- [backend/app/api/queries.py:15-86](file://backend/app/api/queries.py#L15-L86) + +章节来源 +- [tests/test_queries.py:29-154](file://tests/test_queries.py#L29-L154) +- [backend/app/api/queries.py:15-86](file://backend/app/api/queries.py#L15-L86) + +### 引用数据与统计导出测试 +- 引用列表:断言分页、字段与过滤参数生效。 +- 统计信息:断言总量、引用率、按平台分布与趋势数据。 +- CSV导出:断言内容类型、附件头与CSV内容片段。 + +```mermaid +sequenceDiagram +participant C as "测试客户端" +participant R as "引用路由" +participant RS as "引用服务" +participant D as "数据库" +C->>R : "GET /api/v1/citations/?query_id=&platform=&start_date=&end_date=&skip=&limit=" +R->>RS : "get_citations()" +RS->>D : "查询引用记录" +D-->>RS : "返回列表" +RS-->>R : "返回分页" +R-->>C : "200 OK" +C->>R : "GET /api/v1/citations/stats?query_id=" +R->>RS : "get_citation_stats()" +RS->>D : "聚合统计" +D-->>RS : "返回统计" +RS-->>R : "返回统计" +R-->>C : "200 OK" +C->>R : "GET /api/v1/reports/export/csv?query_id=" +R->>RS : "export_citations_csv()" +RS-->>R : "返回CSV内容" +R-->>C : "200 OK, text/csv, attachment" +``` + +图表来源 +- [tests/test_citations.py:23-93](file://tests/test_citations.py#L23-L93) +- [backend/app/api/citations.py:25-78](file://backend/app/api/citations.py#L25-L78) + +章节来源 +- [tests/test_citations.py:23-93](file://tests/test_citations.py#L23-L93) +- [backend/app/api/citations.py:25-78](file://backend/app/api/citations.py#L25-L78) + +### 数据库事务管理与测试数据隔离 +- 会话生命周期:get_db提供异步会话生成与关闭,确保每次请求在独立会话中执行,避免跨请求污染。 +- 依赖注入覆盖:通过app.dependency_overrides在测试中替换get_current_user,保证路由依赖始终可用且可控。 +- 事务隔离:测试中通过patch模拟服务层,不直接写入真实数据;若需真实写入,应在测试事务中回滚或使用独立测试库。 + +```mermaid +flowchart TD +Start(["测试开始"]) --> Override["依赖注入覆盖
get_current_user"] +Override --> Session["获取异步会话
get_db()"] +Session --> Exec["执行路由/服务逻辑"] +Exec --> Commit{"提交/回滚?"} +Commit --> |提交| Close["关闭会话"] +Commit --> |回滚| Close +Close --> End(["测试结束"]) +``` + +图表来源 +- [tests/conftest.py:42-50](file://tests/conftest.py#L42-L50) +- [backend/app/database.py:23-29](file://backend/app/database.py#L23-L29) + +章节来源 +- [tests/conftest.py:42-50](file://tests/conftest.py#L42-L50) +- [backend/app/database.py:23-29](file://backend/app/database.py#L23-L29) + +### 外部服务集成测试 +- 平台模拟:通过patch将平台调用替换为模拟实现,断言触发查询任务与返回的任务信息。 +- 调度器屏蔽:在测试期间mock调度器,避免真实后台任务执行,确保测试确定性。 + +章节来源 +- [tests/test_citations.py:75-93](file://tests/test_citations.py#L75-L93) +- [tests/conftest.py:19-26](file://tests/conftest.py#L19-L26) + +## 依赖分析 +- 应用生命周期:FastAPI应用在lifespan中启动调度器,并在关闭时优雅shutdown。 +- 路由依赖:认证与查询/引用路由均依赖get_current_user与get_db,形成清晰的依赖链。 +- 服务层:认证、查询、引用服务分别封装业务逻辑,便于在测试中通过patch进行替换。 + +```mermaid +graph LR +APP["FastAPI 应用"] --> LIFE["lifespan 启停调度器"] +APP --> AUTH["认证路由"] +APP --> QUERIES["查询路由"] +APP --> CITATIONS["引用路由"] +AUTH --> DEPS["get_current_user"] +QUERIES --> DEPS +CITATIONS --> DEPS +DEPS --> DB["get_db/AsyncSession"] +``` + +图表来源 +- [backend/app/main.py:13-22](file://backend/app/main.py#L13-L22) +- [backend/app/api/deps.py:16-43](file://backend/app/api/deps.py#L16-L43) +- [backend/app/database.py:23-29](file://backend/app/database.py#L23-L29) + +章节来源 +- [backend/app/main.py:13-22](file://backend/app/main.py#L13-L22) +- [backend/app/api/deps.py:16-43](file://backend/app/api/deps.py#L16-L43) +- [backend/app/database.py:23-29](file://backend/app/database.py#L23-L29) + +## 性能考虑 +- 内存测试:使用ASGI传输层与异步客户端,避免网络开销,提升测试吞吐量。 +- 调度器控制:在测试中禁用真实调度器,减少后台任务对测试性能的影响。 +- 数据库隔离:通过独立测试库或事务回滚策略,避免并发测试导致的数据竞争。 +- 负载测试建议:可在现有测试基础上扩展压力测试脚本,结合异步客户端批量发送请求,监控响应时间与错误率;注意在测试环境中使用独立数据库实例以避免影响生产数据。 + +## 故障排查指南 +- 401未认证:确认测试中是否正确设置依赖注入覆盖与Authorization头。 +- 403权限不足:检查配额限制逻辑与用户计划,确保测试数据符合预期。 +- 404资源不存在:确认查询ID与用户归属,确保服务层返回None时路由抛出404。 +- 调度器干扰:若出现不可预期的后台任务行为,确认是否启用了mock_scheduler。 + +章节来源 +- [tests/test_auth.py:88-104](file://tests/test_auth.py#L88-L104) +- [tests/test_queries.py:50-71](file://tests/test_queries.py#L50-L71) +- [tests/test_queries.py:127-154](file://tests/test_queries.py#L127-L154) +- [tests/conftest.py:19-26](file://tests/conftest.py#L19-L26) + +## 结论 +本文档提供了GEO项目集成测试的完整方法论与实施细节,涵盖FastAPI应用测试、数据库连接、外部服务模拟、端到端用户工作流验证、事务与隔离策略以及性能与负载测试建议。通过依赖注入覆盖与异步客户端,测试具备高可靠性与可维护性;借助Docker Compose与配置类,测试环境与生产环境保持一致,便于持续集成与部署。 + +## 附录 +- 建议在CI中增加: + - 数据库迁移与初始化脚本的集成测试。 + - Redis健康检查与连接超时重试策略验证。 + - 前后端联调测试,确保API与前端交互稳定。 \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/部署与运维/Docker容器化部署.md b/.qoder/repowiki/zh/content/部署与运维/Docker容器化部署.md new file mode 100644 index 0000000..f77f51f --- /dev/null +++ b/.qoder/repowiki/zh/content/部署与运维/Docker容器化部署.md @@ -0,0 +1,685 @@ +# Docker容器化部署 + + +**本文档引用的文件** +- [docker-compose.yml](file://docker-compose.yml) +- [backend/Dockerfile](file://backend/Dockerfile) +- [frontend/Dockerfile](file://frontend/Dockerfile) +- [backend/requirements.txt](file://backend/requirements.txt) +- [frontend/package.json](file://frontend/package.json) +- [backend/app/config.py](file://backend/app/config.py) +- [backend/app/main.py](file://backend/app/main.py) +- [backend/app/database.py](file://backend/app/database.py) +- [backend/app/workers/scheduler.py](file://backend/app/workers/scheduler.py) +- [backend/alembic.ini](file://backend/alembic.ini) +- [frontend/next.config.mjs](file://frontend/next.config.mjs) + + +## 目录 +1. [简介](#简介) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构概览](#架构概览) +5. [详细组件分析](#详细组件分析) +6. [依赖关系分析](#依赖关系分析) +7. [性能考虑](#性能考虑) +8. [故障排查指南](#故障排查指南) +9. [结论](#结论) +10. [附录](#附录) + +## 简介 + +GEO项目是一个基于FastAPI和Next.js的引文检索平台,采用Docker容器化部署方案。该部署方案提供了完整的微服务架构,包括PostgreSQL数据库、Redis缓存、后端API服务和前端Web应用。 + +本项目的核心目标是通过容器化技术实现开发环境的一致性和生产环境的可移植性,同时确保各服务之间的可靠通信和数据持久化。 + +## 项目结构 + +项目采用多容器架构,包含四个主要服务: + +```mermaid +graph TB +subgraph "Docker Compose编排" +DC[docker-compose.yml] +subgraph "数据库层" +DB[PostgreSQL 15] +RD[Redis 7] +end +subgraph "应用层" +BE[FastAPI 后端] +FE[Next.js 前端] +end +subgraph "存储卷" +PV[postgres_data] +RV[redis_data] +end +end +DC --> DB +DC --> RD +DC --> BE +DC --> FE +DB --> PV +RD --> RV +``` + +**图表来源** +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) + +**章节来源** +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) + +## 核心组件 + +### 数据库服务 (PostgreSQL) + +数据库服务使用PostgreSQL 15-alpine镜像,配置了完整的环境变量和健康检查机制: + +- **镜像版本**: postgres:15-alpine +- **容器名称**: geo_db +- **端口映射**: 5432:5432 +- **数据持久化**: 使用postgres_data命名卷 +- **环境变量**: 用户名、密码、数据库名 +- **健康检查**: 每5秒检查一次数据库连接 + +### 缓存服务 (Redis) + +Redis服务提供高性能的键值存储和任务队列功能: + +- **镜像版本**: redis:7-alpine +- **容器名称**: geo_redis +- **端口映射**: 6379:6379 +- **数据持久化**: 使用redis_data命名卷 +- **健康检查**: PING命令测试 + +### 后端服务 (FastAPI) + +后端服务基于Python 3.11-slim,提供RESTful API接口: + +- **基础镜像**: python:3.11-slim +- **容器名称**: geo_backend +- **端口映射**: 8000:8000 +- **开发模式**: 支持代码热重载 +- **依赖安装**: Playwright浏览器支持 + +### 前端服务 (Next.js) + +前端服务提供现代化的用户界面: + +- **基础镜像**: node:20-alpine +- **容器名称**: geo_frontend +- **端口映射**: 3000:3000 +- **开发模式**: Next.js开发服务器 +- **依赖管理**: npm包管理器 + +**章节来源** +- [docker-compose.yml:3-71](file://docker-compose.yml#L3-L71) + +## 架构概览 + +系统采用分层架构设计,确保服务间的松耦合和高内聚: + +```mermaid +graph TB +subgraph "客户端层" +Browser[Web浏览器] +Mobile[移动应用] +end +subgraph "API网关层" +Nginx[Nginx反向代理] +end +subgraph "应用服务层" +subgraph "后端服务" +Auth[认证服务] +Query[查询服务] +Citation[引文服务] +Report[报告服务] +end +subgraph "任务处理层" +Scheduler[调度器] +Worker[工作进程] +end +end +subgraph "数据存储层" +subgraph "数据库" +PostgreSQL[PostgreSQL] +Redis[Redis缓存] +end +subgraph "文件存储" +MinIO[S3兼容存储] +end +end +Browser --> Nginx +Mobile --> Nginx +Nginx --> Auth +Nginx --> Query +Nginx --> Citation +Nginx --> Report +Auth --> PostgreSQL +Query --> PostgreSQL +Citation --> PostgreSQL +Report --> PostgreSQL +Query --> Redis +Citation --> Redis +Report --> Redis +Scheduler --> PostgreSQL +Scheduler --> Redis +Worker --> PostgreSQL +Worker --> Redis +``` + +**图表来源** +- [backend/app/main.py:24-48](file://backend/app/main.py#L24-L48) +- [backend/app/config.py:7-8](file://backend/app/config.py#L7-L8) + +## 详细组件分析 + +### Docker Compose配置详解 + +#### 服务依赖关系 + +```mermaid +sequenceDiagram +participant DB as "PostgreSQL" +participant Redis as "Redis" +participant Backend as "FastAPI" +participant Frontend as "Next.js" +Note over DB,Redis : 服务启动顺序 +DB->>DB : 初始化数据库 +Redis->>Redis : 启动缓存服务 +Backend->>DB : 等待数据库就绪 +Backend->>Redis : 等待缓存就绪 +Backend->>Backend : 启动Uvicorn服务 +Frontend->>Backend : 发起API请求 +Frontend->>Frontend : 启动开发服务器 +``` + +**图表来源** +- [docker-compose.yml:46-66](file://docker-compose.yml#L46-L66) + +#### 网络配置 + +Docker Compose自动创建隔离的网络环境,各服务通过服务名进行内部通信: + +- **内部DNS**: 服务间通过服务名访问 +- **端口映射**: 开发环境暴露必要端口 +- **卷挂载**: 数据持久化和代码热重载 + +#### 数据卷配置 + +```mermaid +graph LR +subgraph "宿主机" +Host[宿主机文件系统] +end +subgraph "Docker卷" +PV[postgres_data] +RV[redis_data] +FV[node_modules] +end +subgraph "容器内" +ContainerDB[数据库数据] +ContainerRedis[Redis数据] +ContainerFE[前端模块] +end +Host --> PV +Host --> RV +Host --> FV +PV --> ContainerDB +RV --> ContainerRedis +FV --> ContainerFE +``` + +**图表来源** +- [docker-compose.yml:14-15](file://docker-compose.yml#L14-L15) +- [docker-compose.yml:28-29](file://docker-compose.yml#L28-L29) +- [docker-compose.yml:61-63](file://docker-compose.yml#L61-L63) + +**章节来源** +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) + +### 后端Dockerfile构建流程 + +后端服务采用多阶段构建策略,优化镜像大小和构建效率: + +```mermaid +flowchart TD +Start([开始构建]) --> BaseImage["基础镜像
python:3.11-slim"] +BaseImage --> InstallDeps["安装系统依赖
apt-get更新"] +InstallDeps --> CopyReq["复制依赖文件
requirements.txt"] +CopyReq --> InstallPy["安装Python依赖
pip安装"] +InstallPy --> InstallPW["安装Playwright
chromium浏览器"] +InstallPW --> CopyCode["复制应用代码"] +CopyCode --> ExposePort["暴露端口
8000"] +ExposePort --> CMD["启动命令
uvicorn"] +CMD --> End([构建完成]) +InstallDeps -.-> SysDeps["系统依赖:
curl, wget, libglib2.0-0,
libnss3, libatk1.0-0,
libcairo2, libasound2"] +InstallPy -.-> PyDeps["Python依赖:
FastAPI, SQLAlchemy,
Redis, APScheduler,
Playwright"] +``` + +**图表来源** +- [backend/Dockerfile:1-41](file://backend/Dockerfile#L1-L41) +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) + +#### 关键构建步骤 + +1. **系统依赖安装**: 安装Playwright运行所需的系统库 +2. **Python环境配置**: 设置工作目录和依赖管理 +3. **应用代码部署**: 复制业务逻辑代码 +4. **运行时优化**: 配置Uvicorn ASGI服务器 + +**章节来源** +- [backend/Dockerfile:1-41](file://backend/Dockerfile#L1-L41) +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) + +### 前端Dockerfile构建流程 + +前端服务构建流程相对简洁,专注于开发环境优化: + +```mermaid +flowchart TD +Start([开始构建]) --> BaseImage["基础镜像
node:20-alpine"] +BaseImage --> WorkDir["设置工作目录
/app"] +WorkDir --> CopyPkg["复制包文件
package.json, lock.json"] +CopyPkg --> InstallDeps["安装依赖
npm ci"] +InstallDeps --> CopyCode["复制应用代码"] +CopyCode --> ExposePort["暴露端口
3000"] +ExposePort --> CMD["启动命令
npm run dev"] +CMD --> End([构建完成]) +``` + +**图表来源** +- [frontend/Dockerfile:1-15](file://frontend/Dockerfile#L1-L15) +- [frontend/package.json:1-40](file://frontend/package.json#L1-L40) + +#### 依赖管理策略 + +- **生产依赖**: Next.js 14.2.35,React 18 +- **开发依赖**: TypeScript,ESLint,Tailwind CSS +- **UI组件**: Radix UI,Lucide React图标库 + +**章节来源** +- [frontend/Dockerfile:1-15](file://frontend/Dockerfile#L1-L15) +- [frontend/package.json:1-40](file://frontend/package.json#L1-L40) + +### 环境变量配置 + +系统使用统一的环境变量配置机制,支持不同环境的灵活切换: + +```mermaid +classDiagram +class Settings { ++string DATABASE_URL ++string REDIS_URL ++string JWT_SECRET ++int JWT_EXPIRE_HOURS ++string PLAYWRIGHT_BROWSERS_PATH ++string ZHIPU_API_KEY ++string TONGYI_API_KEY +} +class ConfigFile { ++string env_file ++string extra +} +class Environment { ++Development ++Production ++Testing +} +Settings --> ConfigFile : "读取配置" +Settings --> Environment : "根据环境变化" +``` + +**图表来源** +- [backend/app/config.py:4-16](file://backend/app/config.py#L4-L16) + +#### 核心配置参数 + +| 参数名称 | 默认值 | 用途描述 | +|---------|--------|----------| +| DATABASE_URL | postgresql+asyncpg://postgres:postgres123@db:5432/geo_platform | 数据库连接字符串 | +| REDIS_URL | redis://redis:6379/0 | Redis缓存连接地址 | +| JWT_SECRET | your-secret-key-change-in-production | JWT令牌密钥 | +| JWT_EXPIRE_HOURS | 24 | JWT过期时间(小时) | +| PLAYWRIGHT_BROWSERS_PATH | /ms-playwright | Playwright浏览器路径 | + +**章节来源** +- [backend/app/config.py:7-13](file://backend/app/config.py#L7-L13) + +### 健康检查机制 + +系统实现了多层次的健康检查,确保服务可用性: + +```mermaid +graph TB +subgraph "健康检查层次" +HC[健康检查总览] +subgraph "数据库健康" +DBHC[PostgreSQL健康] +DBTest["pg_isready -U postgres -d geo_platform"] +end +subgraph "缓存健康" +RHC[Redis健康] +RTest["redis-cli ping"] +end +subgraph "应用健康" +BHC[后端健康] +BTest["HTTP GET /health"] +FHC[前端健康] +FTest["HTTP GET /"] +end +end +HC --> DBHC +HC --> RHC +HC --> BHC +HC --> FHC +DBHC --> DBTest +RHC --> RTest +BHC --> BTest +FHC --> FTest +``` + +**图表来源** +- [docker-compose.yml:16-20](file://docker-compose.yml#L16-L20) +- [docker-compose.yml:30-34](file://docker-compose.yml#L30-L34) +- [backend/app/main.py:45-47](file://backend/app/main.py#L45-L47) + +#### 健康检查配置 + +- **检查间隔**: 5秒 +- **超时时间**: 5秒 +- **重试次数**: 5次 +- **检查方式**: + - PostgreSQL: 数据库连接测试 + - Redis: PING命令响应 + - 应用: HTTP端点响应 + +**章节来源** +- [docker-compose.yml:16-34](file://docker-compose.yml#L16-L34) +- [backend/app/main.py:45-47](file://backend/app/main.py#L45-L47) + +## 依赖关系分析 + +### 服务间依赖图 + +```mermaid +graph TD +subgraph "外部依赖" +PG[PostgreSQL] +RD[Redis] +PW[Playwright] +end +subgraph "后端服务" +FA[FastAPI] +SQ[SQLAlchemy] +AP[APScheduler] +RE[Redis Client] +end +subgraph "前端服务" +NX[Next.js] +RC[React] +TW[Tailwind CSS] +end +subgraph "开发工具" +UV[Uvicorn] +NP[npm] +PY[Python] +end +FA --> SQ +FA --> AP +FA --> RE +FA --> PW +NX --> RC +NX --> TW +FA --> UV +NX --> NP +FA --> PY +``` + +**图表来源** +- [backend/requirements.txt:2-35](file://backend/requirements.txt#L2-L35) +- [frontend/package.json:11-38](file://frontend/package.json#L11-L38) + +### 数据库连接分析 + +后端服务通过异步数据库连接池与PostgreSQL交互: + +```mermaid +sequenceDiagram +participant App as "FastAPI应用" +participant Engine as "SQLAlchemy引擎" +participant Pool as "连接池" +participant DB as "PostgreSQL" +App->>Engine : 创建异步引擎 +Engine->>Pool : 初始化连接池 +Pool->>DB : 建立数据库连接 +DB-->>Pool : 连接确认 +Pool-->>Engine : 连接可用 +Engine-->>App : 引擎就绪 +App->>Engine : 获取数据库会话 +Engine->>Pool : 从池中获取连接 +Pool->>DB : 执行SQL查询 +DB-->>Pool : 返回查询结果 +Pool-->>Engine : 释放连接 +Engine-->>App : 会话完成 +``` + +**图表来源** +- [backend/app/database.py:6-28](file://backend/app/database.py#L6-L28) +- [backend/app/config.py:7](file://backend/app/config.py#L7) + +**章节来源** +- [backend/requirements.txt:5-8](file://backend/requirements.txt#L5-L8) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) + +### 任务调度系统 + +系统实现了基于APScheduler的异步任务调度机制: + +```mermaid +flowchart TD +Start([应用启动]) --> InitScheduler["初始化调度器"] +InitScheduler --> AddJob["添加定时任务
check_queries"] +AddJob --> StartJobs["启动所有任务"] +StartJobs --> Timer["1小时定时器"] +Timer --> CheckQueries["检查到期查询"] +CheckQueries --> ExecuteQuery["执行查询任务"] +ExecuteQuery --> UpdateSchedule["更新下次执行时间"] +UpdateSchedule --> Timer +CheckQueries --> ErrorHandle["错误处理"] +ErrorHandle --> Continue["继续下一个查询"] +subgraph "查询执行流程" +ExecuteQuery --> GetPlatform["获取平台列表"] +GetPlatform --> ExecutePlatform["执行平台查询"] +ExecutePlatform --> CreateRecord["创建引文记录"] +CreateRecord --> UpdateQuery["更新查询状态"] +end +``` + +**图表来源** +- [backend/app/workers/scheduler.py:30-95](file://backend/app/workers/scheduler.py#L30-L95) + +**章节来源** +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) + +## 性能考虑 + +### 容器资源优化 + +1. **镜像大小控制**: 使用alpine基础镜像减少体积 +2. **多阶段构建**: 分离构建和运行时环境 +3. **依赖精简**: 只安装必要的系统和Python依赖 +4. **缓存利用**: npm和pip缓存提高构建速度 + +### 数据库性能优化 + +- **连接池管理**: SQLAlchemy异步连接池 +- **索引优化**: 查询表的关键字段建立索引 +- **查询优化**: 使用异步查询避免阻塞 +- **事务管理**: 合理的事务边界控制 + +### 缓存策略 + +- **Redis缓存**: 高速数据缓存 +- **浏览器缓存**: 前端静态资源缓存 +- **CDN加速**: 生产环境静态资源分发 + +## 故障排查指南 + +### 常见问题诊断 + +#### 1. 数据库连接问题 + +**症状**: 后端服务启动失败,显示数据库连接错误 + +**排查步骤**: +1. 检查PostgreSQL容器状态 +2. 验证数据库凭据配置 +3. 确认网络连通性 +4. 查看数据库日志 + +**解决方案**: +- 确保PostgreSQL服务先于后端启动 +- 检查DATABASE_URL格式正确性 +- 验证数据库用户权限 + +#### 2. 前端开发服务器问题 + +**症状**: 前端无法访问,页面加载失败 + +**排查步骤**: +1. 检查Node.js版本兼容性 +2. 验证npm依赖安装 +3. 确认端口占用情况 +4. 查看浏览器控制台错误 + +**解决方案**: +- 清理node_modules缓存 +- 重新安装npm依赖 +- 检查防火墙设置 + +#### 3. 任务调度异常 + +**症状**: 引文查询任务未按时执行 + +**排查步骤**: +1. 检查调度器状态 +2. 验证查询表数据 +3. 查看任务执行日志 +4. 确认Redis连接正常 + +**解决方案**: +- 重启调度器服务 +- 检查查询状态字段 +- 验证平台API密钥 + +### 日志监控 + +系统提供了多层级的日志输出: + +```mermaid +graph TB +subgraph "日志级别" +Debug[调试日志] +Info[信息日志] +Warn[警告日志] +Error[错误日志] +end +subgraph "日志来源" +DBLog[数据库日志] +TaskLog[任务日志] +AppLog[应用日志] +SysLog[系统日志] +end +Debug --> DBLog +Debug --> TaskLog +Debug --> AppLog +Debug --> SysLog +Info --> DBLog +Info --> TaskLog +Info --> AppLog +Info --> SysLog +Warn --> DBLog +Warn --> TaskLog +Warn --> AppLog +Warn --> SysLog +Error --> DBLog +Error --> TaskLog +Error --> AppLog +Error --> SysLog +``` + +**章节来源** +- [backend/app/workers/scheduler.py:22](file://backend/app/workers/scheduler.py#L22) +- [backend/app/main.py:45-47](file://backend/app/main.py#L45-L47) + +### 环境变量验证 + +建议在启动前验证关键环境变量: + +```bash +# 检查数据库连接 +docker exec geo_db pg_isready -U postgres -d geo_platform + +# 检查Redis连接 +docker exec geo_redis redis-cli ping + +# 检查后端API +curl http://localhost:8000/health + +# 检查前端应用 +curl http://localhost:3000 +``` + +## 结论 + +GEO项目的Docker容器化部署方案提供了完整的微服务架构实现,具有以下优势: + +1. **开发友好**: 支持代码热重载和快速迭代 +2. **环境一致**: 容器化确保开发和生产环境一致性 +3. **扩展性强**: 模块化设计便于功能扩展 +4. **运维简便**: 健康检查和日志监控提升可维护性 + +该部署方案为GEO平台的稳定运行和未来发展奠定了坚实的技术基础。 + +## 附录 + +### 完整部署流程 + +```mermaid +sequenceDiagram +participant Dev as "开发者" +participant Docker as "Docker引擎" +participant Compose as "Docker Compose" +participant Registry as "镜像仓库" +Dev->>Compose : docker-compose up -d +Compose->>Registry : 拉取基础镜像 +Registry-->>Compose : 返回镜像 +Compose->>Docker : 构建自定义镜像 +Docker-->>Compose : 镜像构建完成 +Compose->>Docker : 启动数据库容器 +Compose->>Docker : 启动缓存容器 +Compose->>Docker : 启动后端容器 +Compose->>Docker : 启动前端容器 +Docker-->>Dev : 服务启动完成 +``` + +### 最佳实践建议 + +1. **生产环境优化** + - 使用生产级数据库和缓存 + - 配置负载均衡和反向代理 + - 实施监控和告警系统 + +2. **安全加固** + - 使用HTTPS证书 + - 配置防火墙规则 + - 定期更新依赖包 + +3. **性能调优** + - 监控容器资源使用 + - 优化数据库查询 + - 实施缓存策略 + +**章节来源** +- [docker-compose.yml:36-66](file://docker-compose.yml#L36-L66) +- [backend/app/config.py:9](file://backend/app/config.py#L9) \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/部署与运维/生产环境部署.md b/.qoder/repowiki/zh/content/部署与运维/生产环境部署.md new file mode 100644 index 0000000..7e92faf --- /dev/null +++ b/.qoder/repowiki/zh/content/部署与运维/生产环境部署.md @@ -0,0 +1,347 @@ +# 生产环境部署 + + +**本文档引用的文件** +- [docker-compose.yml](file://docker-compose.yml) +- [backend/Dockerfile](file://backend/Dockerfile) +- [frontend/Dockerfile](file://frontend/Dockerfile) +- [backend/app/config.py](file://backend/app/config.py) +- [backend/app/main.py](file://backend/app/main.py) +- [backend/app/database.py](file://backend/app/database.py) +- [backend/requirements.txt](file://backend/requirements.txt) +- [backend/alembic/versions/488d0bd5ab01_initial_migration.py](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py) +- [backend/app/workers/citation_engine.py](file://backend/app/workers/citation_engine.py) +- [backend/app/workers/platforms/kimi.py](file://backend/app/workers/platforms/kimi.py) +- [backend/app/workers/platforms/wenxin.py](file://backend/app/workers/platforms/wenxin.py) +- [frontend/next.config.mjs](file://frontend/next.config.mjs) +- [frontend/package.json](file://frontend/package.json) +- [frontend/tailwind.config.ts](file://frontend/tailwind.config.ts) + + +## 目录 +1. [简介](#简介) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖关系分析](#依赖关系分析) +7. [性能考虑](#性能考虑) +8. [故障排查指南](#故障排查指南) +9. [结论](#结论) +10. [附录](#附录) + +## 简介 +本指南面向GEO项目的生产环境部署,覆盖以下关键主题: +- 部署架构:后端服务、数据库、缓存与前端容器编排 +- Nginx反向代理、SSL证书与负载均衡建议 +- 环境变量与安全配置:数据库、Redis、API密钥与JWT +- 性能优化:静态资源缓存、Gzip压缩与CDN集成 +- 安全加固:防火墙、访问控制与数据加密 +- 域名与DNS、HTTPS证书申请流程 +- 部署后验证与性能基准测试方法 + +## 项目结构 +GEO采用多容器编排,后端使用FastAPI,前端使用Next.js,数据库为PostgreSQL,缓存为Redis。开发阶段通过Compose进行联调,生产环境建议以Nginx作为统一入口,结合反向代理、SSL终止与负载均衡。 + +```mermaid +graph TB +subgraph "生产环境" +LB["负载均衡/反向代理
Nginx"] +subgraph "应用层" +FE["前端容器
Next.js"] +BE["后端容器
FastAPI(Uvicorn)"] +end +subgraph "数据层" +DB["数据库
PostgreSQL"] +RC["缓存/队列
Redis"] +end +end +LB --> FE +LB --> BE +BE --> DB +BE --> RC +``` + +图表来源 +- [docker-compose.yml:36-51](file://docker-compose.yml#L36-L51) +- [backend/Dockerfile:40-41](file://backend/Dockerfile#L40-L41) +- [frontend/Dockerfile:14-15](file://frontend/Dockerfile#L14-L15) + +章节来源 +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) +- [backend/Dockerfile:1-41](file://backend/Dockerfile#L1-L41) +- [frontend/Dockerfile:1-15](file://frontend/Dockerfile#L1-L15) + +## 核心组件 +- 后端服务(FastAPI + Uvicorn):提供REST API,包含认证、查询词、引用数据与报告接口;内置健康检查端点。 +- 数据库(PostgreSQL):异步SQLAlchemy引擎连接,支持迁移脚本。 +- 缓存(Redis):用于任务调度与会话/锁等场景。 +- 前端(Next.js):开发模式运行,生产建议构建并由Nginx提供静态资源与反代。 +- 引擎与平台适配器:基于Playwright的网页自动化,对接Kimi与文心一格平台。 + +章节来源 +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) +- [backend/app/workers/platforms/kimi.py:1-206](file://backend/app/workers/platforms/kimi.py#L1-L206) +- [backend/app/workers/platforms/wenxin.py:1-205](file://backend/app/workers/platforms/wenxin.py#L1-L205) +- [frontend/package.json:1-40](file://frontend/package.json#L1-L40) + +## 架构总览 +下图展示生产环境典型拓扑:Nginx作为入口,负责TLS终止、静态资源分发与反向代理;后端容器提供API;数据库与Redis分别承载持久化与缓存。 + +```mermaid +graph TB +Internet["互联网"] +Nginx["Nginx
SSL/TLS 终止
静态资源缓存"] +subgraph "应用服务" +API["FastAPI 应用"] +Worker["任务/调度器"] +end +subgraph "数据服务" +Postgres["PostgreSQL"] +Redis["Redis"] +end +Internet --> Nginx +Nginx --> API +Nginx --> |静态资源| Nginx +API --> Postgres +API --> Redis +Worker --> Redis +Worker --> Postgres +``` + +图表来源 +- [backend/app/main.py:24-47](file://backend/app/main.py#L24-L47) +- [backend/app/database.py:6-18](file://backend/app/database.py#L6-L18) +- [backend/app/config.py:7-8](file://backend/app/config.py#L7-L8) + +## 详细组件分析 + +### 后端服务(FastAPI) +- 应用生命周期:启动时初始化调度器,关闭时优雅停机。 +- CORS策略:开发默认允许本地前端源,生产需收紧为受信域名。 +- 路由模块:认证、查询词、引用数据、报告与即时执行路由。 +- 健康检查:/health端点返回状态。 + +```mermaid +sequenceDiagram +participant Client as "客户端" +participant Nginx as "Nginx" +participant API as "FastAPI 应用" +participant DB as "PostgreSQL" +participant Redis as "Redis" +Client->>Nginx : "HTTP 请求" +Nginx->>API : "反向代理转发" +API->>DB : "数据库查询" +DB-->>API : "结果" +API->>Redis : "缓存/任务交互" +Redis-->>API : "响应" +API-->>Nginx : "HTTP 响应" +Nginx-->>Client : "返回内容" +``` + +图表来源 +- [backend/app/main.py:13-21](file://backend/app/main.py#L13-L21) +- [backend/app/database.py:23-28](file://backend/app/database.py#L23-L28) +- [backend/app/config.py:7-8](file://backend/app/config.py#L7-L8) + +章节来源 +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) + +### 数据库与迁移 +- 异步引擎:使用异步SQLAlchemy与asyncpg。 +- 迁移:初始版本包含用户、查询、引用记录、任务与订阅表,含索引与外键约束。 + +```mermaid +erDiagram +USERS { +uuid id PK +string email UK +string password_hash +string name +string plan +integer max_queries +boolean is_active +timestamp created_at +timestamp updated_at +} +QUERIES { +uuid id PK +uuid user_id FK +string keyword +string target_brand +jsonb brand_aliases +jsonb platforms +string frequency +string status +timestamp last_queried_at +timestamp next_query_at +timestamp created_at +timestamp updated_at +} +CITATION_RECORDS { +uuid id PK +uuid query_id FK +string platform +boolean cited +integer citation_position +text citation_text +jsonb competitor_brands +text raw_response +timestamp queried_at +} +QUERY_TASKS { +uuid id PK +uuid query_id FK +string platform +string status +text error_message +timestamp scheduled_at +timestamp started_at +timestamp completed_at +} +SUBSCRIPTIONS { +uuid id PK +uuid user_id FK +string plan +string status +date start_date +date end_date +numeric amount +string payment_method +string payment_id +timestamp created_at +} +USERS ||--o{ QUERIES : "拥有" +QUERIES ||--o{ CITATION_RECORDS : "产生" +QUERIES ||--o{ QUERY_TASKS : "调度" +USERS ||--o{ SUBSCRIPTIONS : "订阅" +``` + +图表来源 +- [backend/alembic/versions/488d0bd5ab01_initial_migration.py:24-111](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L24-L111) + +章节来源 +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/alembic/versions/488d0bd5ab01_initial_migration.py:1-128](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L1-L128) + +### 引擎与平台适配器 +- CitationEngine:对指定平台发起查询,执行品牌匹配与竞争品牌检测,并记录结果。 +- 平台适配器:Kimi与文心一言,基于Playwright自动化,具备重试与稳定性等待逻辑。 +- 任务调度:查询任务状态管理,支持失败回退与占位记录。 + +```mermaid +flowchart TD +Start(["开始执行查询"]) --> InitMatcher["初始化品牌匹配器"] +InitMatcher --> IteratePlat["遍历平台列表"] +IteratePlat --> CreateTask["创建/获取任务记录"] +CreateTask --> SetRunning["标记任务为运行中"] +SetRunning --> DoQuery["平台查询Playwright"] +DoQuery --> BrandMatch["品牌匹配与竞争品牌检测"] +BrandMatch --> SaveRecord["保存引用记录"] +SaveRecord --> UpdateQueryTime["更新查询时间与下次查询时间"] +UpdateQueryTime --> NextPlat{"还有平台?"} +NextPlat --> |是| IteratePlat +NextPlat --> |否| End(["结束"]) +``` + +图表来源 +- [backend/app/workers/citation_engine.py:159-234](file://backend/app/workers/citation_engine.py#L159-L234) +- [backend/app/workers/platforms/kimi.py:33-48](file://backend/app/workers/platforms/kimi.py#L33-L48) +- [backend/app/workers/platforms/wenxin.py:33-48](file://backend/app/workers/platforms/wenxin.py#L33-L48) + +章节来源 +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) +- [backend/app/workers/platforms/kimi.py:1-206](file://backend/app/workers/platforms/kimi.py#L1-L206) +- [backend/app/workers/platforms/wenxin.py:1-205](file://backend/app/workers/platforms/wenxin.py#L1-L205) + +### 前端(Next.js) +- 开发模式:通过Dockerfile直接运行开发服务器。 +- 生产建议:构建产物由Nginx托管,配合缓存与压缩策略。 + +章节来源 +- [frontend/Dockerfile:1-15](file://frontend/Dockerfile#L1-L15) +- [frontend/package.json:5-9](file://frontend/package.json#L5-L9) +- [frontend/next.config.mjs:1-5](file://frontend/next.config.mjs#L1-L5) + +## 依赖关系分析 +- 后端依赖:FastAPI、Uvicorn、SQLAlchemy、asyncpg、Pydantic、Redis、APScheduler、Playwright、httpx、python-dotenv等。 +- 前端依赖:Next.js、Radix UI、Tailwind CSS、NextAuth等。 +- 容器编排:Compose定义数据库、Redis、后端与前端服务及其端口映射与健康检查。 + +```mermaid +graph LR +Backend["后端应用"] --> DB["PostgreSQL"] +Backend --> Redis["Redis"] +Backend --> Playwright["Playwright 浏览器"] +Frontend["前端应用"] --> Backend +``` + +图表来源 +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) +- [docker-compose.yml:4-51](file://docker-compose.yml#L4-L51) + +章节来源 +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) + +## 性能考虑 +- 静态资源缓存:Nginx启用长期缓存头,结合文件指纹命名策略,减少带宽与延迟。 +- Gzip/压缩:开启gzip或更高效的压缩算法(如Brotli),降低传输体积。 +- CDN集成:将静态资源(图片、JS/CSS)接入CDN,提升全球访问速度与可用性。 +- 数据库优化:合理索引(已有迁移脚本包含索引)、连接池参数调优、慢查询日志与监控。 +- 缓存策略:Redis用于热点数据与任务队列,避免重复计算与高并发下的数据库压力。 +- 前端构建:生产构建开启Tree Shaking、代码分割与懒加载,缩短首屏时间。 +- 反向代理:Nginx作为统一入口,可集中处理压缩、缓存与限流。 + +## 故障排查指南 +- 健康检查:通过/health端点确认后端存活;Compose中数据库与Redis具备健康检查命令。 +- 日志定位:查看Nginx错误日志、后端Uvicorn日志与容器日志;关注数据库连接与Redis连通性。 +- CORS问题:生产环境需将前端域名加入allow_origins白名单,避免跨域阻断。 +- 数据库迁移:确保迁移脚本成功执行,检查表结构与索引是否存在。 +- Playwright依赖:容器内需安装浏览器依赖,确保Playwright可正常启动浏览器实例。 +- API密钥:确认平台API密钥配置正确,避免请求失败导致任务异常。 + +章节来源 +- [backend/app/main.py:45-47](file://backend/app/main.py#L45-L47) +- [docker-compose.yml:16-20](file://docker-compose.yml#L16-L20) +- [docker-compose.yml:30-34](file://docker-compose.yml#L30-L34) +- [backend/app/config.py:12-13](file://backend/app/config.py#L12-L13) +- [backend/app/workers/platforms/kimi.py:21-32](file://backend/app/workers/platforms/kimi.py#L21-L32) +- [backend/app/workers/platforms/wenxin.py:21-32](file://backend/app/workers/platforms/wenxin.py#L21-L32) + +## 结论 +本指南提供了GEO生产环境部署的完整路径:从容器编排到Nginx反向代理与SSL、从环境变量与安全配置到性能优化与安全加固,并给出了部署后验证与基准测试建议。建议在正式上线前完成域名与DNS解析、证书申请与安装、以及完整的端到端测试。 + +## 附录 + +### 生产环境部署清单 +- 基础设施 + - 负载均衡/反向代理:Nginx(建议启用TLS终止、静态资源缓存与Gzip/Brotli) + - 应用容器:后端(FastAPI)、前端(Next.js生产构建) + - 数据库:PostgreSQL(主从/高可用视规模而定) + - 缓存:Redis(哨兵/集群视规模而定) +- 环境变量与安全 + - 数据库连接:DATABASE_URL(生产使用强密码与只读账号) + - Redis连接:REDIS_URL(网络隔离与ACL) + - JWT密钥:JWT_SECRET(高强度随机值,定期轮换) + - 平台API密钥:ZHIPU_API_KEY、TONGYI_API_KEY(最小权限与限额) +- 配置与优化 + - Nginx:静态资源缓存头、压缩、限流、健康检查探针 + - 后端:连接池大小、超时、日志级别、CORS白名单 + - 数据库:连接数上限、慢查询阈值、备份策略 + - 前端:构建产物缓存、CDN接入、预加载关键资源 +- 安全加固 + - 防火墙:仅开放必要端口(80/443/22等) + - 访问控制:IP白名单、速率限制、WAF + - 数据加密:传输加密(TLS)、静态加密(视合规要求) +- 域名与证书 + - DNS:A/AAAA记录指向负载均衡器;CNAME别名指向子域名 + - 证书:ACME自动签发或商业证书;多域名与SAN +- 部署后验证与基准测试 + - 功能验证:登录、查询、报告生成、引用检测 + - 性能基准:并发用户数、P95/P99延迟、吞吐量、资源占用 + - 可靠性:故障切换、恢复时间、数据一致性校验 \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/部署与运维/监控与日志管理.md b/.qoder/repowiki/zh/content/部署与运维/监控与日志管理.md new file mode 100644 index 0000000..bfc1b5b --- /dev/null +++ b/.qoder/repowiki/zh/content/部署与运维/监控与日志管理.md @@ -0,0 +1,276 @@ +# 监控与日志管理 + + +**本文引用的文件** +- [backend/app/main.py](file://backend/app/main.py) +- [backend/docker-compose.yml](file://backend/docker-compose.yml) +- [backend/app/config.py](file://backend/app/config.py) +- [backend/app/database.py](file://backend/app/database.py) +- [backend/app/workers/scheduler.py](file://backend/app/workers/scheduler.py) +- [backend/app/workers/citation_engine.py](file://backend/app/workers/citation_engine.py) +- [backend/app/workers/platforms/base.py](file://backend/app/workers/platforms/base.py) +- [backend/app/workers/platforms/kimi.py](file://backend/app/workers/platforms/kimi.py) +- [backend/app/workers/platforms/wenxin.py](file://backend/app/workers/platforms/wenxin.py) +- [backend/requirements.txt](file://backend/requirements.txt) +- [backend/Dockerfile](file://backend/Dockerfile) +- [backend/app/api/deps.py](file://backend/app/api/deps.py) + + +## 目录 +1. [简介](#简介) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖分析](#依赖分析) +7. [性能考虑](#性能考虑) +8. [故障排查指南](#故障排查指南) +9. [结论](#结论) +10. [附录](#附录) + +## 简介 +本方案围绕GEO项目的监控与日志管理,系统性地梳理应用健康检查、服务可用性监控、响应时间监控、错误率统计、日志收集与管理、错误追踪与告警通知、性能指标采集与可视化,以及日志分析与故障诊断的最佳实践。当前仓库中已具备基础的健康检查端点、容器编排与健康检查、日志记录与重试机制,但尚未集成统一的监控与告警体系(如Prometheus/Grafana)。本文在现有能力基础上,提出可落地的扩展建议与实施路径。 + +## 项目结构 +后端采用FastAPI框架,通过APScheduler进行定时任务调度,使用Playwright驱动浏览器访问外部AI平台,结合PostgreSQL与Redis作为数据与缓存存储。Docker Compose负责多服务编排与健康检查。 + +```mermaid +graph TB +subgraph "后端服务" +API["FastAPI 应用
/health 健康检查"] +SCH["APScheduler 调度器"] +CE["引用检测引擎"] +DB["PostgreSQL 引擎/会话"] +REDIS["Redis 连接"] +end +subgraph "外部平台" +KIMI["Kimi 适配器"] +WENXIN["文心一言适配器"] +end +API --> SCH +SCH --> CE +CE --> KIMI +CE --> WENXIN +CE --> DB +CE --> REDIS +``` + +图表来源 +- [backend/app/main.py:45-47](file://backend/app/main.py#L45-L47) +- [backend/app/workers/scheduler.py:30-40](file://backend/app/workers/scheduler.py#L30-L40) +- [backend/app/workers/citation_engine.py:148-170](file://backend/app/workers/citation_engine.py#L148-L170) +- [backend/app/database.py:6-18](file://backend/app/database.py#L6-L18) +- [backend/app/config.py:7-8](file://backend/app/config.py#L7-L8) + +章节来源 +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/docker-compose.yml:1-71](file://backend/docker-compose.yml#L1-L71) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) + +## 核心组件 +- 健康检查端点:提供基础可用性探测,便于反向代理与编排系统判断服务状态。 +- 定时任务调度器:基于APScheduler的异步调度器,周期性扫描并执行到期查询任务。 +- 引用检测引擎:封装品牌匹配、竞争品牌检测、平台适配器调用与结果持久化。 +- 平台适配器:Kimi与文心一言的Playwright驱动实现,包含重试与稳定性处理。 +- 数据库与配置:异步SQLAlchemy引擎与环境变量配置,支撑任务状态与结果存储。 +- 容器编排与健康检查:Compose对数据库与Redis进行健康检查,后端服务依赖健康状态启动。 + +章节来源 +- [backend/app/main.py:45-47](file://backend/app/main.py#L45-L47) +- [backend/app/workers/scheduler.py:25-95](file://backend/app/workers/scheduler.py#L25-L95) +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) +- [backend/app/workers/platforms/kimi.py:11-206](file://backend/app/workers/platforms/kimi.py#L11-L206) +- [backend/app/workers/platforms/wenxin.py:11-205](file://backend/app/workers/platforms/wenxin.py#L11-L205) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/docker-compose.yml:16-34](file://backend/docker-compose.yml#L16-L34) + +## 架构总览 +下图展示从API到调度器、引擎、平台适配器与数据库的整体交互流程,并标注健康检查与容器编排的关键节点。 + +```mermaid +sequenceDiagram +participant Client as "客户端" +participant API as "FastAPI 应用" +participant SCH as "调度器" +participant CE as "引用检测引擎" +participant KIMI as "Kimi 适配器" +participant WENXIN as "文心一言适配器" +participant DB as "数据库" +Client->>API : GET /health +API-->>Client : {"status" : "ok"} +SCH->>CE : 触发检查并执行查询 +CE->>KIMI : query(keyword) +CE->>WENXIN : query(keyword) +CE->>DB : 写入CitationRecord/更新QueryTask +CE-->>SCH : 返回执行结果 +``` + +图表来源 +- [backend/app/main.py:45-47](file://backend/app/main.py#L45-L47) +- [backend/app/workers/scheduler.py:51-84](file://backend/app/workers/scheduler.py#L51-L84) +- [backend/app/workers/citation_engine.py:159-234](file://backend/app/workers/citation_engine.py#L159-L234) +- [backend/app/workers/platforms/kimi.py:33-48](file://backend/app/workers/platforms/kimi.py#L33-L48) +- [backend/app/workers/platforms/wenxin.py:33-48](file://backend/app/workers/platforms/wenxin.py#L33-L48) + +## 详细组件分析 + +### 健康检查与服务可用性监控 +- 基础健康检查端点:提供轻量级可用性探测,适合反向代理与编排系统快速判断服务状态。 +- 容器健康检查:Compose对PostgreSQL与Redis进行健康检查,后端服务依赖这些服务健康后再启动,提升整体可用性保障。 +- 建议扩展: + - 在应用内增加更细粒度的依赖检查(数据库连接池、Redis连接、外部平台可用性)。 + - 将健康检查结果暴露为指标,接入Prometheus/Grafana进行可视化与告警。 + +章节来源 +- [backend/app/main.py:45-47](file://backend/app/main.py#L45-L47) +- [backend/docker-compose.yml:16-34](file://backend/docker-compose.yml#L16-L34) + +### 日志收集与管理策略 +- 日志记录范围:调度器、引擎、平台适配器均使用标准日志模块记录信息与错误,覆盖任务执行、平台查询、异常处理等关键环节。 +- 日志级别建议: + - INFO:任务开始/结束、平台查询成功、结果写入。 + - WARNING:重试警告、超时警告。 + - ERROR:平台查询失败、数据库写入失败、异常抛出。 +- 结构化日志格式建议: + - 统一字段:timestamp、level、service、module、function、message、trace_id(可选)、span_id(可选)、extra(JSON)。 + - 示例字段:service=backend、module=scheduler/engine/platform、function=check_and_execute_queries/execute_query/query。 +- 日志轮转与存储: + - 使用logrotate或容器日志驱动自带轮转;生产环境建议将日志输出到stdout/stderr,由容器编排系统集中收集(如Fluent Bit/Fluentd)。 + - 存储策略:短期本地、长期归档至对象存储或集中日志平台(如ELK/Graylog/Loki)。 + +章节来源 +- [backend/app/workers/scheduler.py:40-90](file://backend/app/workers/scheduler.py#L40-L90) +- [backend/app/workers/citation_engine.py:211-227](file://backend/app/workers/citation_engine.py#L211-L227) +- [backend/app/workers/platforms/kimi.py:28-48](file://backend/app/workers/platforms/kimi.py#L28-L48) +- [backend/app/workers/platforms/wenxin.py:28-48](file://backend/app/workers/platforms/wenxin.py#L28-L48) + +### 错误追踪机制 +- 异常捕获与堆栈跟踪: + - 调度器与引擎在关键路径捕获异常并记录详细错误信息,便于定位问题。 + - 平台适配器对Playwright操作进行超时与异常处理,并在多次重试后记录最终失败原因。 +- 告警通知: + - 建议在应用层或网关层集成告警通道(如邮件、Webhook、IM机器人),当ERROR/WARNING级别日志达到阈值时触发。 + - 可结合日志平台的规则引擎或Prometheus Alertmanager实现自动告警。 +- 诊断要点: + - 关注平台查询超时、浏览器启动失败、数据库事务提交失败等高频错误。 + - 为每次查询生成唯一trace_id,贯穿日志链路,便于跨服务串联分析。 + +章节来源 +- [backend/app/workers/scheduler.py:73-74](file://backend/app/workers/scheduler.py#L73-L74) +- [backend/app/workers/citation_engine.py:211-227](file://backend/app/workers/citation_engine.py#L211-L227) +- [backend/app/workers/platforms/kimi.py:116-119](file://backend/app/workers/platforms/kimi.py#L116-L119) +- [backend/app/workers/platforms/wenxin.py:114-117](file://backend/app/workers/platforms/wenxin.py#L114-L117) + +### 性能监控指标 +- CPU与内存:通过容器监控(如cAdvisor/Prometheus Node Exporter)采集后端容器的CPU/内存使用率。 +- 数据库连接数:从数据库侧查看连接数与慢查询,或通过中间件埋点导出指标。 +- API响应时间:在FastAPI中间件中统计请求耗时,按路由分组导出直方图与摘要指标。 +- 业务指标: + - 查询任务执行成功率、失败率、平均耗时。 + - 平台查询成功率、平均响应时间、重试次数。 + - CitationRecord写入速率、QueryTask状态转换时延。 + +章节来源 +- [backend/app/main.py:24-28](file://backend/app/main.py#L24-L28) +- [backend/app/workers/scheduler.py:51-84](file://backend/app/workers/scheduler.py#L51-L84) +- [backend/app/workers/citation_engine.py:159-234](file://backend/app/workers/citation_engine.py#L159-L234) + +### 监控工具选择与配置 +- Prometheus:抓取后端应用指标(自定义指标+Node Exporter),用于构建仪表盘与告警。 +- Grafana:可视化Prometheus数据,创建面板展示健康状态、性能趋势与告警历史。 +- 日志平台(可选):Loki配合Promtail收集日志,Grafana中实现日志与指标联动。 +- 告警:Alertmanager基于规则触发,结合企业微信/钉钉/Slack等通道推送。 + +章节来源 +- [backend/requirements.txt:20-22](file://backend/requirements.txt#L20-L22) +- [backend/Dockerfile:35-40](file://backend/Dockerfile#L35-L40) + +### 日志分析与故障诊断最佳实践 +- 统一日志格式与标签:为每条日志添加服务名、模块、函数、trace_id等标签,便于聚合与检索。 +- 分层告警:针对不同级别与模块设置阈值与静默窗口,避免噪声干扰。 +- 快速定位:优先查看ERROR/WARNING级别日志,结合trace_id串联相关模块日志。 +- 回放与回归:对关键业务路径(如平台查询、数据库写入)建立回放机制,复现问题并验证修复。 + +## 依赖分析 +- 组件耦合: + - 调度器与引擎松耦合,通过接口与数据库交互;平台适配器遵循统一抽象,便于扩展新平台。 + - 数据库连接通过依赖注入提供,降低全局状态耦合。 +- 外部依赖: + - PostgreSQL/Redis:通过环境变量配置,容器编排保证依赖服务健康。 + - Playwright:浏览器自动化依赖系统库,Dockerfile中已安装必要依赖与浏览器。 +- 潜在风险: + - 平台查询超时与不稳定:已有重试与超时处理,建议进一步引入熔断与降级策略。 + - 日志分散:建议集中化收集与结构化输出,避免grep式排查。 + +```mermaid +graph LR +REQ["requirements.txt"] --> FASTAPI["FastAPI"] +REQ --> APS["APScheduler"] +REQ --> SQLA["SQLAlchemy"] +REQ --> REDIS["Redis"] +REQ --> PW["Playwright"] +DF["Dockerfile"] --> SYSDEPS["系统依赖安装"] +DF --> PLY["Playwright 安装"] +DF --> CMD["Uvicorn 启动"] +DC["docker-compose.yml"] --> DBH["PostgreSQL 健康检查"] +DC --> RDH["Redis 健康检查"] +DC --> DEP["后端依赖健康启动"] +``` + +图表来源 +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) +- [backend/Dockerfile:6-33](file://backend/Dockerfile#L6-L33) +- [backend/docker-compose.yml:16-34](file://backend/docker-compose.yml#L16-L34) + +章节来源 +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) +- [backend/Dockerfile:1-41](file://backend/Dockerfile#L1-L41) +- [backend/docker-compose.yml:1-71](file://backend/docker-compose.yml#L1-L71) + +## 性能考虑 +- I/O密集型优化:平台查询与数据库写入均为I/O密集,建议: + - 使用连接池与批量写入减少开销。 + - 对平台查询结果进行缓存(短期有效),降低重复请求。 +- 超时与重试:平台适配器已内置指数退避重试,建议: + - 设置最大重试次数与超时上限,防止雪崩。 + - 引入熔断器,当错误率超过阈值时短时间拒绝请求。 +- 资源限制:在容器编排中设置CPU/内存限制与重启策略,避免单点故障影响整体。 + +## 故障排查指南 +- 健康检查失败: + - 检查后端/数据库/Redis健康检查配置与日志。 + - 确认依赖服务已就绪再启动后端。 +- 平台查询失败: + - 查看平台适配器日志,确认浏览器启动与页面交互是否正常。 + - 检查网络连通性与平台可用性。 +- 数据库写入失败: + - 检查数据库连接字符串与权限。 + - 关注事务提交与异常回滚日志。 +- 日志分析: + - 使用统一字段检索trace_id,串联各模块日志。 + - 结合Grafana面板观察指标趋势,定位异常时段。 + +章节来源 +- [backend/docker-compose.yml:46-50](file://backend/docker-compose.yml#L46-L50) +- [backend/app/workers/platforms/kimi.py:28-48](file://backend/app/workers/platforms/kimi.py#L28-L48) +- [backend/app/workers/platforms/wenxin.py:28-48](file://backend/app/workers/platforms/wenxin.py#L28-L48) +- [backend/app/workers/citation_engine.py:211-227](file://backend/app/workers/citation_engine.py#L211-L227) + +## 结论 +GEO项目已具备基础的健康检查、容器健康检查与完善的日志记录能力。建议在此基础上引入统一的监控与告警体系(Prometheus/Grafana),完善结构化日志与指标导出,增强平台查询的稳定性与可观测性,以支撑生产环境的持续运维与快速故障定位。 + +## 附录 +- 快速对照表 + - 健康检查端点:GET /health + - 数据库连接:DATABASE_URL + - Redis连接:REDIS_URL + - 定时任务:每小时执行一次 + - 平台适配器:Kimi、文心一言 + +章节来源 +- [backend/app/main.py:45-47](file://backend/app/main.py#L45-L47) +- [backend/app/config.py:7-8](file://backend/app/config.py#L7-L8) +- [backend/app/workers/scheduler.py:32-38](file://backend/app/workers/scheduler.py#L32-L38) +- [backend/app/workers/platforms/base.py:4-17](file://backend/app/workers/platforms/base.py#L4-L17) \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/部署与运维/运维最佳实践.md b/.qoder/repowiki/zh/content/部署与运维/运维最佳实践.md new file mode 100644 index 0000000..2e775d4 --- /dev/null +++ b/.qoder/repowiki/zh/content/部署与运维/运维最佳实践.md @@ -0,0 +1,375 @@ +# 运维最佳实践 + + +**本文档引用的文件** +- [docker-compose.yml](file://docker-compose.yml) +- [backend/Dockerfile](file://backend/Dockerfile) +- [frontend/Dockerfile](file://frontend/Dockerfile) +- [backend/requirements.txt](file://backend/requirements.txt) +- [frontend/package.json](file://frontend/package.json) +- [backend/app/config.py](file://backend/app/config.py) +- [backend/app/main.py](file://backend/app/main.py) +- [backend/app/database.py](file://backend/app/database.py) +- [backend/alembic.ini](file://backend/alembic.ini) +- [backend/alembic/env.py](file://backend/alembic/env.py) +- [backend/app/workers/scheduler.py](file://backend/app/workers/scheduler.py) + + +## 目录 +1. [简介](#简介) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖分析](#依赖分析) +7. [性能考虑](#性能考虑) +8. [故障排查指南](#故障排查指南) +9. [结论](#结论) +10. [附录](#附录) + +## 简介 +本指南面向GEO平台的运维团队,围绕日常运维任务、安全运维、故障恢复、容量规划与扩容、运维自动化与CI/CD、团队职责与应急响应、以及成本优化与监控等方面,提供可操作的最佳实践。文档基于仓库现有配置与代码进行梳理,并结合容器化部署与异步数据库访问等技术特性,给出落地建议。 + +## 项目结构 +GEO采用前后端分离的容器化架构: +- 前端服务:Next.js应用,开发模式下通过NPM脚本启动,容器暴露3000端口。 +- 后端服务:FastAPI应用,使用Uvicorn运行,容器暴露8000端口。 +- 数据库:PostgreSQL 15,持久化卷挂载,健康检查通过pg_isready。 +- 缓存:Redis 7,持久化卷挂载,健康检查通过redis-cli ping。 +- 配置:后端通过Pydantic设置从.env文件加载;数据库迁移使用Alembic。 + +```mermaid +graph TB +subgraph "前端" +FE_Docker["前端Dockerfile
暴露3000端口"] +FE_NPM["package.json
dev/build/start/lint"] +end +subgraph "后端" +BE_Docker["后端Dockerfile
暴露8000端口"] +BE_Main["app/main.py
FastAPI应用与路由注册"] +BE_Config["app/config.py
.env加载与默认配置"] +BE_DB["app/database.py
异步引擎与会话工厂"] +BE_Sched["app/workers/scheduler.py
APScheduler定时任务"] +BE_Alembic["alembic.ini / env.py
数据库迁移配置"] +end +subgraph "基础设施" +DB["PostgreSQL 15
健康检查: pg_isready"] +REDIS["Redis 7
健康检查: redis-cli ping"] +end +FE_Docker --> |"3000"| BE_Main +BE_Docker --> |"8000"| BE_Main +BE_Main --> BE_DB +BE_Main --> REDIS +BE_DB --> DB +BE_Sched --> BE_DB +BE_Sched --> REDIS +``` + +图表来源 +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) +- [frontend/Dockerfile:1-15](file://frontend/Dockerfile#L1-L15) +- [backend/Dockerfile:1-41](file://backend/Dockerfile#L1-L41) +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/alembic.ini:1-150](file://backend/alembic.ini#L1-L150) +- [backend/alembic/env.py:1-89](file://backend/alembic/env.py#L1-L89) + +章节来源 +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) +- [frontend/Dockerfile:1-15](file://frontend/Dockerfile#L1-L15) +- [backend/Dockerfile:1-41](file://backend/Dockerfile#L1-L41) +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/alembic.ini:1-150](file://backend/alembic.ini#L1-L150) +- [backend/alembic/env.py:1-89](file://backend/alembic/env.py#L1-L89) + +## 核心组件 +- 应用入口与生命周期:后端通过FastAPI应用注册路由与CORS中间件,并在应用生命周期内启动定时任务调度器。 +- 数据库连接:使用SQLAlchemy异步引擎与会话工厂,支持异步事务与连接池配置。 +- 配置管理:通过Pydantic设置从.env文件读取配置,包含数据库URL、Redis URL、JWT密钥、浏览器驱动路径等。 +- 定时任务:基于APScheduler的异步调度器,按小时检查并执行到期的查询任务。 +- 数据库迁移:Alembic配置与环境脚本,支持离线与在线迁移,连接字符串来自配置。 + +章节来源 +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [backend/alembic.ini:1-150](file://backend/alembic.ini#L1-L150) +- [backend/alembic/env.py:1-89](file://backend/alembic/env.py#L1-L89) + +## 架构总览 +下图展示容器化部署下的服务交互与数据流: + +```mermaid +sequenceDiagram +participant Dev as "开发者/CI" +participant Compose as "docker-compose" +participant FE as "前端容器(3000)" +participant BE as "后端容器(8000)" +participant DB as "PostgreSQL(5432)" +participant R as "Redis(6379)" +Dev->>Compose : 启动/停止/重建 +Compose->>BE : 依赖DB/Redis健康就绪 +Compose->>FE : 依赖后端可用 +FE->>BE : HTTP请求 /api/v1/* +BE->>DB : 异步ORM读写 +BE->>R : 缓存/任务状态 +BE-->>FE : JSON响应 +``` + +图表来源 +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) + +## 详细组件分析 + +### 数据库与迁移(PostgreSQL + Alembic) +- 连接与会话:异步引擎与会话工厂确保高并发下的连接复用与事务一致性。 +- 迁移策略:Alembic支持离线与在线迁移,连接字符串来自配置;日志级别可调以降低噪音。 +- 建议: + - 在生产环境固定DATABASE_URL,避免硬编码。 + - 迁移前先做备份,使用在线迁移时控制窗口时间。 + - 将迁移脚本纳入版本控制并审查变更。 + +```mermaid +flowchart TD +Start(["开始"]) --> CheckCfg["读取DATABASE_URL配置"] +CheckCfg --> Mode{"迁移模式"} +Mode --> |离线| Offline["生成/执行离线迁移"] +Mode --> |在线| Online["建立异步连接并执行迁移"] +Offline --> Done(["结束"]) +Online --> Done +``` + +图表来源 +- [backend/alembic.ini:86-90](file://backend/alembic.ini#L86-L90) +- [backend/alembic/env.py:33-88](file://backend/alembic/env.py#L33-L88) +- [backend/app/config.py:7-8](file://backend/app/config.py#L7-L8) + +章节来源 +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/alembic.ini:1-150](file://backend/alembic.ini#L1-L150) +- [backend/alembic/env.py:1-89](file://backend/alembic/env.py#L1-L89) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) + +### 定时任务调度(APScheduler) +- 触发周期:每小时检查一次到期查询。 +- 执行逻辑:查询状态为active且next_query_at小于等于当前UTC时间的任务,逐条交由引用引擎执行。 +- 错误处理:单任务异常不影响整体调度,日志记录错误并继续处理其他任务。 +- 建议: + - 将调度器纳入应用生命周期,在应用启动时启动,退出时优雅关闭。 + - 对高频任务增加幂等性校验与去重机制。 + - 监控任务执行耗时与积压情况。 + +```mermaid +flowchart TD +S(["启动调度器"]) --> AddJob["添加每小时检查任务"] +AddJob --> Loop{"每小时触发"} +Loop --> Query["查询到期active任务"] +Query --> Exec["逐条执行引用引擎"] +Exec --> Log["记录结果/异常"] +Log --> Loop +``` + +图表来源 +- [backend/app/workers/scheduler.py:30-90](file://backend/app/workers/scheduler.py#L30-L90) + +章节来源 +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) + +### 健康检查与容器编排 +- 数据库健康检查:使用pg_isready,间隔与重试次数可调。 +- Redis健康检查:使用redis-cli ping。 +- 服务依赖:后端等待数据库与缓存健康后再启动,前端等待后端可用。 +- 建议: + - 生产环境调整健康检查参数,避免误判。 + - 使用独立的健康检查端点(如/health)对外暴露,便于外部监控系统探测。 + +```mermaid +sequenceDiagram +participant DC as "docker-compose" +participant DB as "PostgreSQL" +participant R as "Redis" +participant BE as "后端" +DC->>DB : 健康检查(pg_isready) +DB-->>DC : OK +DC->>R : 健康检查(redis-cli ping) +R-->>DC : PONG +DC->>BE : 启动应用(等待DB/Redis健康) +``` + +图表来源 +- [docker-compose.yml:16-34](file://docker-compose.yml#L16-L34) +- [docker-compose.yml:46-50](file://docker-compose.yml#L46-L50) + +章节来源 +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) + +### 配置与环境变量 +- 关键配置项:数据库URL、Redis URL、JWT密钥、过期时间、Playwright浏览器路径、第三方平台API Key等。 +- 加载方式:通过Pydantic设置从.env文件加载,支持类型校验与默认值。 +- 建议: + - 生产环境使用只读权限的数据库账号与专用密钥。 + - 将敏感信息放入密钥管理服务或编排平台的密文存储。 + - 分环境(.env.dev/.env.prod)隔离配置,避免混用。 + +章节来源 +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) + +### 前后端镜像构建 +- 后端镜像:基于python:3.11-slim,安装Playwright所需系统依赖,预装Python依赖并安装浏览器驱动。 +- 前端镜像:基于node:20-alpine,使用npm ci安装依赖。 +- 建议: + - 使用多阶段构建减少镜像体积。 + - 固定基础镜像版本,启用镜像扫描与漏洞告警。 + - 在CI中缓存依赖层以提升构建速度。 + +章节来源 +- [backend/Dockerfile:1-41](file://backend/Dockerfile#L1-L41) +- [frontend/Dockerfile:1-15](file://frontend/Dockerfile#L1-L15) +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) +- [frontend/package.json:1-40](file://frontend/package.json#L1-L40) + +## 依赖分析 +- 组件耦合: + - 后端主程序依赖数据库模块与调度器模块。 + - 调度器依赖数据库会话工厂与引用引擎。 + - Alembic依赖配置与模型元数据。 +- 外部依赖: + - 数据库:PostgreSQL(异步驱动)。 + - 缓存:Redis。 + - 浏览器自动化:Playwright(chromium)。 +- 可能的改进: + - 将调度器抽象为可注入的服务,便于测试与替换。 + - 将数据库连接参数与迁移配置进一步解耦。 + +```mermaid +graph LR +Main["app/main.py"] --> DBMod["app/database.py"] +Main --> Sched["app/workers/scheduler.py"] +Sched --> DBMod +Alembic["alembic/env.py"] --> Cfg["app/config.py"] +Alembic --> Models["app/models/*"] +``` + +图表来源 +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [backend/alembic/env.py:1-89](file://backend/alembic/env.py#L1-L89) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) + +章节来源 +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [backend/alembic/env.py:1-89](file://backend/alembic/env.py#L1-L89) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) + +## 性能考虑 +- 数据库性能: + - 使用异步引擎与连接池,合理设置超时与空闲回收。 + - 为热点查询建立索引,避免全表扫描。 + - 控制事务粒度,减少长事务锁竞争。 +- 缓存策略: + - 利用Redis缓存热点数据与任务状态,降低数据库压力。 + - 设置合理的TTL与淘汰策略,避免内存膨胀。 +- 定时任务: + - 控制任务频率与执行时长,避免高峰期抖动。 + - 对任务执行结果进行统计与告警。 +- 前后端镜像: + - 减少镜像层数与体积,缩短拉取与启动时间。 + - 使用CDN与静态资源优化。 + +## 故障排查指南 +- 健康检查失败 + - 检查数据库与Redis健康检查命令是否可用。 + - 查看容器日志,确认端口映射与网络连通。 +- 数据库连接异常 + - 核对DATABASE_URL格式与凭据。 + - 检查PostgreSQL日志与连接数上限。 +- 定时任务未执行 + - 确认调度器已启动且未报错。 + - 检查任务状态与next_query_at字段。 +- 前端无法访问后端 + - 核对CORS配置与路由前缀。 + - 检查后端/health端点返回状态。 + +章节来源 +- [docker-compose.yml:16-34](file://docker-compose.yml#L16-L34) +- [backend/app/main.py:30-36](file://backend/app/main.py#L30-L36) +- [backend/app/workers/scheduler.py:30-90](file://backend/app/workers/scheduler.py#L30-L90) + +## 结论 +本指南基于仓库现有配置与代码,总结了GEO平台的运维关注点与最佳实践建议。建议在生产环境中进一步完善配置管理、安全加固、监控告警与自动化流程,以保障系统的稳定性与可维护性。 + +## 附录 + +### 日常运维任务清单 +- 定期备份策略 + - 数据库:使用pg_dump/pg_restore进行增量/全量备份,保留至少7天滚动备份。 + - 持久化卷:定期快照或导出postgres_data与redis_data。 +- 数据库维护 + - 清理历史日志与临时表,重建索引,更新统计信息。 + - 监控慢查询与锁等待,定位瓶颈。 +- 系统更新流程 + - 先在测试环境验证镜像与配置变更,再灰度到生产。 + - 使用滚动更新与健康检查,确保零停机。 + +### 安全运维实践 +- 漏洞扫描 + - 镜像扫描:在CI中集成镜像漏洞扫描工具。 + - 依赖扫描:定期扫描Python与Node依赖的安全问题。 +- 安全补丁管理 + - 基础镜像与依赖库定期升级,优先修复高危漏洞。 +- 访问审计 + - 开启数据库与应用日志审计,记录敏感操作。 + - 限制数据库连接权限,最小化管理员账号使用。 + +### 故障恢复流程 +- 灾难恢复计划 + - 明确RTO/RPO目标,准备多活或多副本方案。 +- 数据恢复 + - 从最近备份恢复,核对时间点一致性。 +- 系统回滚策略 + - 采用蓝绿/金丝雀发布,失败自动回滚。 + - 回滚前做好配置与数据备份。 + +### 容量规划与扩容策略 +- 水平扩展 + - 前后端服务均可横向扩展,注意会话与共享状态的处理。 +- 垂直扩展 + - 根据CPU/内存与I/O使用率评估,逐步提升实例规格。 +- 决策因素 + - QPS、响应时间、错误率、资源利用率阈值。 + +### 运维自动化与CI/CD +- 自动化脚本 + - 备份脚本:封装pg_dump与卷快照命令。 + - 部署脚本:封装docker-compose拉起/回滚流程。 +- CI/CD流水线 + - 触发条件:push到主分支或打标签。 + - 步骤:代码检查、单元测试、镜像构建、安全扫描、部署、健康检查。 + - 告警:失败通知与自动回滚。 + +### 运维团队职责与应急响应 +- 职责分工 + - 基础设施:负责容器编排、网络与存储。 + - 应用运维:负责后端服务、数据库与缓存。 + - 安全与合规:负责漏洞扫描、密钥与审计。 +- 应急响应 + - 建立值班制度与应急预案,明确升级路径。 + - 使用统一告警平台,确保信息透明与快速处置。 + +### 成本优化与监控 +- 成本优化 + - 合理选择实例规格与存储类型,避免过度配置。 + - 使用预留实例或批量折扣,优化闲置资源。 +- 资源利用率监控 + - CPU/内存/磁盘/网络IO与数据库连接数。 + - 业务指标:QPS、P95/P99延迟、错误率、任务积压。 \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/部署与运维/部署与运维.md b/.qoder/repowiki/zh/content/部署与运维/部署与运维.md new file mode 100644 index 0000000..c36c8ef --- /dev/null +++ b/.qoder/repowiki/zh/content/部署与运维/部署与运维.md @@ -0,0 +1,418 @@ +# 部署与运维 + + +**本文引用的文件** +- [docker-compose.yml](file://docker-compose.yml) +- [backend/Dockerfile](file://backend/Dockerfile) +- [frontend/Dockerfile](file://frontend/Dockerfile) +- [backend/app/main.py](file://backend/app/main.py) +- [backend/app/config.py](file://backend/app/config.py) +- [backend/requirements.txt](file://backend/requirements.txt) +- [frontend/package.json](file://frontend/package.json) +- [backend/app/workers/scheduler.py](file://backend/app/workers/scheduler.py) +- [backend/app/api/auth.py](file://backend/app/api/auth.py) +- [backend/app/database.py](file://backend/app/database.py) +- [backend/app/models/query.py](file://backend/app/models/query.py) +- [backend/alembic.ini](file://backend/alembic.ini) +- [backend/alembic/env.py](file://backend/alembic/env.py) +- [tests/test_auth.py](file://tests/test_auth.py) + + +## 目录 +1. [简介](#简介) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖分析](#依赖分析) +7. [性能考虑](#性能考虑) +8. [故障排查指南](#故障排查指南) +9. [结论](#结论) +10. [附录](#附录) + +## 简介 +本文件面向GEO项目的部署与运维团队,提供从开发到生产的完整落地指南。内容覆盖Docker容器化部署、镜像构建、服务编排与环境配置;生产部署策略(Nginx反向代理、SSL证书、负载均衡);监控与日志管理(健康检查、错误追踪、性能监控);运维最佳实践(备份、安全、故障恢复)以及CI/CD流水线与自动化部署建议。文档严格基于仓库现有配置与实现进行说明,避免臆测。 + +## 项目结构 +GEO采用前后端分离架构,后端为FastAPI应用,前端为Next.js应用,数据库使用PostgreSQL,缓存使用Redis。通过Docker Compose进行本地开发与演示环境编排,支持数据库、Redis、后端API与前端开发服务器的统一启动与依赖管理。 + +```mermaid +graph TB +subgraph "容器编排" +DC["docker-compose.yml"] +end +subgraph "后端服务" +BE["backend/Dockerfile"] +API["backend/app/main.py"] +CFG["backend/app/config.py"] +DB["backend/app/database.py"] +SCH["backend/app/workers/scheduler.py"] +ALEMBIC["backend/alembic/*"] +end +subgraph "前端服务" +FE["frontend/Dockerfile"] +PKG["frontend/package.json"] +end +subgraph "基础设施" +PG["PostgreSQL:5432"] +RD["Redis:6379"] +end +DC --> BE +DC --> FE +DC --> PG +DC --> RD +BE --> API +API --> DB +API --> SCH +DB --> PG +SCH --> RD +FE --> API +``` + +图表来源 +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) +- [backend/Dockerfile:1-41](file://backend/Dockerfile#L1-L41) +- [frontend/Dockerfile:1-15](file://frontend/Dockerfile#L1-L15) +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [backend/alembic.ini:1-150](file://backend/alembic.ini#L1-L150) +- [backend/alembic/env.py:1-89](file://backend/alembic/env.py#L1-L89) + +章节来源 +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) +- [backend/Dockerfile:1-41](file://backend/Dockerfile#L1-L41) +- [frontend/Dockerfile:1-15](file://frontend/Dockerfile#L1-L15) + +## 核心组件 +- 后端API(FastAPI) + - 应用入口与生命周期管理、路由注册、CORS中间件、健康检查端点。 + - 配置加载与数据库连接、Redis连接、定时任务调度器。 +- 数据库(PostgreSQL) + - 异步SQLAlchemy引擎、会话管理、模型定义与索引。 +- 缓存(Redis) + - 用于任务调度与状态存储等场景。 +- 前端(Next.js) + - 开发模式运行,与后端API交互。 +- 迁移与版本控制(Alembic) + - 异步迁移环境、配置文件与日志级别。 + +章节来源 +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [backend/alembic.ini:1-150](file://backend/alembic.ini#L1-L150) +- [backend/alembic/env.py:1-89](file://backend/alembic/env.py#L1-L89) + +## 架构总览 +下图展示生产环境典型拓扑:Nginx作为反向代理与SSL终止,后端API以多实例运行,数据库与Redis作为共享资源,前端静态资源可由Nginx或CDN提供。 + +```mermaid +graph TB +subgraph "外部访问" +U["用户浏览器"] +end +subgraph "边缘层" +NGINX["Nginx 反向代理
SSL/TLS 终止"] +end +subgraph "应用层" +LB["负载均衡器/反向代理" +subgraph "后端实例" +API1["API 实例 1"] +API2["API 实例 2"] +API3["API 实例 N"] +end +end +end +subgraph "数据与缓存" +DB["PostgreSQL 主库/只读副本"] +RDS["Redis 集群/哨兵"] +end +U --> NGINX +NGINX --> LB +LB --> API1 +LB --> API2 +LB --> API3 +API1 --> DB +API2 --> DB +API3 --> DB +API1 --> RDS +API2 --> RDS +API3 --> RDS +``` + +(本图为概念性架构示意,不对应具体源码文件) + +## 详细组件分析 + +### 后端API(FastAPI)与容器化 +- 容器镜像构建要点 + - 基于Python slim镜像,安装Playwright所需系统依赖,并预装依赖后复制应用代码。 + - 暴露8000端口,使用Uvicorn启动应用。 +- 应用特性 + - 生命周期钩子在启动时初始化调度器,在关闭时优雅停机。 + - 注册认证、查询、引用数据、报告等路由前缀。 + - 提供/CORS允许来自前端开发地址的跨域请求。 + - 提供健康检查端点。 +- 配置与连接 + - 通过环境变量加载数据库URL、Redis URL、JWT密钥、平台API密钥等。 + - 数据库连接为异步引擎,使用会话工厂管理事务。 +- 定时任务 + - 使用APScheduler异步调度器,每小时检查到期查询并调用引用引擎执行。 + +```mermaid +sequenceDiagram +participant C as "容器" +participant U as "Uvicorn" +participant A as "FastAPI 应用" +participant S as "调度器" +participant D as "数据库" +participant R as "Redis" +C->>U : "启动进程" +U->>A : "加载应用与生命周期" +A->>S : "启动调度器" +A->>D : "建立异步连接" +A->>R : "建立连接" +A-->>C : "健康检查端点可用" +S->>D : "周期性查询待执行任务" +S->>A : "触发执行逻辑" +``` + +图表来源 +- [backend/Dockerfile:1-41](file://backend/Dockerfile#L1-L41) +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) + +章节来源 +- [backend/Dockerfile:1-41](file://backend/Dockerfile#L1-L41) +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) + +### 前端(Next.js)与容器化 +- 容器镜像构建要点 + - 基于Node Alpine镜像,安装依赖后复制应用代码。 + - 暴露3000端口,开发模式启动。 +- 与后端交互 + - 默认CORS允许来自前端开发地址的请求,生产环境需根据域名调整。 + +章节来源 +- [frontend/Dockerfile:1-15](file://frontend/Dockerfile#L1-L15) +- [frontend/package.json:1-40](file://frontend/package.json#L1-L40) +- [backend/app/main.py:30-36](file://backend/app/main.py#L30-L36) + +### 数据库与迁移(PostgreSQL + Alembic) +- 连接与会话 + - 使用异步引擎与会话工厂,提供依赖注入式数据库会话。 +- 迁移 + - 配置文件指定数据库URL,环境脚本支持离线与在线迁移,异步连接迁移上下文。 +- 模型与索引 + - 查询模型包含用户外键、关键词、平台列表、频率、状态、时间戳等字段,并建立复合索引以优化查询。 + +```mermaid +erDiagram +USERS { +uuid id PK +string email UK +string name +string plan +int max_queries +boolean is_active +timestamp created_at +timestamp updated_at +} +QUERIES { +uuid id PK +uuid user_id FK +string keyword +string target_brand +jsonb brand_aliases +jsonb platforms +string frequency +string status +timestamp last_queried_at +timestamp next_query_at +timestamp created_at +timestamp updated_at +} +CITATION_RECORDS { +uuid id PK +uuid query_id FK +jsonb raw_data +timestamp created_at +} +QUERY_TASKS { +uuid id PK +uuid query_id FK +string platform +string status +timestamp scheduled_at +timestamp executed_at +} +USERS ||--o{ QUERIES : "拥有" +QUERIES ||--o{ CITATION_RECORDS : "生成" +QUERIES ||--o{ QUERY_TASKS : "拆分任务" +``` + +图表来源 +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/alembic.ini:86-89](file://backend/alembic.ini#L86-L89) +- [backend/alembic/env.py:1-89](file://backend/alembic/env.py#L1-L89) + +章节来源 +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [backend/alembic.ini:1-150](file://backend/alembic.ini#L1-L150) +- [backend/alembic/env.py:1-89](file://backend/alembic/env.py#L1-L89) + +### 认证与安全(JWT) +- 接口 + - 提供注册、登录、获取当前用户信息的接口。 +- 安全要点 + - JWT密钥默认值仅用于开发,生产必须替换。 + - CORS在开发环境允许前端地址,生产需限定来源。 + +章节来源 +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/app/config.py:9-9](file://backend/app/config.py#L9-L9) + +### 编排与健康检查(Docker Compose) +- 服务编排 + - db、redis、backend、frontend四类服务,分别映射端口、挂载卷、加载环境文件。 +- 健康检查 + - PostgreSQL与Redis提供健康检查探针,后端API提供/CORS与健康检查端点。 +- 依赖顺序 + - 后端等待数据库与Redis健康后再启动,前端依赖后端。 + +章节来源 +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) +- [backend/app/main.py:45-48](file://backend/app/main.py#L45-L48) + +## 依赖分析 +- 后端依赖 + - Web框架、数据库、配置校验、认证加密、缓存与调度、浏览器自动化、HTTP客户端、测试工具等。 +- 前端依赖 + - Next.js、React、UI组件库、图表库、类型与样式工具等。 +- 运行时依赖 + - Playwright Chromium浏览器、系统库满足其运行需求。 + +```mermaid +graph LR +subgraph "后端" +REQ["requirements.txt"] +PY["Python 3.11"] +PLT["Playwright"] +end +subgraph "前端" +PKGJSON["package.json"] +NODE["Node 20"] +end +REQ --> PY +REQ --> PLT +PKGJSON --> NODE +``` + +图表来源 +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) +- [frontend/package.json:1-40](file://frontend/package.json#L1-L40) + +章节来源 +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) +- [frontend/package.json:1-40](file://frontend/package.json#L1-L40) + +## 性能考虑 +- 数据库 + - 查询模型已建立多列索引,建议结合慢查询日志与执行计划持续优化。 +- 缓存 + - Redis用于任务调度与临时状态,建议在高并发场景下启用持久化与主从复制。 +- API + - 使用异步数据库连接与事件循环,减少阻塞;对高频接口开启限流与熔断。 +- 前端 + - 生产构建与静态资源缓存策略,配合CDN提升加载速度。 +- 容器 + - 后端与前端镜像体积较小,建议启用只读根文件系统与最小权限运行。 + +(本节为通用指导,不直接分析具体文件) + +## 故障排查指南 +- 健康检查 + - 后端/CORS与健康检查端点可用于快速判断服务可用性。 +- 日志 + - 后端使用标准库日志,Alembic配置了日志级别;建议在生产中接入集中式日志收集。 +- 数据库迁移 + - 使用异步迁移环境,确保数据库URL正确;离线/在线迁移均可通过配置切换。 +- 认证问题 + - 生产需替换JWT密钥;CORS来源需按域名精确配置。 +- 单元测试 + - 认证模块测试覆盖注册、登录与当前用户接口,可作为回归测试基线。 + +章节来源 +- [backend/app/main.py:45-48](file://backend/app/main.py#L45-L48) +- [backend/alembic.ini:115-150](file://backend/alembic.ini#L115-L150) +- [backend/alembic/env.py:64-89](file://backend/alembic/env.py#L64-L89) +- [tests/test_auth.py:1-104](file://tests/test_auth.py#L1-L104) + +## 结论 +本部署与运维文档基于仓库现有配置,给出了从容器化到生产部署的实施路径与最佳实践建议。建议在生产环境中补充Nginx反向代理与SSL、集中化日志与监控、完善的备份与灾难恢复策略,并通过CI/CD实现自动化部署与回滚。 + +## 附录 + +### A. Docker容器化部署流程(开发/演示) +- 准备工作 + - 确保已安装Docker与Docker Compose。 +- 启动服务 + - 在仓库根目录执行编排命令,等待所有服务进入健康状态。 +- 访问应用 + - 前端:http://localhost:3000 + - 后端:http://localhost:8000 + - 数据库:localhost:5432 + - Redis:localhost:6379 + +章节来源 +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) + +### B. 生产环境部署策略 +- 反向代理与SSL + - 使用Nginx作为反向代理,开启HTTPS并配置证书自动续期。 +- 负载均衡 + - 将后端API以多实例部署并通过LB分发流量,确保健康检查与自动扩缩容。 +- 环境隔离 + - 区分开发、测试、预发布与生产环境,严格管理环境变量与密钥。 + +(本节为通用指导,不直接分析具体文件) + +### C. 监控与日志管理 +- 健康检查 + - 利用/CORS与健康检查端点,结合探针实现自动发现与告警。 +- 错误追踪 + - 在应用中集成结构化日志,输出到标准输出,由平台收集至集中式系统。 +- 性能监控 + - 关注数据库慢查询、Redis命中率、API响应时间与错误率。 + +(本节为通用指导,不直接分析具体文件) + +### D. 运维最佳实践 +- 备份策略 + - 数据库与Redis定期快照与归档,验证恢复流程。 +- 安全配置 + - 最小权限原则、密钥轮换、网络隔离、入站/出站策略。 +- 故障恢复 + - 制定回滚预案与演练计划,确保快速恢复。 + +(本节为通用指导,不直接分析具体文件) + +### E. CI/CD流水线与自动化部署 +- 建议阶段 + - 代码提交触发测试(含认证接口测试),通过后构建镜像并推送制品库,随后部署到目标环境。 +- 回滚机制 + - 支持一键回滚至上一个稳定版本。 +- 配置管理 + - 环境变量与密钥通过安全渠道注入,避免硬编码。 + +(本节为通用指导,不直接分析具体文件) \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/项目概述/快速开始.md b/.qoder/repowiki/zh/content/项目概述/快速开始.md new file mode 100644 index 0000000..9574089 --- /dev/null +++ b/.qoder/repowiki/zh/content/项目概述/快速开始.md @@ -0,0 +1,390 @@ +# 快速开始 + + +**本文引用的文件** +- [docker-compose.yml](file://docker-compose.yml) +- [backend/Dockerfile](file://backend/Dockerfile) +- [frontend/Dockerfile](file://frontend/Dockerfile) +- [backend/requirements.txt](file://backend/requirements.txt) +- [frontend/package.json](file://frontend/package.json) +- [backend/app/main.py](file://backend/app/main.py) +- [backend/app/config.py](file://backend/app/config.py) +- [backend/alembic.ini](file://backend/alembic.ini) +- [backend/alembic/env.py](file://backend/alembic/env.py) +- [backend/alembic/versions/488d0bd5ab01_initial_migration.py](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py) +- [backend/app/database.py](file://backend/app/database.py) +- [backend/app/workers/scheduler.py](file://backend/app/workers/scheduler.py) +- [backend/app/workers/citation_engine.py](file://backend/app/workers/citation_engine.py) +- [frontend/README.md](file://frontend/README.md) + + +## 目录 +1. [简介](#简介) +2. [项目结构](#项目结构) +3. [前置条件与环境准备](#前置条件与环境准备) +4. [一键启动(Docker Compose)](#一键启动docker-compose) +5. [首次运行与基本操作](#首次运行与基本操作) +6. [架构概览](#架构概览) +7. [组件详解](#组件详解) +8. [依赖关系分析](#依赖关系分析) +9. [性能与资源建议](#性能与资源建议) +10. [故障排除](#故障排除) +11. [结语](#结语) + +## 简介 +本指南面向首次接触 GEO 项目的用户,帮助你在最短时间内完成环境准备、依赖安装、数据库初始化与容器编排,并体验核心功能:用户注册、创建查询任务、查看结果与图表。文档严格基于仓库中的实际配置与代码,提供可复现的命令行步骤与预期行为说明。 + +## 项目结构 +项目采用前后端分离的多容器架构,通过 Docker Compose 统一编排: +- 后端:FastAPI 应用,提供认证、查询词、引用数据、报告等接口;内置定时任务调度器与浏览器自动化能力。 +- 前端:Next.js 应用,提供登录、注册、仪表盘、查询管理、引用与报告可视化界面。 +- 数据库:PostgreSQL(15),使用 Alembic 进行迁移管理。 +- 缓存:Redis(7),用于任务队列与缓存。 +- Playwright:用于浏览器自动化(爬虫/抓取)场景。 + +```mermaid +graph TB +subgraph "容器编排" +DB["PostgreSQL 容器"] +REDIS["Redis 容器"] +BE["后端容器"] +FE["前端容器"] +end +FE --> |"HTTP 3000"| BE +BE --> |"数据库 5432"| DB +BE --> |"缓存 6379"| REDIS +``` + +**图示来源** +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) + +**章节来源** +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) + +## 前置条件与环境准备 +- 操作系统:Linux/macOS/Windows(WSL2 推荐) +- Docker 与 Docker Compose:用于一键编排 +- Git:克隆仓库 +- 文本编辑器或 IDE:可选,用于查看/修改配置 + +说明 +- 本项目未要求本地安装 Python 3.9+ 或 Node.js,因为所有运行时均在容器内完成。 +- 若你希望在本地直接运行后端或前端,请参考各自镜像版本: + - 后端镜像基于 Python 3.11(容器内运行) + - 前端镜像基于 Node.js 20(容器内运行) + +**章节来源** +- [backend/Dockerfile:1-41](file://backend/Dockerfile#L1-L41) +- [frontend/Dockerfile:1-15](file://frontend/Dockerfile#L1-L15) + +## 一键启动(Docker Compose) +以下为完整的一键启动流程,涵盖容器编排、服务启动顺序、端口映射与健康检查。 + +步骤 +1. 克隆仓库 + - git clone <仓库地址> + - cd GEO + +2. 构建并启动容器 + - docker compose up -d + +3. 查看服务状态 + - docker compose ps + - 预期:db、redis、backend、frontend 均为 Up(healthy) + +4. 访问应用 + - 前端:http://localhost:3000 + - 后端:http://localhost:8000(健康检查接口 /health 可用) + +5. 停止与清理 + - docker compose down + - 如需清理数据卷:docker compose down -v + +```mermaid +sequenceDiagram +participant Dev as "开发者" +participant Compose as "Docker Compose" +participant DB as "PostgreSQL 容器" +participant Redis as "Redis 容器" +participant Backend as "后端容器" +participant Frontend as "前端容器" +Dev->>Compose : "docker compose up -d" +Compose->>DB : "启动并健康检查" +Compose->>Redis : "启动并健康检查" +Compose->>Backend : "等待 DB/Redis 健康后启动" +Backend->>DB : "连接数据库Alembic 已内置迁移" +Compose->>Frontend : "启动并监听 3000" +Frontend->>Backend : "请求 /api/v1/*" +Backend-->>Frontend : "返回 JSON 响应" +``` + +**图示来源** +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) +- [backend/app/main.py:45-48](file://backend/app/main.py#L45-L48) + +**章节来源** +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) +- [frontend/README.md:5-17](file://frontend/README.md#L5-L17) + +## 首次运行与基本操作 +首次启动后,你可以进行如下操作以体验核心功能: + +1. 用户注册与登录 + - 打开 http://localhost:3000 + - 使用注册页面创建账户 + - 登录后进入仪表盘 + +2. 创建查询任务 + - 在“查询”页面新建查询词,设置关键词、目标品牌、平台(默认包含 wenxin/kimi)、频率(日/周/月) + - 提交后,后端调度器会按频率与时间窗口自动执行 + +3. 查看结果 + - “引用数据”页面查看各平台的引用检测结果(是否被提及、置信度、上下文片段等) + - “报告”页面查看趋势与平台分布图表 + +4. 观察后台任务 + - 后端启动时会加载调度器,每小时检查一次到期的查询任务 + - 任务状态会在数据库中持久化,供前端展示 + +```mermaid +flowchart TD +Start(["提交查询任务"]) --> Schedule["写入数据库
设置 next_query_at"] +Schedule --> Timer["定时器每小时触发"] +Timer --> Check{"查询是否到期?"} +Check --> |否| Wait["等待下个小时"] +Check --> |是| Run["执行查询多平台"] +Run --> Detect["品牌匹配与竞争品牌检测"] +Detect --> Persist["写入引用记录与任务状态"] +Persist --> Report["生成可视化数据"] +Report --> End(["前端展示结果"]) +``` + +**图示来源** +- [backend/app/workers/scheduler.py:51-84](file://backend/app/workers/scheduler.py#L51-L84) +- [backend/app/workers/citation_engine.py:159-234](file://backend/app/workers/citation_engine.py#L159-L234) + +**章节来源** +- [backend/app/main.py:38-42](file://backend/app/main.py#L38-L42) +- [backend/app/workers/scheduler.py:30-40](file://backend/app/workers/scheduler.py#L30-L40) +- [backend/app/workers/citation_engine.py:148-170](file://backend/app/workers/citation_engine.py#L148-L170) + +## 架构概览 +整体架构由容器编排层、后端服务层、前端服务层、数据库与缓存组成。后端通过 Alembic 管理数据库迁移,使用 Redis 存储任务状态,使用 Playwright 进行浏览器自动化。 + +```mermaid +graph TB +FE["前端应用
Next.js"] --> API["后端 API
FastAPI"] +API --> DB["数据库
PostgreSQL"] +API --> CACHE["缓存
Redis"] +API --> WORKER["任务执行器
CitationEngine"] +WORKER --> PLATFORMS["平台适配器
Kimi/Wenxin"] +API --> SCHED["调度器
APScheduler"] +SCHED --> API +``` + +**图示来源** +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/config.py:7-13](file://backend/app/config.py#L7-L13) +- [backend/app/workers/scheduler.py:25-39](file://backend/app/workers/scheduler.py#L25-L39) +- [backend/app/workers/citation_engine.py:148-157](file://backend/app/workers/citation_engine.py#L148-L157) + +## 组件详解 + +### 后端应用入口与路由 +- 应用生命周期:启动时初始化模型与调度器,关闭时优雅停机 +- 路由模块:认证、查询词、引用数据、报告、即时执行查询 +- CORS:允许前端 localhost:3000 访问 +- 健康检查:/health 返回状态 + +**章节来源** +- [backend/app/main.py:13-47](file://backend/app/main.py#L13-L47) + +### 配置与连接 +- 配置来源:.env 文件(通过 pydantic-settings 加载) +- 默认数据库:postgresql+asyncpg://postgres:postgres123@db:5432/geo_platform +- 默认缓存:redis://redis:6379/0 +- Playwright 浏览器路径:/ms-playwright +- JWT 密钥与过期时间:开发默认值(生产请务必修改) + +**章节来源** +- [backend/app/config.py:4-16](file://backend/app/config.py#L4-L16) + +### 数据库与迁移 +- 引擎与会话:基于 SQLAlchemy AsyncEngine 与 async session +- 迁移:Alembic 管理,初始迁移包含 users、queries、citation_records、query_tasks、subscriptions 表 +- 运行方式:Compose 启动时由后端容器自动执行迁移(见后端 Dockerfile 中的安装与命令) + +```mermaid +erDiagram +USERS { +uuid id PK +string email UK +string password_hash +string name +string plan +int max_queries +bool is_active +timestamp created_at +timestamp updated_at +} +QUERIES { +uuid id PK +uuid user_id FK +string keyword +string target_brand +jsonb brand_aliases +jsonb platforms +string frequency +string status +timestamp last_queried_at +timestamp next_query_at +timestamp created_at +timestamp updated_at +} +CITATION_RECORDS { +uuid id PK +uuid query_id FK +string platform +bool cited +int citation_position +text citation_text +jsonb competitor_brands +text raw_response +timestamp queried_at +} +QUERY_TASKS { +uuid id PK +uuid query_id FK +string platform +string status +text error_message +timestamp scheduled_at +timestamp started_at +timestamp completed_at +} +SUBSCRIPTIONS { +uuid id PK +uuid user_id FK +string plan +string status +date start_date +date end_date +numeric amount +string payment_method +string payment_id +timestamp created_at +} +USERS ||--o{ QUERIES : "拥有" +QUERIES ||--o{ CITATION_RECORDS : "产生" +QUERIES ||--o{ QUERY_TASKS : "驱动" +USERS ||--o{ SUBSCRIPTIONS : "订阅" +``` + +**图示来源** +- [backend/alembic/versions/488d0bd5ab01_initial_migration.py:21-111](file://backend/alembic/versions/488d0bd5ab01_initial_migration.py#L21-L111) + +**章节来源** +- [backend/app/database.py:6-28](file://backend/app/database.py#L6-L28) +- [backend/alembic/env.py:10-25](file://backend/alembic/env.py#L10-L25) +- [backend/alembic.ini:89-89](file://backend/alembic.ini#L89-L89) + +### 任务调度与执行 +- 调度器:APScheduler AsyncIOScheduler,每小时检查到期查询 +- 执行器:CitationEngine,遍历查询平台,执行品牌匹配与竞争品牌检测,写入记录并更新查询时间 +- 平台适配器:Kimi 与 Wenxin(通过外部平台 API) + +```mermaid +classDiagram +class QueryScheduler { ++start() ++check_and_execute_queries() ++shutdown() +} +class CitationEngine { ++execute_query(query, db) ++execute_single_platform(keyword, platform, target_brand, brand_aliases) ++close() +} +class KimiAdapter { ++query(keyword) ++close() +} +class WenxinAdapter { ++query(keyword) ++close() +} +QueryScheduler --> CitationEngine : "调用执行" +CitationEngine --> KimiAdapter : "平台适配" +CitationEngine --> WenxinAdapter : "平台适配" +``` + +**图示来源** +- [backend/app/workers/scheduler.py:25-95](file://backend/app/workers/scheduler.py#L25-L95) +- [backend/app/workers/citation_engine.py:148-157](file://backend/app/workers/citation_engine.py#L148-L157) + +**章节来源** +- [backend/app/workers/scheduler.py:30-89](file://backend/app/workers/scheduler.py#L30-L89) +- [backend/app/workers/citation_engine.py:159-308](file://backend/app/workers/citation_engine.py#L159-L308) + +## 依赖关系分析 +- 后端依赖:FastAPI、SQLAlchemy、asyncpg、Alembic、Pydantic、Passlib、Redis、APScheduler、Playwright、httpx、python-dotenv、pytest 等 +- 前端依赖:Next.js、NextAuth、Radix UI、Recharts、Tailwind 等 + +```mermaid +graph LR +BE_REQ["后端依赖
requirements.txt"] --> BE_IMG["后端镜像
Python 3.11"] +FE_PKG["前端依赖
package.json"] --> FE_IMG["前端镜像
Node.js 20"] +BE_IMG --> BE_RUN["后端运行时"] +FE_IMG --> FE_RUN["前端运行时"] +``` + +**图示来源** +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) +- [frontend/package.json:11-38](file://frontend/package.json#L11-L38) +- [backend/Dockerfile:28-39](file://backend/Dockerfile#L28-L39) +- [frontend/Dockerfile:6-14](file://frontend/Dockerfile#L6-L14) + +**章节来源** +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) +- [frontend/package.json:11-38](file://frontend/package.json#L11-L38) + +## 性能与资源建议 +- CPU/内存:建议至少 2 核心 4GB 内存,以便同时运行 PostgreSQL、Redis、后端与前端 +- 磁盘:为持久化卷预留足够空间(PostgreSQL 与 Redis 数据目录) +- 网络:Playwright 需要访问外部平台(Kimi/Wenxin),请确保网络连通性 +- 日志:后端默认关闭 SQL echo,若调试数据库问题可临时开启 + +[本节为通用建议,无需特定文件引用] + +## 故障排除 +常见问题与解决思路 +- 容器无法启动或健康检查失败 + - 检查端口占用:确认 5432、6379、8000、3000 未被占用 + - 查看日志:docker compose logs db|redis|backend|frontend + - 重启服务:docker compose restart db|redis|backend|frontend + +- 前端无法访问或跨域报错 + - 确认前端已启动并监听 3000 + - 后端 CORS 已允许 http://localhost:3000,若自定义域名请在后端 CORS 配置中添加 + +- 数据库连接失败 + - 确认 .env 中 DATABASE_URL 与 docker-compose 中的 db 服务一致 + - 确认数据库初始化已完成(首次启动后端会自动执行迁移) + +- Playwright 报错或浏览器不可用 + - 确认后端镜像已安装 Playwright 并完成浏览器安装 + - 如需离线部署,可在 .env 中配置 Playwright 浏览器路径 + +- 任务未执行或结果为空 + - 检查查询频率与 next_query_at 是否正确 + - 查看查询任务表状态(pending/running/success/failed) + +**章节来源** +- [docker-compose.yml:16-20](file://docker-compose.yml#L16-L20) +- [backend/app/main.py:30-36](file://backend/app/main.py#L30-L36) +- [backend/app/config.py:7-11](file://backend/app/config.py#L7-L11) +- [backend/Dockerfile:31-33](file://backend/Dockerfile#L31-L33) +- [backend/app/workers/scheduler.py:51-75](file://backend/app/workers/scheduler.py#L51-L75) + +## 结语 +按照本指南完成 Docker Compose 一键启动后,你即可在浏览器中完成注册、创建查询任务并查看结果与图表。若需进一步扩展(如接入更多平台、调整调度策略或优化前端交互),可基于现有架构与配置进行定制。 \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/项目概述/技术栈.md b/.qoder/repowiki/zh/content/项目概述/技术栈.md new file mode 100644 index 0000000..d5baea2 --- /dev/null +++ b/.qoder/repowiki/zh/content/项目概述/技术栈.md @@ -0,0 +1,348 @@ +# 技术栈 + + +**本文档引用的文件** +- [docker-compose.yml](file://docker-compose.yml) +- [backend/Dockerfile](file://backend/Dockerfile) +- [frontend/Dockerfile](file://frontend/Dockerfile) +- [backend/requirements.txt](file://backend/requirements.txt) +- [frontend/package.json](file://frontend/package.json) +- [backend/app/main.py](file://backend/app/main.py) +- [backend/app/config.py](file://backend/app/config.py) +- [backend/app/database.py](file://backend/app/database.py) +- [backend/app/api/auth.py](file://backend/app/api/auth.py) +- [backend/app/workers/scheduler.py](file://backend/app/workers/scheduler.py) +- [frontend/tsconfig.json](file://frontend/tsconfig.json) +- [frontend/tailwind.config.ts](file://frontend/tailwind.config.ts) +- [frontend/.eslintrc.json](file://frontend/.eslintrc.json) +- [frontend/next.config.mjs](file://frontend/next.config.mjs) + + +## 目录 +1. [简介](#简介) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖分析](#依赖分析) +7. [性能考虑](#性能考虑) +8. [故障排除指南](#故障排除指南) +9. [结论](#结论) +10. [附录](#附录) + +## 简介 +本技术栈文档面向GEO项目的开发者与运维人员,系统梳理了后端(FastAPI + Python 3.9+)、前端(Next.js 14 + TypeScript)、数据库(PostgreSQL)、缓存(Redis)以及容器化部署方案。文档重点解释各技术选型的原因与优势,并结合仓库中的实际配置文件,给出版本要求、兼容性信息与最佳实践建议,帮助团队快速理解并高效迭代。 + +## 项目结构 +项目采用前后端分离的多服务架构,通过 Docker Compose 编排数据库、缓存、后端API与前端应用,形成可独立开发、测试与部署的完整环境。 + +```mermaid +graph TB +subgraph "本地开发环境" +FE["前端应用
Next.js 14 + TypeScript"] +BE["后端API
FastAPI + Python 3.11"] +DB["数据库
PostgreSQL 15"] +RC["缓存
Redis 7"] +end +FE --> |"HTTP/HTTPS"| BE +BE --> DB +BE --> RC +``` + +**图表来源** +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) +- [backend/Dockerfile:1-41](file://backend/Dockerfile#L1-L41) +- [frontend/Dockerfile:1-15](file://frontend/Dockerfile#L1-L15) + +**章节来源** +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) + +## 核心组件 +- 后端框架:FastAPI(高性能异步Web框架,自动生成OpenAPI文档,类型安全) +- 数据库:PostgreSQL(企业级关系型数据库,支持事务与复杂查询) +- 缓存:Redis(高性能键值存储,用于会话、限流与任务队列) +- 前端框架:Next.js 14(App Router架构,SSR/SSG支持,TypeScript原生支持) +- 异步ORM:SQLAlchemy 2.x(异步引擎与会话管理) +- 任务调度:APScheduler(基于事件循环的异步调度器) +- 浏览器自动化:Playwright(跨平台无头浏览器) +- 开发工具链:TypeScript、Tailwind CSS、ESLint、PostCSS、Tailwind CSS插件 + +**章节来源** +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) +- [frontend/package.json:1-40](file://frontend/package.json#L1-L40) +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) + +## 架构总览 +下图展示了从浏览器到后端API、数据库与缓存的整体交互流程,以及定时任务调度器在后台自动执行查询任务的机制。 + +```mermaid +graph TB +Browser["浏览器/移动端"] --> NextUI["Next.js 前端应用"] +NextUI --> FastAPI["FastAPI 后端"] +FastAPI --> SQLAlchemy["SQLAlchemy 异步ORM"] +SQLAlchemy --> PostgreSQL["PostgreSQL 数据库"] +FastAPI --> Redis["Redis 缓存"] +FastAPI --> APS["APScheduler 定时任务"] +APS --> Playwright["Playwright 浏览器自动化"] +Playwright --> ThirdParty["第三方平台接口"] +``` + +**图表来源** +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) + +## 详细组件分析 + +### 后端:FastAPI + Python 3.11 +- 应用入口与生命周期:通过 lifespan 钩子在启动时初始化模型与调度器,在关闭时优雅停机。 +- 中间件与路由:启用CORS以允许前端localhost访问;按模块注册认证、查询、引用数据与报告相关路由。 +- 健康检查:提供 /health 接口便于容器编排健康检查。 +- 版本与依赖:FastAPI≥0.109.0、Uvicorn标准变体、Pydantic≥2.0、SQLAlchemy≥2.0、Redis、APScheduler≥3.10、HTTPX、Playwright≥1.40、pytest-asyncio等。 + +```mermaid +sequenceDiagram +participant Client as "客户端" +participant API as "FastAPI 应用" +participant Router as "路由处理器" +participant DB as "异步数据库会话" +participant ORM as "SQLAlchemy 模型" +Client->>API : "HTTP 请求" +API->>Router : "分发到对应路由" +Router->>DB : "获取异步会话" +DB->>ORM : "执行查询/更新" +ORM-->>DB : "返回结果" +DB-->>Router : "提交/回滚" +Router-->>API : "序列化响应" +API-->>Client : "HTTP 响应" +``` + +**图表来源** +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) + +**章节来源** +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) + +### 数据库与ORM:PostgreSQL + SQLAlchemy 异步 +- 连接配置:通过配置类集中管理 DATABASE_URL,使用 asyncpg 作为异步驱动。 +- 会话管理:定义异步引擎与异步会话工厂,提供依赖注入式数据库会话生成器。 +- 模型基类:统一的 declarative_base,便于扩展业务模型。 + +```mermaid +flowchart TD +Start(["应用启动"]) --> LoadCfg["加载数据库配置"] +LoadCfg --> CreateEngine["创建异步引擎"] +CreateEngine --> SessionFactory["创建异步会话工厂"] +SessionFactory --> Request["请求到达"] +Request --> GetSession["依赖注入获取会话"] +GetSession --> ExecOp["执行ORM操作"] +ExecOp --> Commit["提交事务"] +Commit --> Close["关闭会话"] +Close --> End(["请求结束"]) +``` + +**图表来源** +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) + +**章节来源** +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) +- [backend/app/database.py:1-29](file://backend/app/database.py#L1-L29) + +### 缓存:Redis +- 配置:通过 REDIS_URL 指向 redis:6379/0,容器网络内可达。 +- 用途:会话存储、任务状态缓存、限流与临时数据缓存(结合业务场景使用)。 + +**章节来源** +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) + +### 任务调度:APScheduler 异步调度器 +- 触发周期:每小时检查一次到期的查询任务。 +- 查询逻辑:筛选状态为 active 且 next_query_at 小于等于当前时间的任务。 +- 执行流程:逐条调用引用引擎执行查询,异常记录日志但不中断整体流程。 +- 关闭流程:优雅关闭调度器与引擎资源。 + +```mermaid +flowchart TD +S(["启动调度器"]) --> AddJob["添加定时任务每小时"] +AddJob --> Loop{"事件循环存在?"} +Loop --> |是| RunTask["在当前事件循环创建任务"] +Loop --> |否| NewLoop["新建事件循环执行"] +RunTask --> Check["查询数据库:查找到期任务"] +NewLoop --> Check +Check --> Found{"找到任务?"} +Found --> |否| Wait["等待下次触发"] +Found --> |是| Exec["逐条执行查询"] +Exec --> Wait +Wait --> AddJob +``` + +**图表来源** +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) + +**章节来源** +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) + +### 前端:Next.js 14 + TypeScript +- 框架版本:Next.js 14.2.35,使用 App Router 架构组织页面与布局。 +- 类型系统:TypeScript 5,严格模式与路径别名配置,确保类型安全。 +- UI与样式:Tailwind CSS 3.4.1,配合 tailwindcss-animate 插件实现动画与主题扩展。 +- 工具链:ESLint 8(Next.js核心Web Vitals与TypeScript规则),PostCSS,开发脚本(dev/build/start/lint)。 +- 认证:NextAuth.js 4.24.14,集成OAuth与会话管理。 + +```mermaid +graph LR +Next["Next.js 14 App Router"] --> TS["TypeScript 5"] +Next --> Tailwind["Tailwind CSS 3.4.1"] +Tailwind --> Animate["tailwindcss-animate"] +Next --> ESLint["ESLint 8"] +Next --> NextAuth["NextAuth.js 4.24.14"] +Next --> Radix["Radix UI 组件库"] +Next --> Recharts["Recharts 图表库"] +``` + +**图表来源** +- [frontend/package.json:1-40](file://frontend/package.json#L1-L40) +- [frontend/tsconfig.json:1-27](file://frontend/tsconfig.json#L1-L27) +- [frontend/tailwind.config.ts:1-57](file://frontend/tailwind.config.ts#L1-L57) +- [frontend/.eslintrc.json:1-4](file://frontend/.eslintrc.json#L1-L4) + +**章节来源** +- [frontend/package.json:1-40](file://frontend/package.json#L1-L40) +- [frontend/tsconfig.json:1-27](file://frontend/tsconfig.json#L1-L27) +- [frontend/tailwind.config.ts:1-57](file://frontend/tailwind.config.ts#L1-L57) +- [frontend/.eslintrc.json:1-4](file://frontend/.eslintrc.json#L1-L4) +- [frontend/next.config.mjs:1-5](file://frontend/next.config.mjs#L1-L5) + +### 浏览器自动化:Playwright +- 作用:支持对第三方平台进行无头浏览器模拟,满足动态内容抓取与登录态维护。 +- 容器内安装:Dockerfile 中安装 Chromium 并预装 Playwright 依赖。 + +**章节来源** +- [backend/Dockerfile:1-41](file://backend/Dockerfile#L1-L41) +- [backend/requirements.txt:25-25](file://backend/requirements.txt#L25-L25) + +## 依赖分析 +- 后端依赖:FastAPI、Uvicorn、SQLAlchemy 2.x、asyncpg、Alembic、Pydantic 2.x、Pydantic Settings、python-jose、passlib、redis、apscheduler、httpx、python-dotenv、pytest、pytest-asyncio、aiosqlite、playwright。 +- 前端依赖:Next.js 14、React 18、NextAuth.js、Radix UI、Lucide React、Recharts、Tailwind CSS、Tailwind Merge、Tailwind CSS Animate、TypeScript、ESLint、PostCSS、Tailwind CSS。 + +```mermaid +graph TB +subgraph "后端" +F["FastAPI"] +S["SQLAlchemy 2.x"] +R["Redis"] +P["Playwright"] +A["APScheduler"] +end +subgraph "前端" +N["Next.js 14"] +T["TypeScript"] +TA["Tailwind CSS"] +E["ESLint"] +NA["NextAuth.js"] +end +N --> F +F --> S +F --> R +F --> A +F --> P +N --> TA +N --> E +N --> NA +N --> T +``` + +**图表来源** +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) +- [frontend/package.json:1-40](file://frontend/package.json#L1-L40) + +**章节来源** +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) +- [frontend/package.json:1-40](file://frontend/package.json#L1-L40) + +## 性能考虑 +- 异步优先:后端使用异步ORM与异步调度器,减少阻塞,提升并发处理能力。 +- 事件循环:调度器在当前事件循环或新建事件循环中执行任务,避免阻塞主事件循环。 +- 缓存策略:Redis用于热点数据与会话缓存,降低数据库压力;需结合业务设计合理的过期策略。 +- 数据库连接:异步引擎与会话池化配置,避免频繁建立/断开连接。 +- 前端优化:Next.js 14 App Router支持服务端渲染与静态生成,结合Tailwind CSS减少打包体积与样式开销。 + +## 故障排除指南 +- 健康检查失败 + - 现象:容器未就绪或重启频繁。 + - 排查:查看 docker-compose 中 db 与 redis 的 healthcheck 配置,确认端口映射与环境变量正确。 +- CORS 问题 + - 现象:前端跨域请求被拒绝。 + - 排查:确认后端 CORS 配置允许前端地址(默认允许 http://localhost:3000)。 +- 数据库连接失败 + - 现象:应用启动时报数据库连接错误。 + - 排查:核对 DATABASE_URL 是否指向容器内的 db 服务,确认网络连通与凭据正确。 +- Redis 连接失败 + - 现象:缓存不可用或任务调度异常。 + - 排查:核对 REDIS_URL 指向容器内的 redis 服务,确认端口映射与权限设置。 +- Playwright 依赖问题 + - 现象:容器内无法启动浏览器或报缺少系统库。 + - 排查:确认 Dockerfile 中已安装 Playwright 依赖与浏览器,必要时重建镜像。 +- 前端热更新异常 + - 现象:修改代码后未生效。 + - 排查:确认前端容器挂载了 node_modules 与源码目录,检查端口映射与命令是否正确。 + +**章节来源** +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) +- [backend/app/main.py:30-36](file://backend/app/main.py#L30-L36) +- [backend/app/config.py:7-8](file://backend/app/config.py#L7-L8) +- [backend/Dockerfile:6-25](file://backend/Dockerfile#L6-L25) + +## 结论 +GEO项目采用现代全栈技术栈:后端以 FastAPI 为核心,结合 SQLAlchemy 异步ORM与 APscheduler 实现高并发与可维护的任务调度;前端以 Next.js 14 为基础,配合 TypeScript、Tailwind CSS 与 NextAuth.js 构建现代化用户界面与认证体系;数据库与缓存分别采用 PostgreSQL 与 Redis,满足生产级数据持久化与缓存需求。通过 Docker Compose 实现一键编排,便于本地开发与部署。 + +## 附录 + +### 技术选型与优势 +- FastAPI + - 优势:高性能、自动生成OpenAPI文档、类型驱动的接口校验、异步支持。 + - 适用:高并发API、微服务、可观测性与可测试性要求高的场景。 +- Next.js 14 + TypeScript + - 优势:App Router架构、SSR/SSG、原生TypeScript支持、严格的类型约束。 + - 适用:现代Web应用、SEO友好、团队协作与质量保障。 +- PostgreSQL + - 优势:ACID事务、复杂查询、扩展性强、生态完善。 + - 适用:数据一致性要求高、业务逻辑复杂的场景。 +- Redis + - 优势:高性能KV存储、丰富的数据结构、发布订阅与事务支持。 + - 适用:会话存储、缓存、限流、任务队列。 +- SQLAlchemy 异步ORM + - 优势:类型安全、异步会话、灵活的查询DSL、与FastAPI依赖注入无缝集成。 + - 适用:需要强类型与异步IO的数据库访问层。 + +### 版本与兼容性 +- Python:后端使用 3.11(容器镜像),建议本地开发与CI保持一致。 +- Node.js:前端使用 20(容器镜像),建议本地开发与CI保持一致。 +- Next.js:14.2.35,使用 App Router 与TypeScript。 +- FastAPI:≥0.109.0,配合 Uvicorn 标准变体。 +- SQLAlchemy:≥2.0,异步引擎与会话。 +- Pydantic:≥2.0,配置与数据验证。 +- Redis:7-alpine,使用默认端口6379。 +- PostgreSQL:15-alpine,使用默认端口5432。 +- Playwright:≥1.40,容器内预装Chromium与依赖。 +- APScheduler:≥3.10,异步调度器。 + +### 容器化与部署要点 +- 多阶段思路:后端基于 slim 镜像,安装系统依赖与Playwright;前端基于 alpine 镜像,最小化依赖安装。 +- 端口映射:后端8000、前端3000、数据库5432、缓存6379。 +- 健康检查:db 与 redis 提供健康检查,后端提供 /health。 +- 开发体验:前端启用 --reload(开发命令),后端使用 uvicorn --reload。 + +**章节来源** +- [backend/Dockerfile:1-41](file://backend/Dockerfile#L1-L41) +- [frontend/Dockerfile:1-15](file://frontend/Dockerfile#L1-L15) +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/项目概述/核心功能.md b/.qoder/repowiki/zh/content/项目概述/核心功能.md new file mode 100644 index 0000000..48412cb --- /dev/null +++ b/.qoder/repowiki/zh/content/项目概述/核心功能.md @@ -0,0 +1,455 @@ +# 核心功能 + + +**本文档引用的文件** +- [backend/app/main.py](file://backend/app/main.py) +- [backend/app/api/auth.py](file://backend/app/api/auth.py) +- [backend/app/api/queries.py](file://backend/app/api/queries.py) +- [backend/app/api/citations.py](file://backend/app/api/citations.py) +- [backend/app/api/reports.py](file://backend/app/api/reports.py) +- [backend/app/services/auth.py](file://backend/app/services/auth.py) +- [backend/app/services/query.py](file://backend/app/services/query.py) +- [backend/app/services/citation.py](file://backend/app/services/citation.py) +- [backend/app/models/query.py](file://backend/app/models/query.py) +- [backend/app/models/citation_record.py](file://backend/app/models/citation_record.py) +- [backend/app/workers/scheduler.py](file://backend/app/workers/scheduler.py) +- [backend/app/workers/citation_engine.py](file://backend/app/workers/citation_engine.py) +- [backend/app/workers/platforms/base.py](file://backend/app/workers/platforms/base.py) +- [frontend/app/(dashboard)/dashboard/page.tsx](file://frontend/app/(dashboard)/dashboard/page.tsx) +- [frontend/components/charts/trend-chart.tsx](file://frontend/components/charts/trend-chart.tsx) + + +## 目录 +1. [简介](#简介) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖分析](#依赖分析) +7. [性能考虑](#性能考虑) +8. [故障排查指南](#故障排查指南) +9. [结论](#结论) +10. [附录](#附录) + +## 简介 +本文件为 GEO 平台的核心功能概览,聚焦以下关键能力: +- 用户认证与权限管理:基于邮箱/密码注册登录、JWT 访问令牌签发与校验、会话保护接口。 +- 智能查询任务管理:查询词创建、更新、删除、分页查询;按日/周/月频率自动调度;手动触发即时查询。 +- 品牌引用检测引擎:多阶段匹配(精确/别名/模糊)、置信度评分、竞争品牌识别、上下文片段抽取。 +- 多 AI 平台数据集成:抽象适配器模式对接不同大模型平台,统一查询与结果处理。 +- 数据分析与可视化:统计指标(总查询/引用次数、引用率、平均位置)、平台对比、30 天趋势折线图。 +- 报告导出:CSV 导出引用记录,便于离线分析与归档。 + +这些功能围绕“查询—检测—统计—可视—导出”的闭环展开,既满足管理员对系统运行与任务调度的掌控,也服务于研究人员对品牌监测与趋势分析的需求。 + +## 项目结构 +后端采用 FastAPI + SQLAlchemy 异步 ORM,按领域划分 API、服务、模型与工作器;前端使用 Next.js + React,通过自定义 API 客户端与后端交互;数据库为 PostgreSQL。 + +```mermaid +graph TB +subgraph "后端" +A["FastAPI 应用
app/main.py"] +B["API 层
auth/queries/citations/reports"] +C["服务层
auth/query/citation"] +D["模型层
Query/CitationRecord"] +E["工作器
Scheduler/CitationEngine/Platforms"] +end +subgraph "前端" +F["仪表盘页面
dashboard/page.tsx"] +G["趋势图表组件
trend-chart.tsx"] +end +A --> B +B --> C +C --> D +C --> E +F --> G +F --> B +``` + +**图表来源** +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/app/api/queries.py:1-86](file://backend/app/api/queries.py#L1-L86) +- [backend/app/api/citations.py:1-78](file://backend/app/api/citations.py#L1-L78) +- [backend/app/api/reports.py:1-47](file://backend/app/api/reports.py#L1-L47) +- [backend/app/services/auth.py:1-69](file://backend/app/services/auth.py#L1-L69) +- [backend/app/services/query.py:1-130](file://backend/app/services/query.py#L1-L130) +- [backend/app/services/citation.py:1-269](file://backend/app/services/citation.py#L1-L269) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [backend/app/models/citation_record.py:1-42](file://backend/app/models/citation_record.py#L1-L42) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) +- [frontend/app/(dashboard)/dashboard/page.tsx:1-156](file://frontend/app/(dashboard)/dashboard/page.tsx#L1-L156) +- [frontend/components/charts/trend-chart.tsx:1-60](file://frontend/components/charts/trend-chart.tsx#L1-L60) + +**章节来源** +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) + +## 核心组件 +- 认证与权限 + - 注册/登录:邮箱唯一性校验、密码哈希、JWT 签发;当前用户信息读取。 + - 权限边界:所有业务接口均通过当前用户上下文进行资源归属校验(查询、引用、统计、导出)。 +- 查询任务管理 + - 查询 CRUD:分页列表、创建、读取、更新、删除;创建时校验用户配额上限;更新时可重算下次执行时间。 + - 自动调度:每小时扫描到期查询,自动触发引用检测;手动触发即时查询。 +- 引用检测引擎 + - 多阶段匹配:精确命中 → 别名命中 → 模糊相似度阈值;返回是否引用、置信度、位置、上下文。 + - 竞争品牌:基于预设行业品牌库识别竞品。 + - 结果持久化:生成引用记录,包含平台、是否引用、位置、文本、竞品集合、原始响应。 +- 多 AI 平台集成 + - 适配器基类定义统一接口;内置“文心”“Kimi”适配器;未来可扩展更多平台。 +- 数据分析与可视化 + - 统计聚合:总查询/引用数、引用率、平均位置、按平台汇总、30 天趋势。 + - 前端展示:仪表盘卡片与趋势折线图组件。 +- 报告导出 + - 支持 CSV 下载,包含日期、平台、是否引用、引用位置、引用文本、竞品品牌等字段。 + +**章节来源** +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/app/services/auth.py:1-69](file://backend/app/services/auth.py#L1-L69) +- [backend/app/api/queries.py:1-86](file://backend/app/api/queries.py#L1-L86) +- [backend/app/services/query.py:1-130](file://backend/app/services/query.py#L1-L130) +- [backend/app/api/citations.py:1-78](file://backend/app/api/citations.py#L1-L78) +- [backend/app/services/citation.py:1-269](file://backend/app/services/citation.py#L1-L269) +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) +- [backend/app/workers/platforms/base.py:1-18](file://backend/app/workers/platforms/base.py#L1-L18) +- [frontend/app/(dashboard)/dashboard/page.tsx:1-156](file://frontend/app/(dashboard)/dashboard/page.tsx#L1-L156) +- [frontend/components/charts/trend-chart.tsx:1-60](file://frontend/components/charts/trend-chart.tsx#L1-L60) +- [backend/app/api/reports.py:1-47](file://backend/app/api/reports.py#L1-L47) + +## 架构总览 +下图展示从用户请求到数据落库与可视化的整体流程,以及定时调度与即时查询的协同机制。 + +```mermaid +sequenceDiagram +participant U as "用户" +participant FE as "前端" +participant API as "后端API" +participant S as "服务层" +participant DB as "数据库" +participant W as "工作器" +participant CE as "引用检测引擎" +participant P as "AI平台适配器" +U->>FE : 登录/访问仪表盘 +FE->>API : 获取统计/查询/引用/导出 +API->>S : 参数校验与业务处理 +S->>DB : 读写查询/引用/任务 +Note over S,DB : 权限校验:仅允许访问本人资源 +API->>W : 触发/查询任务 +W->>CE : 执行查询 +CE->>P : 平台查询 +P-->>CE : 原始响应 +CE->>S : 写入引用记录 +S->>DB : 持久化 +DB-->>S : 成功 +S-->>API : 结果 +API-->>FE : 响应数据/流式下载 +``` + +**图表来源** +- [backend/app/main.py:13-21](file://backend/app/main.py#L13-L21) +- [backend/app/api/citations.py:59-77](file://backend/app/api/citations.py#L59-L77) +- [backend/app/workers/scheduler.py:51-84](file://backend/app/workers/scheduler.py#L51-L84) +- [backend/app/workers/citation_engine.py:159-234](file://backend/app/workers/citation_engine.py#L159-L234) +- [backend/app/services/citation.py:204-234](file://backend/app/services/citation.py#L204-L234) + +## 详细组件分析 + +### 用户认证与权限管理 +- 功能要点 + - 注册:邮箱唯一性检查、密码加密存储、返回用户信息。 + - 登录:邮箱+密码验证、签发 JWT(含过期时间),返回用户与令牌。 + - 当前用户:受保护路由读取当前用户上下文。 +- 关键价值 + - 保障数据隔离:所有业务接口均以当前用户为准进行资源归属校验。 + - 易于扩展:JWT 可用于跨域与第三方集成。 +- 典型流程 + - 注册 → 登录 → 携带令牌访问受保护接口 → 获取/创建查询 → 触发查询 → 查看统计/导出报告。 + +```mermaid +sequenceDiagram +participant U as "用户" +participant API as "认证API" +participant S as "认证服务" +participant DB as "数据库" +U->>API : POST /api/v1/auth/register +API->>S : 注册逻辑 +S->>DB : 检查邮箱/保存用户 +DB-->>S : 成功 +S-->>API : 用户对象 +U->>API : POST /api/v1/auth/login +API->>S : 验证邮箱/密码 +S->>DB : 查询用户 +DB-->>S : 用户信息 +S-->>API : JWT令牌 +API-->>U : {access_token,user} +``` + +**图表来源** +- [backend/app/api/auth.py:13-37](file://backend/app/api/auth.py#L13-L37) +- [backend/app/services/auth.py:37-69](file://backend/app/services/auth.py#L37-L69) + +**章节来源** +- [backend/app/api/auth.py:1-43](file://backend/app/api/auth.py#L1-L43) +- [backend/app/services/auth.py:1-69](file://backend/app/services/auth.py#L1-L69) + +### 智能查询任务管理 +- 功能要点 + - 查询 CRUD:分页列表、创建(校验配额与频率)、读取、更新(频率变更时重算下次执行时间)、删除。 + - 自动调度:每小时扫描到期查询,调用引用检测引擎执行;更新查询最近与下次执行时间。 + - 即时查询:手动触发,为每个配置平台创建任务并入队。 +- 核心价值 + - 减少人工干预:按计划自动抓取与检测,提升研究效率。 + - 灵活控制:支持日/周/月频率与手动触发,兼顾实时性与成本。 +- 典型场景 + - 研究员创建查询(关键词、目标品牌、平台、频率),系统按时自动执行;也可随时“立即执行”。 + +```mermaid +flowchart TD +Start(["创建查询"]) --> CheckLimit["检查用户配额"] +CheckLimit --> |未超限| CalcNext["按频率计算下次执行时间"] +CheckLimit --> |超限| Deny["拒绝创建"] +CalcNext --> SaveQ["保存查询"] +SaveQ --> Schedule["等待调度器扫描"] +Schedule --> Due{"到期?"} +Due --> |否| Wait["继续等待"] +Due --> |是| Trigger["触发引用检测"] +Trigger --> UpdateTime["更新最近/下次执行时间"] +UpdateTime --> Done(["完成一轮周期"]) +``` + +**图表来源** +- [backend/app/services/query.py:45-81](file://backend/app/services/query.py#L45-L81) +- [backend/app/workers/scheduler.py:51-84](file://backend/app/workers/scheduler.py#L51-L84) +- [backend/app/workers/citation_engine.py:291-300](file://backend/app/workers/citation_engine.py#L291-L300) + +**章节来源** +- [backend/app/api/queries.py:1-86](file://backend/app/api/queries.py#L1-L86) +- [backend/app/services/query.py:1-130](file://backend/app/services/query.py#L1-L130) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) + +### 品牌引用检测引擎 +- 功能要点 + - 品牌匹配器:精确命中(置信度1.0)→ 别名命中(置信度0.9)→ 模糊相似度(阈值0.4);返回是否引用、置信度、位置、上下文。 + - 竞争品牌检测:基于预设行业品牌库识别其他品牌。 + - 结果持久化:记录平台、是否引用、位置、文本、竞品、原始响应。 +- 核心价值 + - 置信度评分:帮助判断引用可靠性;模糊匹配提供兜底发现。 + - 上下文定位:快速定位品牌在原文中的首次出现段落,便于人工复核。 +- 典型场景 + - 文本中提及“XX品牌”,匹配器判定为“别名命中”,置信度0.9,并返回首次出现段落片段。 + +```mermaid +classDiagram +class CitationEngine { ++execute_query(query, db) CitationRecord[] ++execute_single_platform(keyword, platform, target_brand, aliases) dict +} +class BrandMatcher { ++match(text) dict +} +class CompetitorDetector { ++detect(text, target_brand) str[] +} +class BasePlatformAdapter { +<> ++query(keyword) str +} +CitationEngine --> BrandMatcher : "使用" +CitationEngine --> CompetitorDetector : "使用" +CitationEngine --> BasePlatformAdapter : "委托查询" +``` + +**图表来源** +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) +- [backend/app/workers/platforms/base.py:1-18](file://backend/app/workers/platforms/base.py#L1-L18) + +**章节来源** +- [backend/app/workers/citation_engine.py:19-120](file://backend/app/workers/citation_engine.py#L19-L120) +- [backend/app/workers/citation_engine.py:122-146](file://backend/app/workers/citation_engine.py#L122-L146) +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) + +### 多 AI 平台数据集成 +- 功能要点 + - 适配器基类定义统一接口(平台名、URL、查询方法)。 + - 内置“文心”“Kimi”适配器;引擎按查询配置的平台列表逐一执行。 +- 核心价值 + - 解耦平台差异:统一调用入口,便于扩展更多平台。 + - 可观测性:每个平台独立任务状态(pending/running/success/failed)。 +- 典型场景 + - 查询配置包含“wenxin,kimi”,引擎为两者分别创建任务并行执行,最终汇总结果。 + +```mermaid +sequenceDiagram +participant CE as "CitationEngine" +participant Q as "Query" +participant T as "QueryTask" +participant A as "平台适配器" +CE->>Q : 读取平台列表 +loop 遍历平台 +CE->>T : 获取/创建任务 +CE->>A : 调用 query(keyword) +A-->>CE : 返回原始响应 +CE->>CE : 品牌匹配/竞品检测 +CE->>T : 更新任务状态 +end +``` + +**图表来源** +- [backend/app/workers/citation_engine.py:159-234](file://backend/app/workers/citation_engine.py#L159-L234) +- [backend/app/workers/platforms/base.py:10-17](file://backend/app/workers/platforms/base.py#L10-L17) + +**章节来源** +- [backend/app/workers/platforms/base.py:1-18](file://backend/app/workers/platforms/base.py#L1-L18) +- [backend/app/workers/citation_engine.py:151-157](file://backend/app/workers/citation_engine.py#L151-L157) + +### 数据分析与可视化 +- 功能要点 + - 统计接口:总查询/引用数、引用率、平均位置、按平台汇总、30 天趋势(按自然周聚合)。 + - 前端仪表盘:卡片展示核心指标;趋势折线图展示过去 30 天每周引用次数。 +- 核心价值 + - 快速洞察:总览指标帮助评估监测效果与变化趋势。 + - 易用性:图表直观呈现,降低阅读成本。 +- 典型场景 + - 研究人员查看“过去30天引用趋势”,发现某周显著上升,结合上下文进一步分析。 + +```mermaid +sequenceDiagram +participant FE as "前端" +participant API as "统计API" +participant S as "统计服务" +participant DB as "数据库" +FE->>API : GET /api/v1/citations/stats +API->>S : 统计聚合 +S->>DB : 聚合查询/分组统计 +DB-->>S : 结果集 +S-->>API : {total,rate,avg,by_platform,trend} +API-->>FE : 返回JSON +FE->>FE : 渲染卡片与趋势图 +``` + +**图表来源** +- [frontend/app/(dashboard)/dashboard/page.tsx:20-155](file://frontend/app/(dashboard)/dashboard/page.tsx#L20-L155) +- [frontend/components/charts/trend-chart.tsx:22-59](file://frontend/components/charts/trend-chart.tsx#L22-L59) +- [backend/app/api/citations.py:49-56](file://backend/app/api/citations.py#L49-L56) +- [backend/app/services/citation.py:76-201](file://backend/app/services/citation.py#L76-L201) + +**章节来源** +- [frontend/app/(dashboard)/dashboard/page.tsx:1-156](file://frontend/app/(dashboard)/dashboard/page.tsx#L1-L156) +- [frontend/components/charts/trend-chart.tsx:1-60](file://frontend/components/charts/trend-chart.tsx#L1-L60) +- [backend/app/api/citations.py:1-78](file://backend/app/api/citations.py#L1-L78) +- [backend/app/services/citation.py:1-269](file://backend/app/services/citation.py#L1-L269) + +### 报告导出 +- 功能要点 + - 支持 CSV 导出指定查询的所有引用记录,包含日期、平台、是否引用、引用位置、引用文本、竞品品牌。 + - 流式响应,避免大文件内存压力。 +- 核心价值 + - 离线分析:便于导入 Excel/BI 工具做深度分析。 + - 合规归档:结构化导出满足审计与存档需求。 +- 典型场景 + - 研究员导出某周的全部引用记录,用于撰写专题报告。 + +```mermaid +sequenceDiagram +participant FE as "前端" +participant API as "导出API" +participant S as "导出服务" +participant DB as "数据库" +FE->>API : GET /api/v1/reports/export/csv?query_id=... +API->>S : 导出CSV +S->>DB : 查询引用记录 +DB-->>S : 记录集 +S-->>API : CSV字符串 +API-->>FE : 流式响应(Attachment) +``` + +**图表来源** +- [backend/app/api/reports.py:16-46](file://backend/app/api/reports.py#L16-L46) +- [backend/app/services/citation.py:237-268](file://backend/app/services/citation.py#L237-L268) + +**章节来源** +- [backend/app/api/reports.py:1-47](file://backend/app/api/reports.py#L1-L47) +- [backend/app/services/citation.py:237-268](file://backend/app/services/citation.py#L237-L268) + +## 依赖分析 +- 组件耦合 + - API 层仅负责参数解析与鉴权,业务逻辑集中在服务层,降低控制器复杂度。 + - 引擎与平台适配器通过抽象接口解耦,便于替换与扩展。 + - 调度器与引擎通过 ORM 与任务表协作,避免直接耦合业务数据。 +- 外部依赖 + - FastAPI/SQLAlchemy:Web 框架与 ORM。 + - APScheduler:异步定时任务调度。 + - Recharts:前端图表渲染。 +- 潜在风险 + - 平台适配器异常需隔离,避免影响其他平台任务。 + - 大量并发查询可能带来数据库与外部平台压力,建议限流与重试策略。 + +```mermaid +graph LR +API["API层"] --> SVC["服务层"] +SVC --> MODEL["模型层"] +SVC --> WORKER["工作器"] +WORKER --> ADAPTER["平台适配器"] +FE["前端"] --> API +``` + +**图表来源** +- [backend/app/main.py:38-42](file://backend/app/main.py#L38-L42) +- [backend/app/workers/citation_engine.py:151-157](file://backend/app/workers/citation_engine.py#L151-L157) + +**章节来源** +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) + +## 性能考虑 +- 数据库 + - 查询索引:查询与引用记录表均建立常用过滤字段索引,减少扫描开销。 + - 分页与聚合:统计接口使用分组与聚合,避免一次性拉取全量数据。 +- 引擎与平台 + - 并行执行:同一查询的不同平台可并行处理,缩短总耗时。 + - 错误隔离:单平台失败不影响其他平台,保证整体可用性。 +- 前端 + - 图表懒加载与响应式容器,提升大屏体验。 + - 导出采用流式响应,避免内存峰值。 + +[本节为通用指导,无需具体文件分析] + +## 故障排查指南 +- 认证问题 + - 注册失败:邮箱已被注册;检查重复提交或换用其他邮箱。 + - 登录失败:邮箱或密码错误;确认凭据正确与网络可达。 +- 查询任务 + - 创建被拒:超出配额;联系管理员提升限额或清理历史查询。 + - 无法执行:查询状态非“active”或未配置平台;检查状态与平台列表。 + - 即时查询无响应:平台适配器异常或网络超时;查看任务状态与错误信息。 +- 统计与导出 + - 统计为空:可能因筛选条件导致无数据;尝试放宽时间范围或移除查询筛选。 + - 导出失败:查询不存在或无权限;确认 query_id 与登录态。 +- 调度器 + - 未触发:检查调度器是否启动、时区设置、下次执行时间是否已到达。 + +**章节来源** +- [backend/app/services/auth.py:37-69](file://backend/app/services/auth.py#L37-L69) +- [backend/app/services/query.py:45-81](file://backend/app/services/query.py#L45-L81) +- [backend/app/services/citation.py:204-234](file://backend/app/services/citation.py#L204-L234) +- [backend/app/api/reports.py:16-46](file://backend/app/api/reports.py#L16-L46) +- [backend/app/workers/scheduler.py:30-40](file://backend/app/workers/scheduler.py#L30-L40) + +## 结论 +GEO 平台以“查询—检测—统计—可视—导出”为主线,构建了从自动化采集到深度分析的完整链路。通过严格的权限控制、可扩展的平台适配器、稳健的定时调度与清晰的可视化输出,既能满足管理员对系统运行的掌控,也能为研究人员提供高效、可靠的品牌监测工具。建议后续在平台适配器层面引入重试与熔断、在数据库侧增加慢查询监控与索引优化,持续提升稳定性与性能。 + +[本节为总结性内容,无需具体文件分析] + +## 附录 +- 典型使用流程(管理员) + - 新建用户/分配配额 → 配置平台密钥 → 监控调度器运行 → 查看任务状态与错误日志 → 调整频率策略。 +- 典型使用流程(研究人员) + - 登录 → 创建查询(关键词/目标品牌/平台/频率) → 查看仪表盘趋势 → 导出报告 → 深度分析与汇报。 +- 关键接口路径参考 + - 认证:POST /api/v1/auth/register, POST /api/v1/auth/login, GET /api/v1/auth/me + - 查询:GET/POST/GET/PATCH/DELETE /api/v1/queries + - 引用:GET /api/v1/citations, GET /api/v1/citations/stats, POST /api/v1/queries/{query_id}/run-now + - 报告:GET /api/v1/reports/export/csv + +[本节为概览性内容,无需具体文件分析] \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/项目概述/系统架构.md b/.qoder/repowiki/zh/content/项目概述/系统架构.md new file mode 100644 index 0000000..944de6a --- /dev/null +++ b/.qoder/repowiki/zh/content/项目概述/系统架构.md @@ -0,0 +1,366 @@ +# 系统架构 + + +**本文档引用的文件** +- [docker-compose.yml](file://docker-compose.yml) +- [backend/Dockerfile](file://backend/Dockerfile) +- [frontend/Dockerfile](file://frontend/Dockerfile) +- [backend/app/main.py](file://backend/app/main.py) +- [backend/app/api/queries.py](file://backend/app/api/queries.py) +- [backend/app/services/query.py](file://backend/app/services/query.py) +- [backend/app/models/query.py](file://backend/app/models/query.py) +- [backend/app/workers/citation_engine.py](file://backend/app/workers/citation_engine.py) +- [backend/app/workers/platforms/base.py](file://backend/app/workers/platforms/base.py) +- [backend/app/workers/platforms/kimi.py](file://backend/app/workers/platforms/kimi.py) +- [backend/app/workers/platforms/wenxin.py](file://backend/app/workers/platforms/wenxin.py) +- [frontend/lib/api.ts](file://frontend/lib/api.ts) +- [frontend/components/providers.tsx](file://frontend/components/providers.tsx) +- [frontend/app/layout.tsx](file://frontend/app/layout.tsx) + + +## 目录 +1. [简介](#简介) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖分析](#依赖分析) +7. [性能考虑](#性能考虑) +8. [故障排查指南](#故障排查指南) +9. [结论](#结论) +10. [附录](#附录) + +## 简介 +本系统是一个学术查询与引用管理平台,采用前后端分离架构:前端使用 Next.js 构建用户界面,后端基于 FastAPI 提供 REST API,并通过 SQLAlchemy ORM 访问 PostgreSQL 数据库;AI 平台查询由后端工作器通过适配器模式对接不同平台(Kimi、文心一言),并通过 Redis 缓存与队列进行异步调度。系统通过 Docker Compose 将数据库、缓存、后端与前端容器化编排,便于开发与部署。 + +## 项目结构 +系统分为四层: +- 表现层(前端 Next.js):负责用户界面渲染与 API 调用 +- 业务逻辑层(后端 FastAPI):提供 REST 接口、业务服务与调度 +- 数据访问层(SQLAlchemy ORM):模型定义与数据库交互 +- 基础设施层(Docker 容器):PostgreSQL、Redis、后端与前端容器 + +```mermaid +graph TB +subgraph "表现层前端 Next.js" +FE_APP["Next.js 应用
路由与页面"] +FE_LIB_API["API 客户端
frontend/lib/api.ts"] +FE_PROVIDERS["会话提供者
frontend/components/providers.tsx"] +end +subgraph "业务逻辑层后端 FastAPI" +BE_MAIN["应用入口
backend/app/main.py"] +BE_ROUTERS["API 路由
backend/app/api/*"] +BE_SERVICES["业务服务
backend/app/services/*"] +BE_WORKERS["工作器与适配器
backend/app/workers/*"] +end +subgraph "数据访问层SQLAlchemy ORM" +BE_MODELS["ORM 模型
backend/app/models/*"] +end +subgraph "基础设施层Docker 容器" +DC_DB["PostgreSQL 容器"] +DC_REDIS["Redis 容器"] +DC_BE["后端容器"] +DC_FE["前端容器"] +end +FE_APP --> FE_LIB_API +FE_LIB_API --> BE_MAIN +BE_MAIN --> BE_ROUTERS +BE_ROUTERS --> BE_SERVICES +BE_SERVICES --> BE_MODELS +BE_WORKERS --> BE_MODELS +BE_MAIN --> DC_BE +BE_MODELS --> DC_DB +BE_WORKERS --> DC_REDIS +FE_APP --> DC_FE +``` + +图表来源 +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/api/queries.py:1-86](file://backend/app/api/queries.py#L1-L86) +- [backend/app/services/query.py:1-130](file://backend/app/services/query.py#L1-L130) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) +- [frontend/components/providers.tsx:1-9](file://frontend/components/providers.tsx#L1-L9) + +章节来源 +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [frontend/app/layout.tsx:1-37](file://frontend/app/layout.tsx#L1-L37) + +## 核心组件 +- 前端 Next.js:提供登录注册、查询管理、引用统计与报告导出等功能页面,通过统一 API 客户端发起请求。 +- 后端 FastAPI:提供认证、查询词、引用数据、报告导出等接口;启动时初始化调度器并挂载 CORS 中间件。 +- 工作器与适配器:以适配器模式封装不同 AI 平台(Kimi、文心一言)的查询行为,统一对外接口。 +- ORM 模型:定义查询词、引用记录、任务等实体及索引,支撑业务数据持久化。 +- 基础设施:PostgreSQL 存储业务数据,Redis 支持缓存与队列,Docker Compose 统一编排。 + +章节来源 +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) + +## 架构总览 +系统采用分层解耦与容器化部署,前端通过 REST API 与后端交互,后端通过工作器调用 AI 平台,数据库与缓存分别承担数据持久化与临时状态管理。下图展示从用户请求到 AI 平台查询再到结果返回的完整链路。 + +```mermaid +sequenceDiagram +participant U as "用户" +participant FE as "前端 Next.js" +participant API as "后端 API 路由" +participant SVC as "业务服务" +participant ENG as "引用检测引擎" +participant ADP as "AI 平台适配器" +participant DB as "PostgreSQL" +U->>FE : "提交查询/触发查询" +FE->>API : "HTTP 请求带鉴权" +API->>SVC : "调用业务方法" +SVC->>ENG : "执行引用检测" +loop "遍历平台" +ENG->>ADP : "query(keyword)" +ADP-->>ENG : "返回平台响应文本" +end +ENG->>DB : "写入引用记录/更新任务状态" +DB-->>ENG : "确认写入" +ENG-->>SVC : "返回检测结果" +SVC-->>API : "组装响应" +API-->>FE : "返回结果" +FE-->>U : "展示引用统计/报告" +``` + +图表来源 +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) +- [backend/app/api/queries.py:1-86](file://backend/app/api/queries.py#L1-L86) +- [backend/app/services/query.py:1-130](file://backend/app/services/query.py#L1-L130) +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) +- [backend/app/workers/platforms/kimi.py:1-206](file://backend/app/workers/platforms/kimi.py#L1-L206) +- [backend/app/workers/platforms/wenxin.py:1-205](file://backend/app/workers/platforms/wenxin.py#L1-L205) + +## 详细组件分析 + +### 前端组件分析 +- API 客户端:集中封装鉴权头、错误处理与基础路径,统一暴露认证、查询、引用、报告等接口。 +- 会话提供者:为 NextAuth 集成提供会话上下文,保障页面与 API 调用的鉴权一致性。 +- 布局与字体:全局样式与字体配置,保证一致的视觉体验。 + +```mermaid +flowchart TD +Start(["页面发起请求"]) --> BuildHeaders["构建请求头
含 Authorization"] +BuildHeaders --> FetchAPI["调用 fetchWithAuth"] +FetchAPI --> RespOK{"响应成功?"} +RespOK --> |是| ParseJSON["解析 JSON 响应"] +RespOK --> |否| HandleError["抛出错误包含 HTTP 状态"] +ParseJSON --> ReturnData["返回数据给页面"] +HandleError --> ThrowErr["上抛错误供 UI 处理"] +``` + +图表来源 +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) + +章节来源 +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) +- [frontend/components/providers.tsx:1-9](file://frontend/components/providers.tsx#L1-L9) +- [frontend/app/layout.tsx:1-37](file://frontend/app/layout.tsx#L1-L37) + +### 后端组件分析 +- 应用入口:注册路由、启用 CORS、启动/关闭调度器生命周期钩子。 +- API 路由:提供认证、查询词、引用数据、报告导出等接口,统一前缀与标签。 +- 业务服务:封装查询词的增删改查与配额校验、频率与下次查询时间计算。 +- ORM 模型:定义查询词实体及其索引,关联用户、引用记录与任务。 + +```mermaid +classDiagram +class Query { ++UUID id ++UUID user_id ++string keyword ++string target_brand ++list brand_aliases ++list platforms ++string frequency ++string status ++datetime last_queried_at ++datetime next_query_at ++datetime created_at ++datetime updated_at +} +class User { ++UUID id ++string name ++string email ++int max_queries +} +class CitationRecord { ++UUID id ++UUID query_id ++string platform ++bool cited ++int citation_position ++string citation_text ++list competitor_brands ++string raw_response +} +class QueryTask { ++UUID id ++UUID query_id ++string platform ++string status ++datetime started_at ++datetime completed_at ++string error_message +} +User "1" --> "many" Query : "拥有" +Query "1" --> "many" CitationRecord : "生成" +Query "1" --> "many" QueryTask : "驱动" +``` + +图表来源 +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) + +章节来源 +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/api/queries.py:1-86](file://backend/app/api/queries.py#L1-L86) +- [backend/app/services/query.py:1-130](file://backend/app/services/query.py#L1-L130) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) + +### 工作器与适配器分析 +- 引用检测引擎:根据查询词与目标品牌,遍历平台列表,调用对应适配器获取响应,执行品牌匹配与竞争品牌检测,生成引用记录并更新任务状态与查询时间。 +- 适配器基类:定义平台名称、URL 与抽象查询方法,统一资源清理接口。 +- Kimi 适配器:通过 Playwright 自动化模拟用户输入与提交,等待回复稳定后提取文本。 +- 文心一言适配器:同 Kimi,但针对文心一言页面结构进行适配。 + +```mermaid +classDiagram +class BasePlatformAdapter { +<> ++string platform_name ++string platform_url ++query(keyword) str ++close() +} +class KimiAdapter { ++platform_name = "kimi" ++platform_url = "https : //kimi.moonshot.cn" ++query(keyword) str ++close() +} +class WenxinAdapter { ++platform_name = "wenxin" ++platform_url = "https : //yiyan.baidu.com" ++query(keyword) str ++close() +} +class CitationEngine { ++execute_query(query, db) CitationRecord[] ++execute_single_platform(keyword, platform, target_brand, brand_aliases) dict ++close() +} +BasePlatformAdapter <|-- KimiAdapter +BasePlatformAdapter <|-- WenxinAdapter +CitationEngine --> BasePlatformAdapter : "使用" +``` + +图表来源 +- [backend/app/workers/platforms/base.py:1-18](file://backend/app/workers/platforms/base.py#L1-L18) +- [backend/app/workers/platforms/kimi.py:1-206](file://backend/app/workers/platforms/kimi.py#L1-L206) +- [backend/app/workers/platforms/wenxin.py:1-205](file://backend/app/workers/platforms/wenxin.py#L1-L205) +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) + +章节来源 +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) +- [backend/app/workers/platforms/base.py:1-18](file://backend/app/workers/platforms/base.py#L1-L18) +- [backend/app/workers/platforms/kimi.py:1-206](file://backend/app/workers/platforms/kimi.py#L1-L206) +- [backend/app/workers/platforms/wenxin.py:1-205](file://backend/app/workers/platforms/wenxin.py#L1-L205) + +### 数据流与处理流程 +- 用户在前端提交查询或手动触发查询。 +- 前端通过 API 客户端调用后端接口,携带鉴权信息。 +- 后端路由将请求委派给业务服务,服务层执行权限与配额检查、频率计算等逻辑。 +- 引用检测引擎根据平台列表逐个调用适配器,等待回复稳定后进行品牌匹配与竞争品牌检测。 +- 结果写入数据库并更新任务状态,前端轮询或接收通知后刷新页面。 + +```mermaid +flowchart TD +A["用户提交查询"] --> B["前端 API 客户端"] +B --> C["后端 API 路由"] +C --> D["业务服务权限/配额/频率"] +D --> E["引用检测引擎"] +E --> F["Kimi 适配器"] +E --> G["文心一言适配器"] +F --> H["品牌匹配/竞争品牌检测"] +G --> H +H --> I["写入引用记录/更新任务状态"] +I --> J["返回结果给前端"] +J --> K["展示统计/报告"] +``` + +图表来源 +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) +- [backend/app/api/queries.py:1-86](file://backend/app/api/queries.py#L1-L86) +- [backend/app/services/query.py:1-130](file://backend/app/services/query.py#L1-L130) +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) +- [backend/app/workers/platforms/kimi.py:1-206](file://backend/app/workers/platforms/kimi.py#L1-L206) +- [backend/app/workers/platforms/wenxin.py:1-205](file://backend/app/workers/platforms/wenxin.py#L1-L205) + +## 依赖分析 +- 组件内聚与耦合:后端 API 路由仅负责参数解析与委派,业务服务与模型解耦良好;工作器通过适配器模式与具体平台解耦。 +- 外部依赖:后端依赖 PostgreSQL 与 Redis;前端依赖 Next.js 生态与 NextAuth;工作器依赖 Playwright 浏览器自动化。 +- 容器编排:Compose 文件定义数据库、缓存、后端与前端服务,设置健康检查与端口映射,后端依赖数据库与缓存健康。 + +```mermaid +graph TB +FE["前端"] --> BE["后端"] +BE --> DB["PostgreSQL"] +BE --> RD["Redis"] +BE --> PW["Playwright 浏览器"] +DC["Docker Compose"] --> DB +DC --> RD +DC --> BE +DC --> FE +``` + +图表来源 +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) +- [backend/Dockerfile:1-41](file://backend/Dockerfile#L1-L41) +- [frontend/Dockerfile:1-15](file://frontend/Dockerfile#L1-L15) + +章节来源 +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) +- [backend/Dockerfile:1-41](file://backend/Dockerfile#L1-L41) +- [frontend/Dockerfile:1-15](file://frontend/Dockerfile#L1-L15) + +## 性能考虑 +- 异步与并发:后端使用异步数据库会话与异步适配器调用,提升 I/O 密集场景下的吞吐。 +- 重试与指数退避:适配器对平台请求进行多轮重试与指数退避,降低偶发网络波动影响。 +- 回复稳定性检测:等待 AI 回复文本稳定后再提取,减少因动态渲染导致的截断误差。 +- 索引优化:查询词模型建立多字段索引,加速分页与过滤查询。 +- 缓存与队列:建议结合 Redis 实现任务队列与结果缓存,进一步降低重复查询成本。 + +## 故障排查指南 +- 健康检查:Compose 为数据库与缓存配置健康检查,若服务未就绪,前端将无法连接后端。 +- Playwright 依赖:适配器需使用 Playwright 安装 Chromium,若启动失败请先安装浏览器依赖。 +- CORS 限制:后端仅允许本地前端域名访问,跨域请求需调整中间件配置。 +- 错误处理:前端客户端对非 2xx 响应抛出错误,后端路由对未找到与权限问题返回明确状态码。 + +章节来源 +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/workers/platforms/kimi.py:1-206](file://backend/app/workers/platforms/kimi.py#L1-L206) +- [backend/app/workers/platforms/wenxin.py:1-205](file://backend/app/workers/platforms/wenxin.py#L1-L205) +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) + +## 结论 +该系统通过清晰的分层架构与容器化部署,实现了从前端到后端、再到 AI 平台与数据库的完整闭环。适配器模式有效隔离平台差异,业务服务与 ORM 模型保障可维护性与扩展性。建议后续引入任务队列与缓存策略以进一步提升性能与用户体验。 + +## 附录 +- 开发环境启动:使用 Compose 启动数据库、缓存、后端与前端服务,分别暴露 5432、6379、8000、3000 端口。 +- 后端镜像:基于 Python slim 镜像,预装 Playwright 与系统依赖,暴露 8000 端口。 +- 前端镜像:基于 Node Alpine 镜像,安装依赖后暴露 3000 端口。 + +章节来源 +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) +- [backend/Dockerfile:1-41](file://backend/Dockerfile#L1-L41) +- [frontend/Dockerfile:1-15](file://frontend/Dockerfile#L1-L15) \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/项目概述/项目介绍.md b/.qoder/repowiki/zh/content/项目概述/项目介绍.md new file mode 100644 index 0000000..c628b37 --- /dev/null +++ b/.qoder/repowiki/zh/content/项目概述/项目介绍.md @@ -0,0 +1,348 @@ +# 项目介绍 + + +**本文档引用的文件** +- [backend/app/main.py](file://backend/app/main.py) +- [backend/app/config.py](file://backend/app/config.py) +- [backend/requirements.txt](file://backend/requirements.txt) +- [docker-compose.yml](file://docker-compose.yml) +- [backend/app/models/query.py](file://backend/app/models/query.py) +- [backend/app/models/citation_record.py](file://backend/app/models/citation_record.py) +- [backend/app/models/user.py](file://backend/app/models/user.py) +- [backend/app/workers/citation_engine.py](file://backend/app/workers/citation_engine.py) +- [backend/app/workers/scheduler.py](file://backend/app/workers/scheduler.py) +- [backend/app/api/citations.py](file://backend/app/api/citations.py) +- [frontend/package.json](file://frontend/package.json) +- [frontend/lib/api.ts](file://frontend/lib/api.ts) +- [frontend/components/layout/header.tsx](file://frontend/components/layout/header.tsx) +- [frontend/app/layout.tsx](file://frontend/app/layout.tsx) + + +## 目录 +1. [引言](#引言) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖分析](#依赖分析) +7. [性能考虑](#性能考虑) +8. [故障排查指南](#故障排查指南) +9. [结论](#结论) +10. [附录](#附录) + +## 引言 +GEO平台是一个面向智能学术查询与引用管理的现代化系统,旨在帮助研究人员、市场分析师与品牌监测团队高效地追踪特定品牌在多平台上的提及情况。平台通过“智能引用检测”“多AI平台集成”“定时查询调度”等核心能力,解决传统学术研究中品牌引用检测的痛点与多平台数据整合难题,提供统一的数据采集、分析与可视化入口。 + +- 核心使命:以自动化与智能化手段降低品牌监测与学术研究的人工成本,提升信息获取与洞察效率。 +- 价值主张:统一平台、可扩展的AI适配、稳定的定时调度、清晰的统计与导出能力,满足不同规模用户的使用需求。 + +## 项目结构 +项目采用前后端分离架构,后端基于FastAPI构建REST服务,数据库与缓存分别使用PostgreSQL与Redis;前端基于Next.js,提供用户认证、查询管理、引用数据展示与报告导出等功能。Docker Compose用于本地开发环境的一键编排。 + +```mermaid +graph TB +subgraph "前端(Frontend)" +FE_APP["Next.js 应用"] +FE_AUTH["认证模块
登录/注册/会话"] +FE_UI["UI组件与布局
头部/侧边栏/图表"] +FE_API["API封装
统一请求与鉴权"] +end +subgraph "后端(Backend)" +BE_MAIN["FastAPI 应用
路由与生命周期"] +BE_SCHED["定时调度器
APScheduler"] +BE_ENGINE["引用检测引擎
品牌匹配/竞争品牌检测"] +BE_MODELS["数据模型
用户/查询/引用记录"] +BE_API["API接口
认证/查询/引用/报告"] +end +subgraph "基础设施" +DB["PostgreSQL 数据库"] +REDIS["Redis 缓存/任务队列"] +end +FE_APP --> FE_API +FE_API --> BE_MAIN +BE_MAIN --> BE_API +BE_MAIN --> BE_SCHED +BE_SCHED --> BE_ENGINE +BE_ENGINE --> DB +BE_MAIN --> DB +BE_MAIN --> REDIS +``` + +**图示来源** +- [backend/app/main.py:24-47](file://backend/app/main.py#L24-L47) +- [backend/app/workers/scheduler.py:25-95](file://backend/app/workers/scheduler.py#L25-L95) +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) +- [backend/app/models/user.py:11-41](file://backend/app/models/user.py#L11-L41) +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [backend/app/models/citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) + +**章节来源** +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) + +## 核心组件 +- 引用检测引擎:负责对指定关键词在多个AI平台进行检索,并执行品牌引用检测与竞争品牌识别,生成标准化的引用记录。 +- 定时调度器:基于APScheduler周期性扫描数据库中到期的查询任务,自动触发引用检测流程。 +- 数据模型:围绕用户、查询、引用记录与任务状态建立清晰的关系模型,支撑查询计划、历史记录与统计分析。 +- API层:提供认证、查询管理、引用数据查询与统计、即时执行、报告导出等接口。 +- 前端应用:提供用户认证、查询配置、引用数据浏览、趋势与平台分布图表以及CSV导出能力。 + +**章节来源** +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) +- [backend/app/workers/scheduler.py:25-95](file://backend/app/workers/scheduler.py#L25-L95) +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [backend/app/models/citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) +- [backend/app/api/citations.py:1-78](file://backend/app/api/citations.py#L1-L78) +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) + +## 架构总览 +下图展示了从用户操作到数据落库的关键交互路径,包括认证、查询创建、定时调度、平台适配与结果存储。 + +```mermaid +sequenceDiagram +participant U as "用户" +participant FE as "前端应用" +participant API as "后端API" +participant S as "调度器" +participant E as "引用检测引擎" +participant P as "AI平台适配器" +participant DB as "数据库" +U->>FE : 登录/创建查询 +FE->>API : 发起请求(带鉴权) +API->>DB : 写入查询/更新状态 +API-->>FE : 返回响应 +S->>DB : 查询到期的查询任务 +S->>E : 触发执行 +E->>P : 平台查询(keyword) +P-->>E : 返回原始回复 +E->>E : 品牌匹配/竞争品牌检测 +E->>DB : 写入引用记录 +E-->>S : 完成并更新下次执行时间 +``` + +**图示来源** +- [backend/app/workers/scheduler.py:51-84](file://backend/app/workers/scheduler.py#L51-L84) +- [backend/app/workers/citation_engine.py:159-234](file://backend/app/workers/citation_engine.py#L159-L234) +- [backend/app/models/query.py:25-31](file://backend/app/models/query.py#L25-L31) +- [backend/app/models/citation_record.py:19-33](file://backend/app/models/citation_record.py#L19-L33) + +## 详细组件分析 + +### 引擎与平台适配 +- 品牌匹配器:支持精确、别名与模糊匹配,返回是否引用、置信度、首次出现段落位置及上下文片段。 +- 竞争品牌检测器:基于预定义行业品牌清单,识别文本中除目标品牌外的其他品牌。 +- 引用检测引擎:封装平台适配器调用、品牌匹配与竞争品牌检测,生成标准化引用记录并更新查询的下次执行时间。 +- 平台适配器:当前包含“文心”“Kimi”两个适配器,未来可扩展更多平台。 + +```mermaid +classDiagram +class CitationEngine { ++execute_query(query, db) list ++execute_single_platform(keyword, platform, target_brand, aliases) dict +-_get_or_create_task(db, query_id, platform) QueryTask +-_calculate_next_query_at(freq) datetime ++close() void +} +class BrandMatcher { ++match(text) dict +-_extract_candidates(text) list +-_extract_position_and_context(text, keyword) tuple +} +class CompetitorDetector { ++detect(text, target_brand) list +} +class QueryScheduler { ++start() void ++shutdown() void ++check_and_execute_queries() void +} +CitationEngine --> BrandMatcher : "使用" +CitationEngine --> CompetitorDetector : "使用" +QueryScheduler --> CitationEngine : "驱动执行" +``` + +**图示来源** +- [backend/app/workers/citation_engine.py:19-120](file://backend/app/workers/citation_engine.py#L19-L120) +- [backend/app/workers/citation_engine.py:122-146](file://backend/app/workers/citation_engine.py#L122-L146) +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) +- [backend/app/workers/scheduler.py:25-95](file://backend/app/workers/scheduler.py#L25-L95) + +**章节来源** +- [backend/app/workers/citation_engine.py:19-120](file://backend/app/workers/citation_engine.py#L19-L120) +- [backend/app/workers/citation_engine.py:122-146](file://backend/app/workers/citation_engine.py#L122-L146) +- [backend/app/workers/citation_engine.py:148-309](file://backend/app/workers/citation_engine.py#L148-L309) + +### 定时调度流程 +- 启动阶段:应用生命周期内启动调度器,设置每小时检查一次。 +- 执行阶段:查询状态为“active”且下次执行时间已到达的任务,逐个触发引用检测。 +- 错误处理:单个任务失败不影响整体调度,异常被记录并继续处理其他任务。 + +```mermaid +flowchart TD +Start(["启动调度器"]) --> AddJob["添加定时任务(每小时)"] +AddJob --> Wait["等待触发"] +Wait --> Trigger{"到达触发时间?"} +Trigger --> |否| Wait +Trigger --> |是| QueryDue["查询到期的查询任务"] +QueryDue --> HasTasks{"是否有待执行任务?"} +HasTasks --> |否| Wait +HasTasks --> |是| ExecOne["执行单个查询任务"] +ExecOne --> UpdateTime["更新下次执行时间"] +UpdateTime --> Wait +``` + +**图示来源** +- [backend/app/workers/scheduler.py:30-90](file://backend/app/workers/scheduler.py#L30-L90) +- [backend/app/models/query.py:25-31](file://backend/app/models/query.py#L25-L31) + +**章节来源** +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) + +### 数据模型与关系 +- 用户(User):用户基本信息、订阅计划与配额。 +- 查询(Query):关键词、目标品牌、别名、平台集合、频率、状态与时间戳。 +- 引用记录(CitationRecord):平台来源、是否引用、引用位置、上下文、竞争品牌列表与原始回复。 +- 关系:用户与查询一对多;查询与引用记录、查询任务一对多。 + +```mermaid +erDiagram +USERS { +uuid id PK +string email UK +string name +string plan +int max_queries +boolean is_active +} +QUERIES { +uuid id PK +uuid user_id FK +string keyword +string target_brand +jsonb brand_aliases +jsonb platforms +string frequency +string status +timestamp last_queried_at +timestamp next_query_at +} +CITATION_RECORDS { +uuid id PK +uuid query_id FK +string platform +boolean cited +int citation_position +text citation_text +jsonb competitor_brands +text raw_response +} +USERS ||--o{ QUERIES : "拥有" +QUERIES ||--o{ CITATION_RECORDS : "产生" +``` + +**图示来源** +- [backend/app/models/user.py:11-41](file://backend/app/models/user.py#L11-L41) +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [backend/app/models/citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) + +**章节来源** +- [backend/app/models/user.py:11-41](file://backend/app/models/user.py#L11-L41) +- [backend/app/models/query.py:11-55](file://backend/app/models/query.py#L11-L55) +- [backend/app/models/citation_record.py:11-42](file://backend/app/models/citation_record.py#L11-L42) + +### API与前端交互 +- 后端API:提供认证、查询管理、引用数据分页与统计、即时执行、报告导出等接口。 +- 前端封装:统一的API模块负责鉴权头注入与错误处理,页面通过hooks与组件化UI实现交互。 +- 认证流程:登录成功后保存会话,后续请求携带令牌访问受保护资源。 + +```mermaid +sequenceDiagram +participant C as "客户端" +participant A as "认证API" +participant S as "会话状态" +participant D as "受保护API" +C->>A : POST /api/v1/auth/login +A-->>C : 返回令牌 +C->>S : 保存令牌 +C->>D : GET /api/v1/queries (携带Authorization) +D-->>C : 返回数据 +``` + +**图示来源** +- [backend/app/api/citations.py:25-78](file://backend/app/api/citations.py#L25-L78) +- [frontend/lib/api.ts:23-57](file://frontend/lib/api.ts#L23-L57) +- [frontend/components/layout/header.tsx:7-28](file://frontend/components/layout/header.tsx#L7-L28) + +**章节来源** +- [backend/app/api/citations.py:1-78](file://backend/app/api/citations.py#L1-L78) +- [frontend/lib/api.ts:1-58](file://frontend/lib/api.ts#L1-L58) +- [frontend/components/layout/header.tsx:1-30](file://frontend/components/layout/header.tsx#L1-L30) + +## 依赖分析 +- 运行时依赖:FastAPI、SQLAlchemy、Pydantic、Redis、APScheduler、Playwright、HTTPX、Python-Jose、Passlib等。 +- 前端依赖:Next.js、Next-Auth、Recharts、Radix UI等。 +- 基础设施:PostgreSQL与Redis容器通过Docker Compose编排,后端服务暴露8000端口,前端服务暴露3000端口。 + +```mermaid +graph LR +subgraph "后端" +F["FastAPI"] +S["SQLAlchemy"] +P["Pydantic"] +R["Redis"] +A["APScheduler"] +PW["Playwright"] +H["HTTPX"] +end +subgraph "前端" +N["Next.js"] +NA["Next-Auth"] +RC["Recharts"] +RU["Radix UI"] +end +F --> S +F --> P +F --> R +F --> A +F --> PW +F --> H +N --> NA +N --> RC +N --> RU +``` + +**图示来源** +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) +- [frontend/package.json:11-28](file://frontend/package.json#L11-L28) + +**章节来源** +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) +- [frontend/package.json:1-40](file://frontend/package.json#L1-L40) +- [docker-compose.yml:36-66](file://docker-compose.yml#L36-L66) + +## 性能考虑 +- 异步与并发:后端使用异步数据库会话与异步调度器,减少阻塞;平台适配器在执行查询时应避免同步阻塞操作。 +- 缓存策略:利用Redis缓存短期高频数据与任务状态,降低数据库压力。 +- 分页与索引:引用数据查询支持分页与多条件过滤;数据库表建立必要索引以优化查询性能。 +- 调度粒度:默认每小时检查一次到期任务,可根据业务量调整间隔。 +- 前端渲染:图表组件按需加载,避免一次性渲染大量数据导致卡顿。 + +## 故障排查指南 +- 健康检查:后端提供健康检查端点,可用于容器编排下的存活探针。 +- 日志定位:调度器与引擎均输出详细日志,便于定位任务失败原因。 +- 数据一致性:若发现引用记录缺失,检查任务状态与错误信息,确认平台适配器可用性与网络连通性。 +- 认证问题:确认前端令牌是否正确注入,后端CORS配置是否允许前端域名访问。 + +**章节来源** +- [backend/app/main.py:45-47](file://backend/app/main.py#L45-L47) +- [backend/app/workers/scheduler.py:76-84](file://backend/app/workers/scheduler.py#L76-L84) +- [backend/app/workers/citation_engine.py:211-227](file://backend/app/workers/citation_engine.py#L211-L227) + +## 结论 +GEO平台通过“智能引用检测+多平台集成+定时调度”的组合拳,有效解决了品牌监测与学术研究中的重复劳动与数据割裂问题。其模块化设计与前后端分离架构便于扩展与维护,适合研究人员、市场分析师与品牌监测团队在不同场景下灵活使用。 + +## 附录 +- 快速开始:使用Docker Compose一键启动数据库、缓存、后端与前端服务,访问 http://localhost:3000 进行体验。 +- 开发建议:新增平台适配器时遵循现有适配器接口规范;为关键流程补充单元测试与集成测试;持续优化数据库索引与查询性能。 \ No newline at end of file diff --git a/.qoder/repowiki/zh/content/项目概述/项目概述.md b/.qoder/repowiki/zh/content/项目概述/项目概述.md new file mode 100644 index 0000000..5de7b3b --- /dev/null +++ b/.qoder/repowiki/zh/content/项目概述/项目概述.md @@ -0,0 +1,408 @@ +# 项目概述 + + +**本文档引用的文件** +- [backend/app/main.py](file://backend/app/main.py) +- [backend/app/config.py](file://backend/app/config.py) +- [backend/app/workers/scheduler.py](file://backend/app/workers/scheduler.py) +- [backend/app/workers/citation_engine.py](file://backend/app/workers/citation_engine.py) +- [backend/app/workers/platforms/base.py](file://backend/app/workers/platforms/base.py) +- [backend/app/api/queries.py](file://backend/app/api/queries.py) +- [backend/app/models/query.py](file://backend/app/models/query.py) +- [backend/app/models/citation_record.py](file://backend/app/models/citation_record.py) +- [frontend/app/layout.tsx](file://frontend/app/layout.tsx) +- [frontend/package.json](file://frontend/package.json) +- [frontend/components/charts/platform-chart.tsx](file://frontend/components/charts/platform-chart.tsx) +- [frontend/lib/platforms.ts](file://frontend/lib/platforms.ts) +- [docker-compose.yml](file://docker-compose.yml) +- [backend/requirements.txt](file://backend/requirements.txt) + + +## 目录 +1. [引言](#引言) +2. [项目结构](#项目结构) +3. [核心组件](#核心组件) +4. [架构总览](#架构总览) +5. [详细组件分析](#详细组件分析) +6. [依赖分析](#依赖分析) +7. [性能考虑](#性能考虑) +8. [故障排查指南](#故障排查指南) +9. [结论](#结论) +10. [附录](#附录) + +## 引言 +GEO平台是一个面向智能学术查询与引用管理的系统,旨在帮助用户通过多AI平台进行关键词检索,并自动识别目标品牌在检索结果中的被引用情况,同时提供可视化报表与定时任务调度能力。平台采用前后端分离架构,后端基于FastAPI构建REST服务,前端基于Next.js提供交互界面;数据库采用PostgreSQL,缓存与任务调度由Redis与APScheduler支撑。 + +本项目的核心目标是: +- 提供统一的查询入口,整合多家AI平台能力 +- 自动化定时查询与引用检测,降低人工成本 +- 以图表形式直观展示各平台引用率趋势 +- 保障可扩展性与可维护性,便于后续接入更多平台与功能 + +## 项目结构 +项目采用前后端分离与容器化部署策略: +- 后端:FastAPI应用,负责API路由、业务逻辑、定时任务调度与数据库交互 +- 前端:Next.js应用,负责用户界面、图表展示与API调用 +- 数据层:PostgreSQL存储查询与引用记录;Redis用于任务调度与缓存 +- 容器编排:Docker Compose统一管理数据库、缓存、后端与前端服务 + +```mermaid +graph TB +subgraph "前端" +FE_APP["Next.js 应用
页面与图表组件"] +end +subgraph "后端" +BE_API["FastAPI 应用
路由与服务"] +BE_SCHED["APScheduler 调度器
定时任务"] +BE_ENGINE["引用检测引擎
品牌匹配与竞争品牌检测"] +end +subgraph "数据与基础设施" +DB[("PostgreSQL")] +CACHE[("Redis 缓存/队列")] +end +FE_APP --> |"HTTP 请求"| BE_API +BE_API --> |"数据库访问"| DB +BE_API --> |"任务调度"| BE_SCHED +BE_SCHED --> |"调用引擎"| BE_ENGINE +BE_ENGINE --> |"写入记录"| DB +BE_API --> CACHE +BE_SCHED --> CACHE +``` + +**图表来源** +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) + +**章节来源** +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) + +## 核心组件 +- 后端主程序与生命周期管理:负责应用启动/关闭、CORS配置、路由注册与健康检查 +- 定时任务调度器:基于APScheduler的异步调度器,周期性扫描待执行查询并触发引用检测 +- 引用检测引擎:封装品牌匹配、竞争品牌检测与平台适配器调用流程 +- 平台适配器基类:定义统一的AI平台查询接口,便于扩展新的平台 +- API路由与服务:提供查询词的增删改查、运行一次查询等接口 +- 数据模型:定义查询、引用记录、任务等核心实体及其索引 +- 前端布局与图表:提供页面骨架、主题样式与平台引用率柱状图展示 + +**章节来源** +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) +- [backend/app/workers/platforms/base.py:1-18](file://backend/app/workers/platforms/base.py#L1-L18) +- [backend/app/api/queries.py:1-86](file://backend/app/api/queries.py#L1-L86) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [backend/app/models/citation_record.py:1-42](file://backend/app/models/citation_record.py#L1-L42) +- [frontend/app/layout.tsx:1-37](file://frontend/app/layout.tsx#L1-L37) +- [frontend/components/charts/platform-chart.tsx:1-68](file://frontend/components/charts/platform-chart.tsx#L1-L68) + +## 架构总览 +GEO平台采用分层架构: +- 表现层:Next.js应用,负责UI渲染与用户交互 +- 控制层:FastAPI路由与服务,处理请求、鉴权与业务编排 +- 领域层:引用检测引擎与平台适配器,实现品牌匹配与跨平台查询 +- 基础设施层:PostgreSQL与Redis,提供持久化与任务调度支撑 +- 集成层:Docker Compose统一编排,确保服务间依赖与网络互通 + +```mermaid +graph TB +UI["前端页面
Next.js"] --> API["后端API
FastAPI"] +API --> SCHED["调度器
APScheduler"] +API --> DBM["数据库
SQLAlchemy ORM"] +API --> CACHE["缓存
Redis"] +SCHED --> ENGINE["引擎
CitationEngine"] +ENGINE --> ADAPTERS["平台适配器
BasePlatformAdapter"] +ENGINE --> DBM +ADAPTERS --> |"调用外部平台"| EXTERNAL["外部AI平台"] +``` + +**图表来源** +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) +- [backend/app/workers/platforms/base.py:1-18](file://backend/app/workers/platforms/base.py#L1-L18) + +## 详细组件分析 + +### 后端主程序与生命周期 +- 负责注册认证、查询、引用、报告等路由 +- 配置CORS允许前端本地开发环境访问 +- 应用生命周期内启动调度器并在关闭时优雅退出 + +```mermaid +sequenceDiagram +participant Client as "浏览器" +participant API as "FastAPI 应用" +participant Sched as "调度器" +participant Engine as "引用引擎" +Client->>API : "GET /health" +API-->>Client : "{status : ok}" +Note over API,Sched : "应用启动时" +API->>Sched : "start()" +Sched->>Sched : "注册每小时检查任务" +Note over Sched,Engine : "定时触发" +Sched->>Engine : "执行查询与检测" +``` + +**图表来源** +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) + +**章节来源** +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) + +### 定时任务调度器 +- 使用AsyncIOScheduler按小时轮询查询表 +- 过滤状态为active且到达下次查询时间的记录 +- 逐条调用引用引擎执行查询,更新任务状态与下次查询时间 + +```mermaid +flowchart TD +Start(["定时任务触发"]) --> Fetch["查询数据库
筛选待执行查询"] +Fetch --> HasMore{"是否有待执行查询?"} +HasMore --> |否| End(["结束"]) +HasMore --> |是| Exec["调用引擎执行查询"] +Exec --> Update["更新查询任务状态与下次查询时间"] +Update --> Fetch +``` + +**图表来源** +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) + +**章节来源** +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) + +### 引用检测引擎 +- 品牌匹配器:支持精确、别名、模糊匹配,输出引用位置与置信度 +- 竞争品牌检测:在已知品牌库中识别其他相关品牌 +- 平台适配器:封装不同AI平台的查询接口,统一返回原始文本 +- 任务管理:为每次查询创建或获取任务记录,跟踪状态与错误信息 +- 结果持久化:生成引用记录并写入数据库 + +```mermaid +classDiagram +class CitationEngine { ++execute_query(query, db) list ++execute_single_platform(keyword, platform, target_brand, aliases) dict +-_get_or_create_task(db, query_id, platform) QueryTask +-_calculate_next_query_at(frequency) datetime ++close() void +} +class BrandMatcher { ++match(text) dict +-_extract_candidates(text) list +-_extract_position_and_context(text, keyword) tuple +} +class CompetitorDetector { ++detect(text, target_brand) list +} +class BasePlatformAdapter { +<> ++platform_name str ++platform_url str ++query(keyword) str ++close() void +} +CitationEngine --> BrandMatcher : "使用" +CitationEngine --> CompetitorDetector : "使用" +CitationEngine --> BasePlatformAdapter : "调用" +``` + +**图表来源** +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) +- [backend/app/workers/platforms/base.py:1-18](file://backend/app/workers/platforms/base.py#L1-L18) + +**章节来源** +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) +- [backend/app/workers/platforms/base.py:1-18](file://backend/app/workers/platforms/base.py#L1-L18) + +### API与数据模型 +- 查询API:提供查询词的分页列表、创建、读取、更新、删除 +- 查询模型:包含关键词、目标品牌、别名、平台集合、频率、状态与时间戳 +- 引用记录模型:记录每次查询在特定平台上的引用结果、竞争品牌与原始响应 + +```mermaid +erDiagram +QUERIES { +uuid id PK +uuid user_id FK +string keyword +string target_brand +jsonb brand_aliases +jsonb platforms +string frequency +string status +timestamp last_queried_at +timestamp next_query_at +timestamp created_at +timestamp updated_at +} +CITATION_RECORDS { +uuid id PK +uuid query_id FK +string platform +boolean cited +int citation_position +text citation_text +jsonb competitor_brands +text raw_response +timestamp queried_at +} +QUERIES ||--o{ CITATION_RECORDS : "包含" +``` + +**图表来源** +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [backend/app/models/citation_record.py:1-42](file://backend/app/models/citation_record.py#L1-L42) + +**章节来源** +- [backend/app/api/queries.py:1-86](file://backend/app/api/queries.py#L1-L86) +- [backend/app/models/query.py:1-55](file://backend/app/models/query.py#L1-L55) +- [backend/app/models/citation_record.py:1-42](file://backend/app/models/citation_record.py#L1-L42) + +### 前端布局与可视化 +- 页面布局:定义站点元数据与全局样式,提供Provider容器 +- 图表组件:基于Recharts绘制平台引用率柱状图,支持响应式与自定义颜色 +- 平台映射:提供平台键值到中文名称的映射,便于展示 + +```mermaid +graph LR +Layout["页面布局
layout.tsx"] --> Providers["全局Provider"] +Providers --> Charts["图表组件
PlatformChart"] +Charts --> Data["引用统计数据"] +Charts --> Map["平台映射
platforms.ts"] +``` + +**图表来源** +- [frontend/app/layout.tsx:1-37](file://frontend/app/layout.tsx#L1-L37) +- [frontend/components/charts/platform-chart.tsx:1-68](file://frontend/components/charts/platform-chart.tsx#L1-L68) +- [frontend/lib/platforms.ts:1-18](file://frontend/lib/platforms.ts#L1-L18) + +**章节来源** +- [frontend/app/layout.tsx:1-37](file://frontend/app/layout.tsx#L1-L37) +- [frontend/components/charts/platform-chart.tsx:1-68](file://frontend/components/charts/platform-chart.tsx#L1-L68) +- [frontend/lib/platforms.ts:1-18](file://frontend/lib/platforms.ts#L1-L18) + +## 依赖分析 +- 技术栈选择理由 + - 后端:FastAPI提供高性能异步API与自动生成文档;SQLAlchemy与异步驱动支持高并发;APScheduler与Redis满足任务调度与缓存需求 + - 前端:Next.js提供SSR/CSR混合模式与良好开发体验;Radix UI与Tailwind提供一致的UI组件与样式;Recharts用于数据可视化 + - 数据与基础设施:PostgreSQL适合结构化数据与复杂查询;Redis适合任务调度与会话缓存 +- 外部依赖与集成点 + - 平台适配器:通过抽象基类统一不同AI平台的查询接口 + - CORS:允许前端本地开发端口访问后端 + - 环境变量:数据库、缓存、密钥与浏览器自动化路径通过配置集中管理 + +```mermaid +graph TB +subgraph "后端依赖" +FAST["FastAPI"] +SQL["SQLAlchemy + asyncpg"] +REDIS["Redis"] +APS["APScheduler"] +PW["Playwright"] +end +subgraph "前端依赖" +NEXT["Next.js"] +RADIX["Radix UI"] +RECHARTS["Recharts"] +TAILWIND["TailwindCSS"] +end +FAST --> SQL +FAST --> REDIS +FAST --> APS +FAST --> PW +NEXT --> RADIX +NEXT --> RECHARTS +NEXT --> TAILWIND +``` + +**图表来源** +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) +- [frontend/package.json:1-40](file://frontend/package.json#L1-L40) + +**章节来源** +- [backend/requirements.txt:1-35](file://backend/requirements.txt#L1-L35) +- [frontend/package.json:1-40](file://frontend/package.json#L1-L40) + +## 性能考虑 +- 异步化与并发 + - 后端采用异步数据库会话与异步调度器,提升I/O密集型场景下的吞吐量 +- 索引优化 + - 查询与引用记录模型建立复合索引,加速定时任务扫描与报表查询 +- 缓存与任务解耦 + - Redis承担任务调度与会话缓存,避免阻塞主业务线程 +- 可视化性能 + - 图表组件使用响应式容器与轻量级渲染,减少重绘开销 + +[本节为通用性能建议,无需特定文件引用] + +## 故障排查指南 +- 健康检查 + - 后端提供健康检查端点,用于确认服务可用性 +- 日志与错误处理 + - 调度器与引擎在执行过程中记录错误日志,便于定位失败原因 +- 常见问题 + - 数据库连接失败:检查PostgreSQL容器健康状态与连接字符串 + - 任务未执行:确认Redis可用与调度器已启动 + - 平台适配器异常:检查对应平台的API密钥与网络连通性 + +**章节来源** +- [backend/app/main.py:45-48](file://backend/app/main.py#L45-L48) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [backend/app/config.py:1-17](file://backend/app/config.py#L1-L17) + +## 结论 +GEO平台通过“多AI平台集成 + 定时查询任务调度 + 数据可视化”的组合,为用户提供从查询到洞察的一体化能力。其分层清晰、模块解耦的设计便于后续扩展更多平台与功能;容器化部署降低了运维复杂度。对于初学者,平台提供了直观的可视化与简洁的API;对于开发者,平台展示了异步架构、任务调度与领域建模的最佳实践。 + +[本节为总结性内容,无需特定文件引用] + +## 附录 + +### 系统边界与核心组件关系图 +```mermaid +graph TB +subgraph "外部系统" +Users["用户"] +AIs["多家AI平台"] +end +subgraph "GEO平台" +Frontend["前端应用"] +Backend["后端API"] +Scheduler["调度器"] +Engine["引用引擎"] +DB[("PostgreSQL")] +Cache[("Redis")] +end +Users --> Frontend +Frontend --> Backend +Backend --> DB +Backend --> Cache +Scheduler --> Engine +Engine --> AIs +Engine --> DB +Scheduler --> DB +Scheduler --> Cache +``` + +**图表来源** +- [backend/app/main.py:1-48](file://backend/app/main.py#L1-L48) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) +- [docker-compose.yml:1-71](file://docker-compose.yml#L1-L71) + +### 数据流向说明 +- 用户在前端发起查询管理与报表查看请求 +- 后端API接收请求并访问数据库与缓存 +- 调度器周期性触发引用引擎执行跨平台查询 +- 引擎对平台返回的原始文本进行品牌匹配与竞争品牌检测 +- 结果写入数据库并供前端图表组件消费 + +**章节来源** +- [backend/app/api/queries.py:1-86](file://backend/app/api/queries.py#L1-L86) +- [backend/app/workers/scheduler.py:1-95](file://backend/app/workers/scheduler.py#L1-L95) +- [backend/app/workers/citation_engine.py:1-309](file://backend/app/workers/citation_engine.py#L1-L309) +- [frontend/components/charts/platform-chart.tsx:1-68](file://frontend/components/charts/platform-chart.tsx#L1-L68) \ No newline at end of file diff --git a/.qoder/repowiki/zh/meta/repowiki-metadata.json b/.qoder/repowiki/zh/meta/repowiki-metadata.json new file mode 100644 index 0000000..48250d1 --- /dev/null +++ b/.qoder/repowiki/zh/meta/repowiki-metadata.json @@ -0,0 +1 @@ +{"knowledge_relations":[{"id":1,"source_id":"34e572eb-6fc8-4de7-8061-63783ef8be24","target_id":"2793782c-91c8-4052-b9db-39513426c736","source_type":"WIKI_ITEM","target_type":"WIKI_ITEM","relationship_type":"PARENT_CHILD","extra":"Wiki parent-child relationship: 34e572eb-6fc8-4de7-8061-63783ef8be24 -\u003e 2793782c-91c8-4052-b9db-39513426c736","gmt_create":"2026-04-22T19:25:18.918463+08:00","gmt_modified":"2026-04-22T19:25:18.918463+08:00"},{"id":2,"source_id":"34e572eb-6fc8-4de7-8061-63783ef8be24","target_id":"e23bd86e-b4ac-40eb-b1c1-38d929fd5419","source_type":"WIKI_ITEM","target_type":"WIKI_ITEM","relationship_type":"PARENT_CHILD","extra":"Wiki parent-child relationship: 34e572eb-6fc8-4de7-8061-63783ef8be24 -\u003e e23bd86e-b4ac-40eb-b1c1-38d929fd5419","gmt_create":"2026-04-22T19:25:18.919833+08:00","gmt_modified":"2026-04-22T19:25:18.919833+08:00"},{"id":3,"source_id":"34e572eb-6fc8-4de7-8061-63783ef8be24","target_id":"70b3948e-f456-42d2-b7ad-a0097ad5ee5f","source_type":"WIKI_ITEM","target_type":"WIKI_ITEM","relationship_type":"PARENT_CHILD","extra":"Wiki parent-child relationship: 34e572eb-6fc8-4de7-8061-63783ef8be24 -\u003e 70b3948e-f456-42d2-b7ad-a0097ad5ee5f","gmt_create":"2026-04-22T19:25:18.92034+08:00","gmt_modified":"2026-04-22T19:25:18.92034+08:00"},{"id":4,"source_id":"34e572eb-6fc8-4de7-8061-63783ef8be24","target_id":"1c4316a1-f1d7-46da-bcfc-ea2fa0c56110","source_type":"WIKI_ITEM","target_type":"WIKI_ITEM","relationship_type":"PARENT_CHILD","extra":"Wiki parent-child relationship: 34e572eb-6fc8-4de7-8061-63783ef8be24 -\u003e 1c4316a1-f1d7-46da-bcfc-ea2fa0c56110","gmt_create":"2026-04-22T19:25:18.920778+08:00","gmt_modified":"2026-04-22T19:25:18.920778+08:00"},{"id":5,"source_id":"34e572eb-6fc8-4de7-8061-63783ef8be24","target_id":"7e679d62-c415-4d6a-a7e5-d41d00e0ed69","source_type":"WIKI_ITEM","target_type":"WIKI_ITEM","relationship_type":"PARENT_CHILD","extra":"Wiki parent-child relationship: 34e572eb-6fc8-4de7-8061-63783ef8be24 -\u003e 7e679d62-c415-4d6a-a7e5-d41d00e0ed69","gmt_create":"2026-04-22T19:25:18.921203+08:00","gmt_modified":"2026-04-22T19:25:18.921204+08:00"},{"id":6,"source_id":"c7eb113b-b503-4d83-be7f-9ccf3350c9d9","target_id":"f94da0b4-8a07-4de1-b0b3-d3b32a12c3c1","source_type":"WIKI_ITEM","target_type":"WIKI_ITEM","relationship_type":"PARENT_CHILD","extra":"Wiki parent-child relationship: c7eb113b-b503-4d83-be7f-9ccf3350c9d9 -\u003e f94da0b4-8a07-4de1-b0b3-d3b32a12c3c1","gmt_create":"2026-04-22T19:25:18.921638+08:00","gmt_modified":"2026-04-22T19:25:18.921638+08:00"},{"id":7,"source_id":"c7eb113b-b503-4d83-be7f-9ccf3350c9d9","target_id":"df661133-efbf-43fe-97c3-f581c81f47a7","source_type":"WIKI_ITEM","target_type":"WIKI_ITEM","relationship_type":"PARENT_CHILD","extra":"Wiki parent-child relationship: c7eb113b-b503-4d83-be7f-9ccf3350c9d9 -\u003e df661133-efbf-43fe-97c3-f581c81f47a7","gmt_create":"2026-04-22T19:25:18.922154+08:00","gmt_modified":"2026-04-22T19:25:18.922154+08:00"},{"id":8,"source_id":"c7eb113b-b503-4d83-be7f-9ccf3350c9d9","target_id":"11936bb5-374f-40f4-bb53-b75264fc4b9d","source_type":"WIKI_ITEM","target_type":"WIKI_ITEM","relationship_type":"PARENT_CHILD","extra":"Wiki parent-child relationship: c7eb113b-b503-4d83-be7f-9ccf3350c9d9 -\u003e 11936bb5-374f-40f4-bb53-b75264fc4b9d","gmt_create":"2026-04-22T19:25:18.922617+08:00","gmt_modified":"2026-04-22T19:25:18.922617+08:00"},{"id":9,"source_id":"c7eb113b-b503-4d83-be7f-9ccf3350c9d9","target_id":"23ad3419-5473-4e2c-ac87-d9715090279d","source_type":"WIKI_ITEM","target_type":"WIKI_ITEM","relationship_type":"PARENT_CHILD","extra":"Wiki parent-child relationship: c7eb113b-b503-4d83-be7f-9ccf3350c9d9 -\u003e 23ad3419-5473-4e2c-ac87-d9715090279d","gmt_create":"2026-04-22T19:25:18.923059+08:00","gmt_modified":"2026-04-22T19:25:18.923059+08:00"},{"id":10,"source_id":"c7eb113b-b503-4d83-be7f-9ccf3350c9d9","target_id":"19e75845-5147-4aeb-90be-16f3aa270465","source_type":"WIKI_ITEM","target_type":"WIKI_ITEM","relationship_type":"PARENT_CHILD","extra":"Wiki parent-child relationship: c7eb113b-b503-4d83-be7f-9ccf3350c9d9 -\u003e 19e75845-5147-4aeb-90be-16f3aa270465","gmt_create":"2026-04-22T19:25:18.923883+08:00","gmt_modified":"2026-04-22T19:25:18.923883+08:00"},{"id":11,"source_id":"c7eb113b-b503-4d83-be7f-9ccf3350c9d9","target_id":"83fe6837-7874-4467-8114-103062f15f58","source_type":"WIKI_ITEM","target_type":"WIKI_ITEM","relationship_type":"PARENT_CHILD","extra":"Wiki parent-child relationship: c7eb113b-b503-4d83-be7f-9ccf3350c9d9 -\u003e 83fe6837-7874-4467-8114-103062f15f58","gmt_create":"2026-04-22T19:25:18.924565+08:00","gmt_modified":"2026-04-22T19:25:18.924565+08:00"},{"id":12,"source_id":"3af3bb9e-4d99-4dc3-b1ed-2686db385c26","target_id":"c112a3f4-b0a1-4c4a-a325-e3ede8c90be6","source_type":"WIKI_ITEM","target_type":"WIKI_ITEM","relationship_type":"PARENT_CHILD","extra":"Wiki parent-child relationship: 3af3bb9e-4d99-4dc3-b1ed-2686db385c26 -\u003e c112a3f4-b0a1-4c4a-a325-e3ede8c90be6","gmt_create":"2026-04-22T19:25:18.925143+08:00","gmt_modified":"2026-04-22T19:25:18.925143+08:00"},{"id":13,"source_id":"3af3bb9e-4d99-4dc3-b1ed-2686db385c26","target_id":"2b32ec11-d228-42c0-9232-103ba7e44f71","source_type":"WIKI_ITEM","target_type":"WIKI_ITEM","relationship_type":"PARENT_CHILD","extra":"Wiki parent-child relationship: 3af3bb9e-4d99-4dc3-b1ed-2686db385c26 -\u003e 2b32ec11-d228-42c0-9232-103ba7e44f71","gmt_create":"2026-04-22T19:25:18.925589+08:00","gmt_modified":"2026-04-22T19:25:18.925589+08:00"},{"id":14,"source_id":"3af3bb9e-4d99-4dc3-b1ed-2686db385c26","target_id":"ba0390b3-c249-4e92-a3c8-6820343aeda9","source_type":"WIKI_ITEM","target_type":"WIKI_ITEM","relationship_type":"PARENT_CHILD","extra":"Wiki parent-child relationship: 3af3bb9e-4d99-4dc3-b1ed-2686db385c26 -\u003e ba0390b3-c249-4e92-a3c8-6820343aeda9","gmt_create":"2026-04-22T19:25:18.926026+08:00","gmt_modified":"2026-04-22T19:25:18.926026+08:00"},{"id":15,"source_id":"3af3bb9e-4d99-4dc3-b1ed-2686db385c26","target_id":"760610d4-dff6-4c6d-831f-7c9078db86a6","source_type":"WIKI_ITEM","target_type":"WIKI_ITEM","relationship_type":"PARENT_CHILD","extra":"Wiki parent-child relationship: 3af3bb9e-4d99-4dc3-b1ed-2686db385c26 -\u003e 760610d4-dff6-4c6d-831f-7c9078db86a6","gmt_create":"2026-04-22T19:25:18.926451+08:00","gmt_modified":"2026-04-22T19:25:18.926451+08:00"},{"id":16,"source_id":"3af3bb9e-4d99-4dc3-b1ed-2686db385c26","target_id":"a40267bc-925a-4364-a01f-b96d3df60aea","source_type":"WIKI_ITEM","target_type":"WIKI_ITEM","relationship_type":"PARENT_CHILD","extra":"Wiki parent-child relationship: 3af3bb9e-4d99-4dc3-b1ed-2686db385c26 -\u003e a40267bc-925a-4364-a01f-b96d3df60aea","gmt_create":"2026-04-22T19:25:18.927133+08:00","gmt_modified":"2026-04-22T19:25:18.927133+08:00"},{"id":17,"source_id":"3af3bb9e-4d99-4dc3-b1ed-2686db385c26","target_id":"7bd2c461-81b0-48e8-8cb3-b4b14305806d","source_type":"WIKI_ITEM","target_type":"WIKI_ITEM","relationship_type":"PARENT_CHILD","extra":"Wiki parent-child relationship: 3af3bb9e-4d99-4dc3-b1ed-2686db385c26 -\u003e 7bd2c461-81b0-48e8-8cb3-b4b14305806d","gmt_create":"2026-04-22T19:25:18.927624+08:00","gmt_modified":"2026-04-22T19:25:18.927625+08:00"},{"id":18,"source_id":"ffbb970e-56e0-40ad-bd88-1a1e55482d96","target_id":"d5c75004-b01b-416f-850a-4791c5489a32","source_type":"WIKI_ITEM","target_type":"WIKI_ITEM","relationship_type":"PARENT_CHILD","extra":"Wiki parent-child relationship: ffbb970e-56e0-40ad-bd88-1a1e55482d96 -\u003e d5c75004-b01b-416f-850a-4791c5489a32","gmt_create":"2026-04-22T19:25:18.928064+08:00","gmt_modified":"2026-04-22T19:25:18.928064+08:00"},{"id":19,"source_id":"ffbb970e-56e0-40ad-bd88-1a1e55482d96","target_id":"c3fa653f-8dd7-41fe-8c2e-8b60adbf70f4","source_type":"WIKI_ITEM","target_type":"WIKI_ITEM","relationship_type":"PARENT_CHILD","extra":"Wiki parent-child relationship: ffbb970e-56e0-40ad-bd88-1a1e55482d96 -\u003e c3fa653f-8dd7-41fe-8c2e-8b60adbf70f4","gmt_create":"2026-04-22T19:25:18.928495+08:00","gmt_modified":"2026-04-22T19:25:18.928495+08:00"},{"id":20,"source_id":"ffbb970e-56e0-40ad-bd88-1a1e55482d96","target_id":"262a3941-fdc6-46b0-b767-be40aa9d5761","source_type":"WIKI_ITEM","target_type":"WIKI_ITEM","relationship_type":"PARENT_CHILD","extra":"Wiki parent-child relationship: ffbb970e-56e0-40ad-bd88-1a1e55482d96 -\u003e 262a3941-fdc6-46b0-b767-be40aa9d5761","gmt_create":"2026-04-22T19:25:18.928926+08:00","gmt_modified":"2026-04-22T19:25:18.928926+08:00"},{"id":21,"source_id":"ffbb970e-56e0-40ad-bd88-1a1e55482d96","target_id":"7c0201f7-9e02-4cba-9ee1-81c3477f049a","source_type":"WIKI_ITEM","target_type":"WIKI_ITEM","relationship_type":"PARENT_CHILD","extra":"Wiki parent-child relationship: ffbb970e-56e0-40ad-bd88-1a1e55482d96 -\u003e 7c0201f7-9e02-4cba-9ee1-81c3477f049a","gmt_create":"2026-04-22T19:25:18.929347+08:00","gmt_modified":"2026-04-22T19:25:18.929347+08:00"},{"id":22,"source_id":"deb919cc-9541-4ed7-a581-ae2876ea67c2","target_id":"a1cc822d-5382-431c-8c49-cf398fb5eb3c","source_type":"WIKI_ITEM","target_type":"WIKI_ITEM","relationship_type":"PARENT_CHILD","extra":"Wiki parent-child relationship: deb919cc-9541-4ed7-a581-ae2876ea67c2 -\u003e a1cc822d-5382-431c-8c49-cf398fb5eb3c","gmt_create":"2026-04-22T19:25:18.929786+08:00","gmt_modified":"2026-04-22T19:25:18.929786+08:00"},{"id":23,"source_id":"deb919cc-9541-4ed7-a581-ae2876ea67c2","target_id":"2fc79486-ec65-4533-860a-89c8877c2ea0","source_type":"WIKI_ITEM","target_type":"WIKI_ITEM","relationship_type":"PARENT_CHILD","extra":"Wiki parent-child relationship: deb919cc-9541-4ed7-a581-ae2876ea67c2 -\u003e 2fc79486-ec65-4533-860a-89c8877c2ea0","gmt_create":"2026-04-22T19:25:18.930204+08:00","gmt_modified":"2026-04-22T19:25:18.930204+08:00"},{"id":24,"source_id":"deb919cc-9541-4ed7-a581-ae2876ea67c2","target_id":"d8e2cef5-37e6-44e7-8a7b-9bd365b82a72","source_type":"WIKI_ITEM","target_type":"WIKI_ITEM","relationship_type":"PARENT_CHILD","extra":"Wiki parent-child relationship: deb919cc-9541-4ed7-a581-ae2876ea67c2 -\u003e d8e2cef5-37e6-44e7-8a7b-9bd365b82a72","gmt_create":"2026-04-22T19:25:18.930606+08:00","gmt_modified":"2026-04-22T19:25:18.930606+08:00"},{"id":25,"source_id":"cfc48cde-e1c3-47f6-afe2-a6d7a8f4ab69","target_id":"37003fc0-1cf5-4264-996b-40807001875f","source_type":"WIKI_ITEM","target_type":"WIKI_ITEM","relationship_type":"PARENT_CHILD","extra":"Wiki parent-child relationship: cfc48cde-e1c3-47f6-afe2-a6d7a8f4ab69 -\u003e 37003fc0-1cf5-4264-996b-40807001875f","gmt_create":"2026-04-22T19:25:18.931019+08:00","gmt_modified":"2026-04-22T19:25:18.931019+08:00"},{"id":26,"source_id":"cfc48cde-e1c3-47f6-afe2-a6d7a8f4ab69","target_id":"2126339b-b0f5-4152-924b-cbe028cd0c39","source_type":"WIKI_ITEM","target_type":"WIKI_ITEM","relationship_type":"PARENT_CHILD","extra":"Wiki parent-child relationship: cfc48cde-e1c3-47f6-afe2-a6d7a8f4ab69 -\u003e 2126339b-b0f5-4152-924b-cbe028cd0c39","gmt_create":"2026-04-22T19:25:18.931426+08:00","gmt_modified":"2026-04-22T19:25:18.931426+08:00"},{"id":27,"source_id":"cfc48cde-e1c3-47f6-afe2-a6d7a8f4ab69","target_id":"fab60eaf-9652-4cb5-9f9e-0525caa62d63","source_type":"WIKI_ITEM","target_type":"WIKI_ITEM","relationship_type":"PARENT_CHILD","extra":"Wiki parent-child relationship: cfc48cde-e1c3-47f6-afe2-a6d7a8f4ab69 -\u003e fab60eaf-9652-4cb5-9f9e-0525caa62d63","gmt_create":"2026-04-22T19:25:18.931829+08:00","gmt_modified":"2026-04-22T19:25:18.931829+08:00"},{"id":28,"source_id":"cfc48cde-e1c3-47f6-afe2-a6d7a8f4ab69","target_id":"00359e45-209d-4be3-8795-50dea52bdba1","source_type":"WIKI_ITEM","target_type":"WIKI_ITEM","relationship_type":"PARENT_CHILD","extra":"Wiki parent-child relationship: cfc48cde-e1c3-47f6-afe2-a6d7a8f4ab69 -\u003e 00359e45-209d-4be3-8795-50dea52bdba1","gmt_create":"2026-04-22T19:25:18.932303+08:00","gmt_modified":"2026-04-22T19:25:18.932304+08:00"},{"id":29,"source_id":"cfc48cde-e1c3-47f6-afe2-a6d7a8f4ab69","target_id":"2632a6f9-774e-4a91-94da-a984bdb20758","source_type":"WIKI_ITEM","target_type":"WIKI_ITEM","relationship_type":"PARENT_CHILD","extra":"Wiki parent-child relationship: cfc48cde-e1c3-47f6-afe2-a6d7a8f4ab69 -\u003e 2632a6f9-774e-4a91-94da-a984bdb20758","gmt_create":"2026-04-22T19:25:18.932811+08:00","gmt_modified":"2026-04-22T19:25:18.932811+08:00"},{"id":30,"source_id":"49915365-e38e-43b5-beac-6aa7d5a74cbb","target_id":"7c7564ee-7fe2-4555-8ff0-4ec1b757997a","source_type":"WIKI_ITEM","target_type":"WIKI_ITEM","relationship_type":"PARENT_CHILD","extra":"Wiki parent-child relationship: 49915365-e38e-43b5-beac-6aa7d5a74cbb -\u003e 7c7564ee-7fe2-4555-8ff0-4ec1b757997a","gmt_create":"2026-04-22T19:25:18.933272+08:00","gmt_modified":"2026-04-22T19:25:18.933272+08:00"},{"id":31,"source_id":"49915365-e38e-43b5-beac-6aa7d5a74cbb","target_id":"74d3018f-4e23-4687-8bf4-7fb403f479f7","source_type":"WIKI_ITEM","target_type":"WIKI_ITEM","relationship_type":"PARENT_CHILD","extra":"Wiki parent-child relationship: 49915365-e38e-43b5-beac-6aa7d5a74cbb -\u003e 74d3018f-4e23-4687-8bf4-7fb403f479f7","gmt_create":"2026-04-22T19:25:18.933683+08:00","gmt_modified":"2026-04-22T19:25:18.933683+08:00"},{"id":32,"source_id":"49915365-e38e-43b5-beac-6aa7d5a74cbb","target_id":"737f8d6c-bd8f-4c11-b142-71a400423323","source_type":"WIKI_ITEM","target_type":"WIKI_ITEM","relationship_type":"PARENT_CHILD","extra":"Wiki parent-child relationship: 49915365-e38e-43b5-beac-6aa7d5a74cbb -\u003e 737f8d6c-bd8f-4c11-b142-71a400423323","gmt_create":"2026-04-22T19:25:18.934096+08:00","gmt_modified":"2026-04-22T19:25:18.934096+08:00"},{"id":33,"source_id":"49915365-e38e-43b5-beac-6aa7d5a74cbb","target_id":"ae62f4a8-8840-4c8e-9a42-3373370299ff","source_type":"WIKI_ITEM","target_type":"WIKI_ITEM","relationship_type":"PARENT_CHILD","extra":"Wiki parent-child relationship: 49915365-e38e-43b5-beac-6aa7d5a74cbb -\u003e ae62f4a8-8840-4c8e-9a42-3373370299ff","gmt_create":"2026-04-22T19:25:18.934506+08:00","gmt_modified":"2026-04-22T19:25:18.934506+08:00"},{"id":34,"source_id":"98909ecc-fceb-4ab7-a26f-741838eb2e50","target_id":"1ed0b482-3d6a-4bfd-af40-47a5d1f3e802","source_type":"WIKI_ITEM","target_type":"WIKI_ITEM","relationship_type":"PARENT_CHILD","extra":"Wiki parent-child relationship: 98909ecc-fceb-4ab7-a26f-741838eb2e50 -\u003e 1ed0b482-3d6a-4bfd-af40-47a5d1f3e802","gmt_create":"2026-04-22T19:25:18.934991+08:00","gmt_modified":"2026-04-22T19:25:18.934991+08:00"},{"id":35,"source_id":"98909ecc-fceb-4ab7-a26f-741838eb2e50","target_id":"26f80935-eac2-44ee-bcdb-d6a79c537750","source_type":"WIKI_ITEM","target_type":"WIKI_ITEM","relationship_type":"PARENT_CHILD","extra":"Wiki parent-child relationship: 98909ecc-fceb-4ab7-a26f-741838eb2e50 -\u003e 26f80935-eac2-44ee-bcdb-d6a79c537750","gmt_create":"2026-04-22T19:25:18.935426+08:00","gmt_modified":"2026-04-22T19:25:18.935426+08:00"},{"id":36,"source_id":"98909ecc-fceb-4ab7-a26f-741838eb2e50","target_id":"eb1288f7-5ea1-413a-8f54-4f870306d14d","source_type":"WIKI_ITEM","target_type":"WIKI_ITEM","relationship_type":"PARENT_CHILD","extra":"Wiki parent-child relationship: 98909ecc-fceb-4ab7-a26f-741838eb2e50 -\u003e eb1288f7-5ea1-413a-8f54-4f870306d14d","gmt_create":"2026-04-22T19:25:18.935875+08:00","gmt_modified":"2026-04-22T19:25:18.935875+08:00"},{"id":37,"source_id":"98909ecc-fceb-4ab7-a26f-741838eb2e50","target_id":"aa6db9b6-71e1-4497-a0e2-f3ff25358d3a","source_type":"WIKI_ITEM","target_type":"WIKI_ITEM","relationship_type":"PARENT_CHILD","extra":"Wiki parent-child relationship: 98909ecc-fceb-4ab7-a26f-741838eb2e50 -\u003e aa6db9b6-71e1-4497-a0e2-f3ff25358d3a","gmt_create":"2026-04-22T19:25:18.936315+08:00","gmt_modified":"2026-04-22T19:25:18.936316+08:00"},{"id":38,"source_id":"643984fc-5a57-498b-8f25-68cc318d9d82","target_id":"13c568d2-dfa7-4d1b-81c0-dfef247cbb67","source_type":"WIKI_ITEM","target_type":"WIKI_ITEM","relationship_type":"PARENT_CHILD","extra":"Wiki parent-child relationship: 643984fc-5a57-498b-8f25-68cc318d9d82 -\u003e 13c568d2-dfa7-4d1b-81c0-dfef247cbb67","gmt_create":"2026-04-22T19:25:18.936799+08:00","gmt_modified":"2026-04-22T19:25:18.936799+08:00"},{"id":39,"source_id":"643984fc-5a57-498b-8f25-68cc318d9d82","target_id":"222a9371-45c1-4e0d-b1da-e8ada1c501c2","source_type":"WIKI_ITEM","target_type":"WIKI_ITEM","relationship_type":"PARENT_CHILD","extra":"Wiki parent-child relationship: 643984fc-5a57-498b-8f25-68cc318d9d82 -\u003e 222a9371-45c1-4e0d-b1da-e8ada1c501c2","gmt_create":"2026-04-22T19:25:18.937215+08:00","gmt_modified":"2026-04-22T19:25:18.937215+08:00"},{"id":40,"source_id":"643984fc-5a57-498b-8f25-68cc318d9d82","target_id":"979be617-a83d-4db9-b73e-38581348f8c1","source_type":"WIKI_ITEM","target_type":"WIKI_ITEM","relationship_type":"PARENT_CHILD","extra":"Wiki parent-child relationship: 643984fc-5a57-498b-8f25-68cc318d9d82 -\u003e 979be617-a83d-4db9-b73e-38581348f8c1","gmt_create":"2026-04-22T19:25:18.937741+08:00","gmt_modified":"2026-04-22T19:25:18.937741+08:00"},{"id":41,"source_id":"643984fc-5a57-498b-8f25-68cc318d9d82","target_id":"422c46b3-e69c-4023-a878-411a48ae182f","source_type":"WIKI_ITEM","target_type":"WIKI_ITEM","relationship_type":"PARENT_CHILD","extra":"Wiki parent-child relationship: 643984fc-5a57-498b-8f25-68cc318d9d82 -\u003e 422c46b3-e69c-4023-a878-411a48ae182f","gmt_create":"2026-04-22T19:25:18.938186+08:00","gmt_modified":"2026-04-22T19:25:18.938186+08:00"},{"id":42,"source_id":"b0c36b2b-bb03-4624-933d-c1f6a320b7ca","target_id":"8150ddbb-7aa9-48d0-9953-2ef55e4bcfd5","source_type":"WIKI_ITEM","target_type":"WIKI_ITEM","relationship_type":"PARENT_CHILD","extra":"Wiki parent-child relationship: b0c36b2b-bb03-4624-933d-c1f6a320b7ca -\u003e 8150ddbb-7aa9-48d0-9953-2ef55e4bcfd5","gmt_create":"2026-04-22T19:25:18.938603+08:00","gmt_modified":"2026-04-22T19:25:18.938603+08:00"},{"id":43,"source_id":"b0c36b2b-bb03-4624-933d-c1f6a320b7ca","target_id":"8c17b44f-1586-459b-a83d-c9b961cd2142","source_type":"WIKI_ITEM","target_type":"WIKI_ITEM","relationship_type":"PARENT_CHILD","extra":"Wiki parent-child relationship: b0c36b2b-bb03-4624-933d-c1f6a320b7ca -\u003e 8c17b44f-1586-459b-a83d-c9b961cd2142","gmt_create":"2026-04-22T19:25:18.93902+08:00","gmt_modified":"2026-04-22T19:25:18.93902+08:00"},{"id":44,"source_id":"b0c36b2b-bb03-4624-933d-c1f6a320b7ca","target_id":"05e59a75-d52f-42e1-a924-f6a32f06f2fe","source_type":"WIKI_ITEM","target_type":"WIKI_ITEM","relationship_type":"PARENT_CHILD","extra":"Wiki parent-child relationship: b0c36b2b-bb03-4624-933d-c1f6a320b7ca -\u003e 05e59a75-d52f-42e1-a924-f6a32f06f2fe","gmt_create":"2026-04-22T19:25:18.939451+08:00","gmt_modified":"2026-04-22T19:25:18.939452+08:00"},{"id":45,"source_id":"c5849940-e223-4222-be17-aa0a6cb36bc8","target_id":"d2c7157d-157f-4990-8f76-11f3ea5435f7","source_type":"WIKI_ITEM","target_type":"WIKI_ITEM","relationship_type":"PARENT_CHILD","extra":"Wiki parent-child relationship: c5849940-e223-4222-be17-aa0a6cb36bc8 -\u003e d2c7157d-157f-4990-8f76-11f3ea5435f7","gmt_create":"2026-04-22T19:25:18.939978+08:00","gmt_modified":"2026-04-22T19:25:18.939978+08:00"},{"id":46,"source_id":"c5849940-e223-4222-be17-aa0a6cb36bc8","target_id":"e1e0bd0e-01b3-4543-9781-d9beb32b9a57","source_type":"WIKI_ITEM","target_type":"WIKI_ITEM","relationship_type":"PARENT_CHILD","extra":"Wiki parent-child relationship: c5849940-e223-4222-be17-aa0a6cb36bc8 -\u003e e1e0bd0e-01b3-4543-9781-d9beb32b9a57","gmt_create":"2026-04-22T19:25:18.940415+08:00","gmt_modified":"2026-04-22T19:25:18.940415+08:00"},{"id":47,"source_id":"c5849940-e223-4222-be17-aa0a6cb36bc8","target_id":"e9a02c86-236b-49cb-bbed-9462ee123c04","source_type":"WIKI_ITEM","target_type":"WIKI_ITEM","relationship_type":"PARENT_CHILD","extra":"Wiki parent-child relationship: c5849940-e223-4222-be17-aa0a6cb36bc8 -\u003e e9a02c86-236b-49cb-bbed-9462ee123c04","gmt_create":"2026-04-22T19:25:18.941227+08:00","gmt_modified":"2026-04-22T19:25:18.941227+08:00"}],"wiki_catalogs":[{"id":"1bb55e12-24c4-42cb-8f59-ddc303460d45","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"项目概述","description":"project-overview","prompt":"为GEO项目创建全面的项目概述内容。详细介绍GEO平台的核心目标、主要功能特性和技术架构。解释项目作为智能学术查询与引用管理系统的设计理念,包括多AI平台集成、定时查询任务调度、数据可视化展示等核心功能。说明前后端技术栈的选择原因和整体架构设计思路。提供项目背景、应用场景和价值主张。包含系统边界、核心组件关系图和数据流向说明。面向初学者提供概念性理解,同时为经验丰富的开发者提供技术决策的背景信息。","progress_status":"completed","dependent_files":"backend/app/main.py,backend/app/config.py,docker-compose.yml,README.md","gmt_create":"2026-04-22T18:56:47.080895+08:00","gmt_modified":"2026-04-22T18:58:50.329037+08:00","raw_data":"WikiEncrypted:0MI1/XkBoMl0lTbK6t0Cn/+8FdvqrJ62ianMLvZj02elJtBUgH0Ns0veIx1WPtM9wwkxI+/XAqHLxJLlHB8wVx/ob+gHrWfBRFOXtNWHzmTEJrNEttrYXRV2niAmjrnc6VwXmKYktKqbX8suWQzVig0VjADoZYGObEeg0k6UmhPkxkWTv0CFbAx+cDhkRVaksYUcZryJYvMvtyIQASEjZKRRtKwti+2pbfHzXGaF917ir8bLf35/8ERIvyLM91k6J7WrkQWQzCSMouoCq7SDEG2Mf9UPmNoJ04sS8RtxbiiFeHu6NAI8L7L1EfxY3DDdg47NhHU1wH7bOT2u9DuFl4zIbaVNsenyjEzRQZrcsq9OLnl8yWNKJf21GOA6ESfKQqN6mlowI6I1W8qLXaH+z7qnaPWhOyo4mje7g8hCZG4S41ZGWOkatECxYDASI4FIduNeEVW9aCmI7PB7nSnpPSwQE5aC5VqWoMBShuC18DGj8AvqVxfjw6b7bxR8HDUKjzXB/K7KmXdVGCcJNb3tgApQuSTsYxQ2jHsSoUECE/BEAaW7oLRxWD3c3uNNvgrgdfQuFT+k7Ko5zOXJ006KKVxWcThwPsColn6fIwGOPrS7ncbwW0n28B2g2hw91A5Hzpgm/Dy0+YIlPQM8anuTom0Oj0p1Y2g2Nbp2PLaPBGGPUa6bDoCNqzoUs1784Rwjp+aLG05lN7onsW7Z7C9feQ4P5Ry0h8YnKKl/dR4mKs1k+dkOeMPFUpWt/qlFcHAMSoVy+O+Xu+reQzwoCKYlSwgFgjiZzs3p+kL1LWvbxSvArAOemC7uu5lEzWdggi+MLrSVpB7hLalHKGdJv8NlK4SmNaadctIMUQvY+5PZwitnGdyyjjlS7/wScj8Uv3/EZk3ITEUlIvWPJ15U6QBNxHy2DbosENNEgp0lDni/UFPduUos6SlLo5L0WKgfv72BnFY4lZfB3LChbEu2eBhM8AALCKaUIhj7t/oEI+9I9qqfPimBNi6FyH0jllt2yllX04VJRxl32uuWOFcZnm/WuJgw/BbrZCR++fefrA1l6s02TJzhZ0Y5XWJsKulCubVCF9tUQ4UJbnnU1nI5rTahkANjI7mTWIpkPHp+GUXL8Q3UpSwYl2BshG/kyfJIyFu26CIJnX5HtHezPZNWAwBQVebhTaSURWwish62V3jO/gRkcmDJHlE8YC2bAo62/asrgniVM+qniMotIgbt1nopX3ishgnMYYmFqKU8+YXUiE9/z243TcTEzltDGAWj5YN10RF/Mx3dT+62D1cwhO1486VYAZpYZtOjHjPeP2/4TW7FbMhub9iGFZ8FNqneYJyWZBa+udx1oyHzyo3YHrDcFGJPXi9ido7nGq53tKEM64A="},{"id":"7c3e3c0b-5d1f-4d64-99be-668041c6cd9d","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"核心框架配置","description":"core-framework","prompt":"为GEO后端核心框架创建详细的配置文档。深入解释FastAPI应用实例的初始化过程,包括应用名称、版本管理和生命周期管理。详细说明CORS中间件配置、路由注册机制和健康检查端点。文档化配置管理系统的实现,包括环境变量读取、配置验证和默认值设置。解释数据库连接池配置、异步会话管理和连接超时设置。包含应用启动和关闭流程的详细说明,以及资源清理机制。提供配置最佳实践和常见问题解决方案。","parent_id":"7c2ac186-ec90-400a-ac1d-b3b7ca2f01d5","progress_status":"completed","dependent_files":"backend/app/main.py,backend/app/config.py,backend/app/database.py","gmt_create":"2026-04-22T18:57:02.316945+08:00","gmt_modified":"2026-04-22T19:03:21.69307+08:00","raw_data":"WikiEncrypted:luoNp8LvFa7zGThvIT9T4rhp6COPRJpm3tT/kftARqFq4dg1cG/8M10NcCrCelp/LC8SV6McSk0Sx9xEgJV/YmEmjfMmlukALh9v7ZKLxT8XDDYkBCrff2Ia9KfGFRdlfVmmQUGfhQARTeykGZh6O8/4GECBHUYLS0F6NXI7fSS3R+/I5c/HIjI7F17mVNpZDQZcc2HmM4zhdyIinOGyErSf3vIPcfVVobPB4D/wCUvMWxl+jpvktykHTIgH9ze7f4fTCC02xK5vRnt1RlMdAQ3d1Km7OOGjAga30OGvRdp9UtT48gdhzPQH5FEbfzn7K6TnuPzqQRf4GuS2kVTRSUFbSnmj5d5HqHmgx7D9iQYR/XxRRVbv+GeFwINvVa+UX9cnanYU19zazOGWG2rcdDk8jqu5MwAhVZsT28hykyuLfKeTf7xuM6Li86T1mZ18SROCJHe4LXYEaH94K6CHjZLCyEl10xBk02/CkW4aTW29TIgrponrmnrvV6bShQ+ZitR1TcivPsNN5t6TCaHMnlaLlOpznSGNpMyf4S51cRPxl2cwZrqJbrdFxehIoPrUyOvtRvtEsLhpo30456ihrInvbVXgJMwZIcDaKHSu0Bf7ma8y+UCWH61+VBIqW44KzA9zmKcXU9hiiwL+gjwiNcV7JDcgg43Un4qvjEzeJiBRBMGGzc8Oc05dWgRs1K6QUdS1kZTjI/JKiGS8zFYKz0mvQqLwc6/xe1/MDhMyU1QUXSqpcNNU6aFmY7te5kENN2aSPZqigqID+RwvIV8k8JETdmHaxJUzCSrNEpbpUukFuVQKz419athf4kMeQH2fKsLMZABn10WnnmY9HGGqBx+gpNOTU9AiSz1l4b0Ej8XoQcwIn1lqw49qdJ6+PRuuFBv6OcjxgJSMNSQzm4c/eyl7yv6J4vXceT7Ul+ySMAGRnSpDUehmOuw1DT4dV0n05Rcrx5gBjuztOAexxUhLwMUK/v+8qdn/0OcciISCZ/U1GhzhWFYiVKRBfNuNkuRjwmc4zg5Zc97TPRBXYs2q2e0/3cT+Dr7lqeGnXWL3X4iTjTygXMekhDQBjc0NbF6cdDOJcPAUnOg6jDkreBiEkTJ7hZg3J9ho2RYDaHuPvyMlkW4YWTpGRODtXIs4aLG0KFbNr6uOG31hOPZsgnHmoNwcPwRUO4G89Nkl2M6ZTMiNHWtsSwES+gLmCLPunm7R2zQNtdei+K3lzSg0bQgUvg==","layer_level":1},{"id":"bcaa04da-04e3-427f-ba01-847ad657e78a","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"Next.js应用配置","description":"nextjs-app-config","prompt":"创建Next.js 14应用配置的详细文档。说明App Router的页面组织结构,包括路由规则、嵌套路由和页面布局设计。文档化全局样式配置、字体系统和主题定制。解释Tailwind CSS的配置选项、自定义样式和响应式设计原则。说明TypeScript配置、类型定义和开发工具设置。包含性能优化配置、构建优化和生产环境部署设置。提供配置最佳实践和常见问题解决方案。","parent_id":"d907a8eb-795c-4060-9cdd-50d31cf39be3","progress_status":"completed","dependent_files":"frontend/app/layout.tsx,frontend/app/globals.css,frontend/next.config.mjs,frontend/tailwind.config.ts,frontend/tsconfig.json","gmt_create":"2026-04-22T18:57:04.216045+08:00","gmt_modified":"2026-04-22T19:04:47.361369+08:00","raw_data":"WikiEncrypted:MoQ9U2rERvJfHcC2U9f5yo5PrhAUeAY0OH9kgPk99cOGQSyTenUDM+krr5XcMxqj+FSA7CYnuq8hz8eS8Pvzc8yZuRRHbRPExvPpdrOEr8fRtUXkjnECv8NVvFPloo2fonxY9Q2vmL0x2O1UsV1O6L6tkraG28x1o504hsBitow5XMWkk8uSk+sDbyHo/y9/SBDWpcEOXVqeU4nwD/UU+7N90n5i5T89nz6b4M+31tkvffIuZRigZ+1+IJqlsqFgRt9PNrxL9ZUsD2D/Yvh8EJ0sCr9z7AI+D3z+JYM2kgFT/3ugS/EnEzwByGZk/e2EsOY4D+NkRBas836JX++o61ZH5GPQBXTHnVa8h5B1LAVlbA5zBuJHHY3V2XS4X1cTo9Hj76mg9Ha+jWvX2A9fv4vnscE6cVxnM+WjWnSfN8N7Vtla2YRDrNXZRAL50+PNfhUKpb48d0hKFyzf1Av1T4WBKgaYEo2znphEy99ViuG+oOFS6D5pVQ/w5yoPe5MpZCDk4aNsO/owrJkILcGc3+qHfld4Yd4HtYM6madWmmOvHIIuCy5CPGojBzKDcTTSPP6ZjuY1EC/RG3lBkYCQtQj889+80GZpaSbrqWjTmMI1DX1KLNXJkoI3dFk5Tx+w4TqiKICJ8q9KJhOX+mEVk+ECXqrc2Qz787D34zY5UlwsaA44ziBvWDZuwfPWmtelAq4KjqfWTFWYqdY6D0fUvetLh3/VEqH/o5Nh4HrlGvy/faZbU9FKOBZPkAy8c0J24rW5p2pKmRq6+2IicNTgufRROtiY87GKfbfmqpDYBEn3Dogv/Q3i5beVavuD1tJWhRsewhAZAUi1jWdiK3LyS0QI7dMFXVJw3cm2FnegeXA8CNlWnjUVSOY/OfLFqJJqYrfcj57f3nAU4a88w4h10+iFBLh0yZkX50llRxgUjV0fnjyEFUDoinC1xDA9ip9IOosTI+avZPhLTC0Yn4ZGnJi4NCD5cD88+KIdRUnVZJ5Sqf4ZcCCQamtYBT8NJ+PlByYxpqJpGtLyV9BuexYC86YnD3PJau0oXazcytuXMYh/TTaEo89ZzW4jpsqDM1G/gBUq7R+QDzLElNZfw3Ov0E0+Pvfm89W+v2iSsJ2JI2Yq7wrml7FiTN0KoP5KVEBCEzDLwebnJFqa7GBaghwADYcgoZ2VX2Xtin82a3oqlX2WNfur2DIr4YURiSmE8g8O","layer_level":1},{"id":"9e3d703f-f424-47f4-84df-b99873b93e5a","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"项目介绍","description":"project-introduction","prompt":"为GEO项目创建详细的项目介绍内容。解释GEO平台作为智能学术查询与引用管理系统的核心使命和价值主张。详细描述项目解决的实际问题,包括传统学术研究中品牌引用检测的痛点、多平台数据整合的需求等。说明项目的目标用户群体和应用场景,如研究人员、市场分析师、品牌监测团队等。介绍项目的发展历程、设计理念和创新点。提供系统的功能概览,包括智能引用检测、多AI平台集成、定时查询调度等核心能力。面向初学者解释项目的基本概念和使用价值,为后续技术文档奠定基础。","parent_id":"1bb55e12-24c4-42cb-8f59-ddc303460d45","progress_status":"completed","dependent_files":"backend/app/main.py,backend/app/config.py,README.md","gmt_create":"2026-04-22T18:57:13.329466+08:00","gmt_modified":"2026-04-22T19:05:00.376007+08:00","raw_data":"WikiEncrypted:0MI1/XkBoMl0lTbK6t0CnyIiRc/FeBluVaO5vMjpi7GJDj6RYPJBK8z3pvJGG/q2Umz028dfdXdKirGdHRY2lrn0erk9nv1ZHCzuughhzuFHwcxdCPIpZdrZtQpPh6bsxkWiwYzSScq7WGo0sBeTvyoiisiPXNkFNbSjUGQOu0M9zOwZAcknYWqtf16nUcOV3lRfAFHfNSt77Nc0IUQGl1zM4BWFjitEsa6cq/jjGIh7XC2gcTvm2eWRaWDDr5n6IcJZtD0ODJYzhH9IJVk4uIXlKksbQcuIrmEWEkZBxeh+DAIp0tmgXr52zuL9uRxapFuSlOq3BlU5N3I+Iu8Fg/rBAnNcvKEwSk/71gIbFlwaxLvik5gposBrfcD4U1+MJLRTPhgg/HJwljnpTC13NERk70aJg2/SCvFZcO+86xWuLwvNjWRxc2K8yOeJggfWG6S/Dt0dGaFl5F44u6XDbQV0GKiDfNOzo5CtzO7CrkSRJ+zErGQOEEoF6UhVmmpA6ONf2YDr/z3Z5bIP5z605qNZZWUDPVLjU5elINOMi4fsEw3OYaerNp6aoSHA1eXnOJoJIMsU0KM3eGAO9QNhHE2PlkwXulCEX5l1tkohbwthcv94cBVsn64CtaMMgjrgUkflNYnfEM1djmP2iJFEG/MjNMntZCrVRGXdjjqR4Q+lRe3ELhXuHjqeU5RS+KdYLASsYBDzti2T8foSfBxxXcpMIz/lS8+Qx3pTLW3ktH5AaEtSdclWCF2HLc1/03O/gdv070KOck/8TXbyZsJ6g3fzThY7xASttVsW1IITTaX9XE52YkNOe7KOrjJwkSbUgOU1Rv3Aa7x3VJ/ac9nDL4AgS+TlYNxfYBlGkbesL+sqDgWrL268y38MyK0DRqEQVjCxV9+7aAkxLvaUXVMYIwxqAM9Id66zRb0pJB3uwgpOAIRzOCskNEghjp17p4BK3lgehkRcM9RAGb7WqgeNfl/+ox2b25Pj+nvbWN3XghNZwNzoTW+kBZxXhOTRvIyAne7IPRpmtD1XD6ZBx/C6ucQ7kTUL/7LqFXE66g3Zey3hiaiQihueSMCFVer6F+BIMSBpV+9cmAczdqYvXRnKIjQg15zwVGz7mKE4D7caiDjkAQeMUSqziw+i/S26Ont/JBHIsLpYISQJErTAl2rGwC+7ADw+H6jg0rn+nE5s5u9fxUPMsWodcWi5oGIFRRTyGeIZYSTNveEwN8h0LGwXhUlHR6RxBAIpbiwKVUcOFOKc7mojhJEN36mXsQdRyCUjV3YdNzj2Tw5tG4exPzStsjDxSn98XK42QisGde126iI=","layer_level":1},{"id":"a06436ee-1678-4a51-bbf8-b0d0ac3456b9","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"数据库架构","description":"database-architecture","prompt":"创建GEO项目数据库架构文档。详细说明基于PostgreSQL的异步数据库连接配置,包括SQLAlchemy异步引擎设置、连接池管理和会话生命周期。文档化数据库连接字符串配置、环境变量管理和安全考虑。解释异步数据库操作的优势、性能特点和最佳实践。包含数据库连接监控、错误处理和故障恢复机制。提供生产环境配置建议和性能调优指南。","parent_id":"243a1a45-d31f-405e-a3b9-694d2fe617d5","progress_status":"completed","dependent_files":"backend/app/database.py,backend/app/config.py,backend/alembic.ini","gmt_create":"2026-04-22T18:57:14.160162+08:00","gmt_modified":"2026-04-22T19:05:13.623934+08:00","raw_data":"WikiEncrypted:veTYwq2y4io5qXerCTrkkJxdCaPVm76qYvPs0DmFrBYr++cWSoBpsZCop64XjnmZv6vHv8URIJAqohtlaw1HFotKc3mK0aD3tk5jmFWmTjGEd9iSTEcB45Yccg7p5eggZzqexWRLP1afxRQPau2t5uy1dZIlExyu+lyiNQI9/hXf1UppgfCjo4u6akBIZtYhzgcUSoYY4jCjv0naBAT/PyevZQctOpKPIBwMdeB86h71UT0hKB1hXxbyQoljBYZmATJaRjmlQzZf/Ka1gnDj1s1WVsGMpSoA7jyNRPH+Ra6hR+5N8Ba5vkjxM/HcRFq282Jnr/TnidF4MF5HpD5Piye73QZ3zAvIbu8+iOSvpQee/FTzkrUQBy0xOA5lsfFOy5OFOKtQh0mHybuNqe6H5squ2Tge+K4+RKXbDCt0wptrSV+BVQ+OkAJJEtLNRpJElsQ5bw1UU6CuzbgnnEYrMXB5TMBI5N2/J6hmlqt978t04N3zVI9JnnFKK0H2og6TVu1Hrxq/xEB8RFWTf/WNN1p4Akee5r1uOuUry6hsO2yQUTlmHq74QnJz4o4YJodWkhwAmcyzPIlMeSFg/Kr/EvsQJN4UprhudCTvNitnPaBesVkucYDYQiVAhxyNSAqCzZUYXbyuLD4r5DKxKzvHjkhRugm73yBM7wzbxB2lABMiGM9p90FASIxS0SfzIl4qrFbO/oQGRIFaYfUCnQF8h0YDi40UX4/LVmnlT/L8y/d+hU00P5j5PI0qOyoRDqsfQZFaze+qtibkMnJ//iy0Awl0wqQ47SxaB0jhTc07lPFBLSHFSoVUaKDs1DPGiwKjpWZRzAw7rbv9NFuV+irKc0l8qDNCaK6Rk0rRd0ukFuszcaE9n92URbvv0Fk8XTHLr8sFAh/xMm63r5hUAuqEYN6z/Si5Isq52UmJ1/TvnLYIUK58JHTuDsVn3MKJ2fTgUw/U3g+N61WpDIgGvaHO75crgWZbS9qQyDoSSj4wNdxWR+wtmKjwZdRK44kWRBS1QebMTLNYDBgHD5hPnmOa5W/Rx4Y0QBw6UFjCy/GOmDI=","layer_level":1},{"id":"7fea5a24-e6de-4003-bc70-9dae6d8fdb25","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"适配器架构设计","description":"adapter-architecture","prompt":"创建适配器架构设计的详细文档。详细解释BasePlatformAdapter抽象基类的设计理念和实现细节,包括平台名称、平台URL等核心属性的定义。说明抽象方法query()的设计目的和参数规范,以及close()方法的资源清理机制。文档化适配器模式在AI平台集成中的应用价值,包括如何通过统一接口实现不同平台的无缝切换。提供适配器扩展的最佳实践和代码示例,帮助开发者理解如何为新平台创建适配器。","parent_id":"7e66719b-4a8a-484d-889d-10fdc76788bb","progress_status":"completed","dependent_files":"backend/app/workers/platforms/base.py","gmt_create":"2026-04-22T18:57:14.592407+08:00","gmt_modified":"2026-04-22T19:06:14.01609+08:00","raw_data":"WikiEncrypted:mEeeU1uNqYKn4jkG2zPtuGBVTFJMKL8+aALSFPkyoEYifQL2Z+klmgmPGyzF3YlP0xw/C/hG8jzgXcVjqaWbA8lOQYPPvd4qFK8fszqAYs/fYEHXvWBVaOTbFhjNiT5k+li1MqVRx7SiaRze/N6NV4j39o5dfNAouxnCXgLyBZdG5QXj6JAfhJ1mTHsYQFDmrIU+nOtrQvRh3bruUOURVd64c13kwbBISJKHVwByRrfqWLRTsCLXPYfsgZF4rkL+Iz9x27Nl0VXvNcIIoMXj9B7hg2PPcQlmup4U8y2LhUHmKrdiKpW1t+XZ8rpO2aPKpzr47rn+3n2axWoQIDjWKlHAwi5AhxgPysB/i5EGv0pvgo/mNNIfsZlmSYgXASM6bgCzH5W4O4oJVqOi+gaLuI5+i7N8/+s+FKb63H9ZNvwClw5jXJe2Xm1YUwVGLG8KX5beKRTmBgnyDPmOW17/OXwsWMWQSQKrgpiv+2C7VQqI8X/agBVWawLJAc8VloY281S3ZH5LTa/6ajeGUVXiwpWSD/YWzo9sxLdws1ZXCInzLYy5vX7YflgBOlw+pn6Z901TBktq5jxrG3CFTbRLGxaeG1aJ06sLEFoPzLSXIIiEIuAROC6lPr6XpYAKESgWGOG0GdM3BinxdkriGn21ZZjENIJ436aZjsOiE5M20/kJAnjWIBPEJ/sv3nsi7u5iLfYFvkus4GfnKXJyto9LWiTgqrwHoEUQy4+pnVW+ce//iLlM/t0ZknimuBfzgYSWx/jPc8o9ZU5OIserDAYGaE4sDZIDQas1jOq8HGN/q4wJR7sW7Fw1jKyl4suQZE6QSfYx6molnJ1cSVKcsGrf7AHDKM5x+uGRmLWXqMMUZXXtbbCw0mwqfxAUPiRLUdVyzot8dh/NIWX6B1KABd/kKs+L+vVuCmMen1XVfP72Uy91fYOkBj/xPkA6GYrempnrB6zh6INdvGw5dEKlvNARMnZZLCA64Xw4h2TBjbkIjsBawvJtT+wEGkfkPk5z99jbaaGS65e/JDF/g5WNnKSX0B1Xo+frM1AFwnKR4dIJXZ8=","layer_level":1},{"id":"78288302-33bd-44f7-8b29-24f516c8b6bb","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"调度器设计","description":"scheduler-design","prompt":"创建调度器设计的详细文档。解释基于APScheduler的AsyncIOScheduler架构选择和配置,包括调度器初始化、触发器设置和任务注册机制。详细说明调度器的核心组件,包括QueryScheduler类的设计模式、事件循环管理和异步任务包装。文档化调度器的启动和关闭流程,包括资源清理和优雅停机机制。提供调度器配置参数、性能调优选项和扩展方法。包含实际代码示例展示调度器的使用模式和最佳实践。","parent_id":"7416db23-312f-478d-8883-2fc3c41ac2e8","progress_status":"completed","dependent_files":"backend/app/workers/scheduler.py","gmt_create":"2026-04-22T18:57:25.383156+08:00","gmt_modified":"2026-04-22T19:07:22.171607+08:00","raw_data":"WikiEncrypted:Ixm4kI/Z7v0V3JxitGlz9ZcE16c7bpwHYYT05QZRkQpqpcesze5XUXS1vkiVEZxtNobDh2UDQ262IRWHPAAsEfjffD0RXnPHjavBBNWFb55SS8wVV2Rm8qd7oVzDBvIVleBJx9ucizitwm2GR+t/zrSdfu/UzvGrffKAHLxZUN3s9E/3BLo1n+ImmJ5dx5UCkwVkXSzRKQAMfuIsH7ma+0I71aPP4I7dEiw6qsvVmEMkNxEr++lMh5Dnb5VgV5vajcBNj0ryBY+/GPzHWk1n5VS1l7zWR8cQS4bF17Gse7V/kjmebgHXeuF15QchHtqeN80YGSckXh6FvN4l+UAWPIoq0JKn25d4owSwjXjSpulylSlauIyNGCF3tRu8nKJlAin/M3iM636BZyJfj7q0PSxmh0Ftm6aHzsTPV6/t2nIaU6GykmNuYnPJBlmMN8ESL5uQVNFLG1SLfXVeME0V3yt6x17pfAw6u7Gy+ZCv5JbVDVkLi4wb4s6WMriDfmpSYZR6GN1wH0tP/N+ion1coTaOCnxsG5GG0QhSuXNsn2cTNfgu8H7kf/34F3OJ90+zqpfd+jgzlFcH6kOMEwW4fNvpgS1kbhXDLvumYYyF2HQLViSBNJMMOA1kdWN1zkg9b+nPACXFB5p5BaJwP2Knh6j6CHP+/exmzFnui0wsMSOy/5pwf/QXfaDhWZfXWMVaOZlG5zB11FP2E3auQLNixhJ3xGJ8y13Acb6oQu2SH3GcTe/GdtLF0yD4utctwTrXZGbe+gPjopDqmmw1vBORIQ3c0FlSSzFVrDiPydc/1RVFuLEm9Ju/mP8NtLWQh5vEClbIFrP2IyDH3EQuIQTBdp88+xasqDk9GItxnSgEKKSCKA/Mh6tW5FnUV7NhRtMcCQVTk1QCoRqzuxs5TIcLS4IZcjW2h8HYA9ai6ciOjWbbCoX7gOdogk0UzHdLZkIN0KNI9ynsGgKcvqJH+9+OpUYonXczqc3ZIr80LaNunN8cg2fpq04dAFKFt3JxYfc/0FXl0OogP4Y4rPnyi+QBmyQeqyaNPZgqcXxDmENgXrhVUp8O1YTalGtL4AbPfpSeWLWUYFiKpnKp8V2OgCdMSHz9aKwcwvdbaugwpBNWMGk=","layer_level":1},{"id":"b027f234-4ac5-4d6d-9b38-afc8054325f5","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"单元测试","description":"unit-testing","prompt":"创建GEO项目单元测试的详细文档。说明各个模块的单元测试实现,包括认证模块测试、引用引擎测试、查询处理测试和引用数据测试的具体用例设计。文档化测试夹具的使用方法,包括mock_user、auth_token、auth_headers等测试辅助对象的创建和配置。详细说明测试断言的编写方法和测试覆盖率要求。提供具体的测试代码示例,展示如何测试异步函数、依赖注入和错误处理。包含测试数据准备和清理的最佳实践。","parent_id":"fc6f24c3-594e-4153-854f-19250834eeb1","progress_status":"completed","dependent_files":"tests/test_auth.py,tests/test_citation_engine.py,tests/test_citations.py,tests/test_queries.py","gmt_create":"2026-04-22T18:57:25.616384+08:00","gmt_modified":"2026-04-22T19:06:55.090058+08:00","raw_data":"WikiEncrypted:oQ7VNoYBl3ApU4O5/TGa/YEO9ObmRNw6knddpSF6M+qfBokxpt9FAn5Oh9wXkjVweu6TpLk0XawcRFRcQ09Zi+YnQyN8+4Ep9fANKzHyAxzcnhkI4VZF35j3DwTbhekUFTKQ+71fms4SP6LGrVw+8IP/eqJC2TH96LJR1oxckQk//yfvKqFHsMLgziXy6zrXuQO8PIu5U/WbkKUkJHWSkLkoTtPteU+WKljSMapCS60e0RUDWZEPuFD2uupr2PJxT8AoOXx358C9XEj8tQ+OwHpsBiR7NdmO0D3GmcgVUcuU7WWxnToWu1UM1d/t4u+x7bIQvHQIDjLWR40nTrvBZJBSSiui0he3T03jLkIB7r9kxCpm9mFUx0fkmUSWp7FRfs7MfJkuoQYVU0YHf6/FF6g/PoQ8ieyqr4YaNjM3kk8GirHkmKjfdQt9YmQtuzMWnzUQ6COtN+BG3STyOvO+12YQAYi5LWe7u7oP4D7TJRReg7FeLDQ60Y8KEoKBJKM051F7pgHlorthWz42brF5tF7oa0tjB4AtWaEwXdCvH278kzwHNEW9E5zeERzX3hlHYL+mKpTbemJlfuHuRugCRy4EKFRP0RYiIUecBK82Pi4Vi9MGjB7cQxIEutHQ/3D0JNLGwEo5TCsk4W3isoF+mJDFy6/IDwgW/+5FIo7V/G18dTg9YbFRwMecdvBfJU5QZV4R993bdx+kH6qB5IoJrA0AzBOCn99YQ/xgAd3w/82Z5vR8syeNZjHYmcx8P/XrYDk4UZTwaacEHfn7tcq+tL5qJDbxdY95wjaWRo9giFE8woDfoLWstvKA1/q+YOyDsIHC/uHjNP1L0ELPZ9ssFuHcwLVrqDwlcblya4+oXlPEOH2ar22uXNRmFyd8Mf8MmEWVZklodBHSKiGUlj8eSWqtR+Cw5Am1gmeUcOxqan5YwzpyXet6g5ll12dnu6ItLby6qm+kjET3rsjaJag7UBlMXWV9xn9Vj4QDK8USPTTWkRUEAj4WH9QqECQzUtYJ6+GsIE7LGx/5tzApMdX3BThIiG8C2k5ry3z310nAMX/pTSrGFGjy8tWFf+KH95sJ6aG4CDyxSPHT/3ivhXAnKDesNYTVgSIKNmAUcj5hRjTfJeGyljB7NEs6Gxz0tf5k+GQLwvRX6PxSnMGkm+q0yx0MNDRBYJNlPCFKUR74ZAU=","layer_level":1},{"id":"940e5918-1689-4001-a284-44f2de75b8ee","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"认证接口","description":"authentication-api","prompt":"创建认证系统的完整API文档。详细记录用户注册接口(/api/v1/auth/register)的请求参数、响应格式和错误处理。说明用户登录接口(/api/v1/auth/login)的JWT令牌生成机制、认证流程和安全考虑。文档化用户信息接口(/api/v1/auth/me)的权限验证和当前用户获取功能。包含完整的请求示例、响应示例、状态码说明和常见错误处理。提供认证中间件的使用指南和最佳实践。","parent_id":"dd12cc44-0c95-45d5-a726-e7b8bcf2531b","progress_status":"completed","dependent_files":"backend/app/api/auth.py,backend/app/schemas/auth.py,backend/app/services/auth.py","gmt_create":"2026-04-22T18:57:37.836287+08:00","gmt_modified":"2026-04-22T19:07:12.356456+08:00","raw_data":"WikiEncrypted:uxAckj1IXK2TQH//kEdeBPmScopsydAsQUjZb8Fs6jI7ezX3WnM5wMv74LMiw55tL2+YKxETbMgwkBQYqIg1VTja5IjBYqljlAzD5l6SrkKU835vZQoOKlYgIFEEXj5PMURu9+2Om7SDVgjbSQ05vLa2aVIBV6PiVswRGc5EGTGtYnV+DJs4BxHzYpH0Ac7KkyHUCGgH6w5QDaOxWt6c5gftFmsRwr8WVKANmqlNU9uhDjmWzjTtSqZ39EVbLd++OjVbpLbTtijQ6gOF0KBBS4B0N5k+Xm2/gDv+HNK9kZ0BphZE1Si3NfV0+cu27HLJ2XV9w357rte4pplU0wYG5oC6cuO7eBFxQdCR6e+ejXIvKGahQOce8d/jWexjfdWnUzvQ18PU4+bUZmiP+YD1dJVBFgxggbCIHG8VFTQBqMaYwc3NLWogjXnczII+Z3sLLneLkVAQEdQ381GeRKdaD4PrtAWp1Ss2QpGHWUKDdaNSLcfm5NXEe6GeECxXH96p6BvyegD3twPfZmY9WKG8YOhXqbpzae/+DXnUTfldYKnMfbonSav0nUBYt8JA0L3oceaNs03211fvlSgE8L9vtUDtNF5lHkhOLMLbu000pe/VWc8WVQRbCKd8KPd4TUxMkB61vQNIcvlRKLMJLNSnE1VftRTpdEpWKhm5qkLfOfikgsp2qp1iSw5Idpu/TLQPIGN/sYpTJ1eACeXt/A3qrlITHFV8by4utTXDT1gRQQ7j7n51fQHHEIbpU+6CRhSBBQfa5IlWAqkarenBmr5RY74jeX7tlhDhvFojQGHYyMaDsZO8G/H93/20YG/li4iihHLIYmsN+2YRP/KWCNaopBGp7tRyZi//RKVXdHGteWxP7vse7URwPhfmHDUB6dMExCT5PwgsYoDwD32k19Eauv4c6OogAMMUN3I9rhSo3V0cde7/xSOhJ34SUY3w9VpPAu1I4zTqhY649YmBnNEjtGLZGTnEJWSdneSPsudvl1HJKg6faq1dMy0dVkMqbHaqMCooFDhiYVS1AM1cG07y9rQisvQP8Ic3NTPAA7BhH0nrPnLc615Jg+XEAjs3brkNXEVZlxNvX5ZqMCoMqsiZrA==","layer_level":1},{"id":"6406f42a-e10b-4a2b-84a5-0a21c1c759ea","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"代码规范","description":"code-standards","prompt":"创建GEO项目的代码规范文档。详细说明Python代码风格指南,包括PEP8标准、命名约定、导入顺序和注释规范。文档化TypeScript/JavaScript代码规范,包括类型定义、接口设计、模块组织和错误处理模式。说明前后端代码风格差异和一致性要求。提供具体的代码示例展示正确和错误的编码方式。包含自动化代码检查工具的配置和使用方法。","parent_id":"2148d7d6-25c4-497f-8894-c4eb7a0d2bd7","progress_status":"completed","dependent_files":"backend/requirements.txt,frontend/tsconfig.json,frontend/.eslintrc.json,backend/app/__init__.py","gmt_create":"2026-04-22T18:57:39.373374+08:00","gmt_modified":"2026-04-22T19:07:51.274139+08:00","raw_data":"WikiEncrypted:Gw00+yNdsAXvNpUHE0sZ2+QbHW33I6XBnIRt1QjI/FW/5OIgpgiMvU/f+bhXrxoZK5Mgo6YrGuAKhtZ2BPutU1EdQeDFUnQQxxqSupe1WUkFgEECvCkbZOAHimRWBeUutq2qKCfVhu1+US1y8n87OZXUUvVSAksZOa8lbb2AHyiKTMdzzPThS0D+8uOalkBg1mvJCS9fqVoZzY5BYYpuH5eZJBglHbA4uMbdFVTSKVFfHaJ8nlm6uYjaY0sJCSg+dhEAIiMfOTXP5Bh59kxWfLSdZU6b8oPvLV0btsiS22/oEJZI4rWjJ0YTxfQ4jRv348P+GQ6oi1pvwveiJyOxLq8QcVKSiDxO55Ovd76PhOgpOp2WJcUsKoKBoxs817utm+Ld0htpPBS3V90Rw+suMSPCU0mCa80FTrBNn6PnXemH7lH1ozQIu3Y/AGNYBN93acBxtiZ09sGFh2sO9mbbCU0iRuNe9nwWa4MsA5rJMLGdRbyv6jk03+sAvuKwZHXyTWxqeXvMatOUKXzTNfmhXsE+Bexe1vaukG22SWhsXu7FhIaH4Rt7CdpMW0jjbZOf4CwKY36ucVBRwft6ymuED4ihzikPz7pJ5IEuxLGtCyt5QDqT7kbv5ITx4NM/Q2IMMTRZf9bAZyN64PV2TJVCNK11yU+cE7y8HFBNFSzPfDBGFmxijKjTL9MvlElogPp0+qckxGclnip/IOFc6/WAbPNK0qXq98Jq5DsZwLsx4//8yOyRyaQGmOVaOWxb3Aum4RIf933X/hzoTFpV2uSLSznRyCwLpKqjp2zMS7lCj9aDhWLcx5J9NxXfdxFVxBsapMeDDl5xR5nfTFJcOFylsdPxEdxf5J8ULB77F3OmE8Hff/40adNMxJHMyBmju99HNnMZkGLjC68HEPNMZ/6T0+m/Sq1yHOrakYMiw6vnQvmLW4cBIhsTFbhbgpkazGFTk8xsKH3pGySrLXL4xmadfs/g2X/lRrLbSonWaQehTqHYrhz4HIxrTUDn4kdxaPPDhH4fHdEu/69Dri0DSzmSF2m8W3hh1gbAbUO/mbHob1eJg0yKGzyh8AmvUlwXIRGrOwQA5dUZNPSo+oCcv1qlrQ==","layer_level":1},{"id":"e8ec6ac6-ad1a-4332-a7be-727b47d71233","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"Docker容器化部署","description":"docker-containerization","prompt":"创建GEO项目Docker容器化部署的详细文档。说明Docker Compose配置文件的结构和各个服务的配置参数,包括数据库(PostgreSQL)、Redis缓存、后端(FastAPI)和前端(Next.js)服务的容器设置。详细解释容器间的依赖关系、网络配置和数据卷挂载。文档化Dockerfile的构建过程,包括多阶段构建策略、依赖安装和运行时配置。提供完整的容器编排流程,从镜像构建到服务启动的完整步骤。包含环境变量配置、健康检查设置和故障排查方法。","parent_id":"803bfef4-3985-477e-a61c-915a246d0061","progress_status":"completed","dependent_files":"docker-compose.yml,backend/Dockerfile,frontend/Dockerfile,.env","gmt_create":"2026-04-22T18:57:40.354835+08:00","gmt_modified":"2026-04-22T19:08:34.084609+08:00","raw_data":"WikiEncrypted:h7ADvj4WLJHyulzCl+z7kNgZWY+r5P000yRKX1NQnlv1Xv3Dqgu5Ve8V72HxPVQrbKK8WU/ipQ1PAqV6SDrNgrc1Neea6KoTKr95imYAh3dYgIt/lZ1sndBbQCFqOF87dQsUIEKQgwrsicAeQ0zdudUWh6EW2nyIF0NGa50xffbgEr1mm/MThBZjOua+YnCRogCbgtirEO/6HGlC+I33w/bfFI+rREYKdumqUEia0kUP0pWSpwWjOQp06I7cUoSrx/LuJJ4in5YsZetdjzxdbA3ZvGMjeyI/VVF6UC/QUOgE2csP9z42v6XdOOngM/coBHI3DiUrroBGTPa6BVxJdngItCJ9XcNvXPbo0xMAkAoUKnNPA72B2pRuk4F1sTiOaSCQ1jaMiCTFvbj7+VleaodRFLDFsYa8f1w2FkiW7cEBhFpFgB7Qfru+Yi11rpkjnM2def+Sqq2PSHQ+Ji83jPoQXFl935rDQbNUy28wOIE3BdWN5jjM3nyoXcqKjPVjLf3cxGvg/sbdWk1FYPMIsbK/DUWayyTHt6L/fYkDQYjY5h0H6Q0jBxlhNQ34GG8kEcvrZ6Dy/jtCtE149pi/ToeMbpPq21c5KUhs9OHcEHgMjxWMaEVjQXMjFCpKyJIPfC984tfoZhAHi9WupzyEi37/igcgVJnvIdtadpX7uRxW2JCdfoHn1H87nGQj4eAeI3kMhtJ3EIPd/D6STxUcgtyLdTIZu9kgvnBIN4OyZeI1ygNZfawwQMmbDxHZ1pPcyZDHFlz1HZ0NFX+7VnN2b25TJP2hxMCZ/5OdYeSLozIbM/azA8QMHofESSdxDX9ZDdhvENDlngxUBmxM8+yCAWHENGYqWDL0gZMMDwe89n48Xy9qnKNLX0ROfG4FAtoXsEgLlPQM3AWFETnzr7Kvuma+8sY1iaMVIXJ/MWoy1zLwuY/oOZhVqGLX+DOEupKhk1oQmiqzciCeV8my5Q6LQeriSBww4jA3r6VQMPzBVzKJYz+TLizB6Esf//9+ilsgUVUc6GjD41Ml4fb1GZO3PzxGWrcX1RUE6RKllUBVNffxzE2WCirEX4e4YprntsHu6SAb9TLIInCx7aZnlVnaDQE5BCiHLjIQJnsHHJzlBm+8jtzE4zi7ljHLfbJwhtZ9W6S9KGUqjjNryjTVnVyYQlumOT8HCAkz3fSM6MpXwQr1qz/gHyDrQanmM1O9B/0ox5O2NWcOm5qtsteiyxwKACdyxrsUx0X9BaU8xygDL75XcTfcaOTcICRwaRZF3/UQW+zhbJo9LnHxq30JzTbxU7wEdjpWZQHZ+tu5BMrIqi4=","layer_level":1},{"id":"a91fff3d-ec5e-43df-8176-22f0084109ef","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"功能扩展","description":"feature-extension","prompt":"创建GEO项目功能扩展的详细文档。说明如何新增API接口,包括后端路由添加、数据模型扩展和业务逻辑实现的具体步骤。详细介绍前端页面扩展方法,包括Next.js页面路由添加、组件开发和状态管理集成。文档化引用检测引擎的扩展机制,包括新品牌匹配策略添加和竞争品牌识别算法的实现。提供UI组件库的扩展指南,包括新组件开发、样式定制和响应式设计。包含完整的代码示例和最佳实践建议。","parent_id":"b2bb2e3c-5661-497c-97dd-15bf6ee8b7d4","progress_status":"completed","dependent_files":"backend/app/api/,frontend/app/(dashboard)/,backend/app/workers/citation_engine.py,frontend/components/ui/","gmt_create":"2026-04-22T18:57:48.684421+08:00","gmt_modified":"2026-04-22T19:08:25.140739+08:00","raw_data":"WikiEncrypted:5RlNBsIi2/3o8ATlHuzaYD9/6k0SjLB2nW/cTN9Sp7ydYLe7mauzXPQFm5pYVoY1D1K/aULghB4g32vyxSUIsHV6NsxwYA/5+FQ8w/4xsQc3urcx9OEKeSC0hGU52WXucdAGjiEq/f4wJ2c9FdJEeALD6eX7MltcooFsxAVRWIKzHsMdBiy0RQN6s7vv4sb8vpV3b1ET27D9QlTUiTPU7RgtJLMui8iYiwJIirfL1TgdGRSx0Rr3tY/JvS3/6/L6VU/69BKglW8DnLwDkWKuthI/2sfWQa+lb+uTuixXL/VbltWQnzwjMVYghhTizEEC0xTpFGcDY/ajvLcjy5+wSleNEr9AFy/ytJKkpqEO8Zt5qyVJ4eANJMSXYcoKrZEskjVpnrPEHGdOzaEaCqcl2sQYkXde8LWM29fWmwlMjwJzucGwhudoQLXgkIwJnD+jb1r2SdjjafTI76MM53AnSfD4aj8K26yrOkVtoD6nWG1A1fG6F22pPBhJdezNJCYuKBaSDxJDES5486L5Gm5RvjYDq4r6RZ5CheGPgxl29XjCimgWuxXKVX0jPLUawteo/Vo5D7AHzucx2dX4mKr4rSSSHpD4HGdbwnxZ717DkRG01vTOnue6gSQ7pceMjSZ6TnB7dvtwwarHBKQMKwJXrpqxc83aZQjW4sRTAJs2Gc9+11c7qT9fQnGK54JJjEARaQZVaDmTpKmme4SKKxCB7V8yUqR+D+swVuSjIpYdL1m7THJtbWSSLaKkWmYiom1C6qb4GIgO4CT/IiFSyQ0Sz8MUIJi/dK0l/vVmpp5p3IF5K1m6NHDX94yEPWEHsWIS8eC2B74AyX6PG1fWSt7HpiBAzq8nu8n+IrXPqDege5sXbjfNrbvkRot94AvLF2aqMbteOUGOZxMLASQYyfRQBlaEEFMiKEVNTRSaHqiS8mS3pG6XLcV7TkWJIyMzhcT93hKXrSB2+jLa/rrxxrZwfp6lddgEcV+XZzQU3KsJf8blNUd+UkA4TcwFugu0um1Kr6Hro8EHAR34+FGMbPskEPPX2yd42YXx6qfmcFrI1xvHIQfnc7QuxZwO6GHR0G5LiPtuV4ZSxklXCwgnGWy1lpC13xANNvM1UKqAuhpuV2qU+xayK5IQv7kQaDOJpnMzFj/dSxtRgL2Xm20nH4BA2lhBCQMqeM5OxJ5vzwVlgVMZxDbhwrRaDU6cudgNfRJDFh2lGQqE2flEkvSF7g87Hc5H3qDQxUv56qpdOPJcgugKJtT3Jz4dNt3+eojvEwrTUc7d2Kq4KDTFUa5jDr5uo9PZhNR2TDJazpNIdl1x/64=","layer_level":1},{"id":"e3ae8925-4862-4280-b85a-0b376841b15e","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"快速开始","description":"getting-started","prompt":"创建GEO项目的快速开始指南。提供详细的环境要求、依赖安装步骤和本地开发环境配置说明。包含Docker容器化部署的完整流程,从环境准备到应用启动的每一步操作。提供基本的使用示例,包括用户注册登录、创建查询任务、查看引用数据等核心功能演示。说明开发环境的调试方法和常见问题解决方案。确保新开发者能够在最短时间内成功运行项目并理解基本使用流程。","order":1,"progress_status":"completed","dependent_files":"docker-compose.yml,backend/requirements.txt,frontend/package.json,.env.example","gmt_create":"2026-04-22T18:56:47.081987+08:00","gmt_modified":"2026-04-22T18:58:54.237875+08:00","raw_data":"WikiEncrypted:qfgbutC7oyxR6nMxrwk1ODnNMBEQ3/sG78fQT1yXWju83LC7H/jAM8CEAvWT8huE5OClmQiIfNYNxHQ9s7RRS/GGSwI5DTgXTPiPdxvdU9tU52Y4wj4FfVrOtqmH0nMPa+EBKH1Wo4HibSewimTkeCuJVAAjyygiPePcftR4ud09LKmopy1NSgZrtHMoDFDwUOcNJQad4OVgxMnOi6B6z6s2Oh2aPWNJX5EvE5xGkLAc6XvhE2T7MU40VJ4lUOoKlCNZNdI06grEqwyizBqt13Hg20wonqeQ1uPs49UbF84IJecH3DmE6KW3oqmqvQY6tztk79eE1bKZmBtQKXz8WhEMWZwALTsVk+hkk4dH/fcDoLteR1iuO4FaxDwAOaD71KF79AaYxDZiM/8RER9YsajjGSI+sfpJALuiQbqqfKbYFSgliiUcf4gTM20Ey5rtfBkcvBY6yOBPpqdKg7/ls7jdggNEWDJfN+DPVzV4SGAYRD4g/kDzPa5vaNZ6USRa7hVIR0SJufX0DGSdzX5BMh5DQEEyd9svxV+bs4KPZphBIeqAOzvo6zyF9l9LjPPdEook/uTIBHjGgx+8Ck1NQEeNOs61Zqr5EfISope1Au8dYCrzwpr02s4XAb/6bYsMSkfVqETlSSUkipDodGoLJU8QiYr5Pjv63Bb9Qb7SZ+i7NrGp6UJNVT0etywEMKSQn+73I8e9AYrAxlN5ST6K5b3W7cOPzFQMCVRfzvdd/FbuN30u7JXlFCnm6Ee8LDHW6nteOxBP1D6JxbrQirCQE0cSuk+d8OR5tR/DJiRDcjbsG38KVBEqWnIek8KlMGxEt6N3oq2ceUCCLe4jVURPc/2G/6gNiyVq0dPPAVwjaNKNwtiyaDhhBu4T21k9NUfhdQNz9DJWD0wXuaZc89RoqLNlCHJ6TtxEHJ/Sv4cgvm3QUierIBIqUawkCMEDn3ZEo7L2fUS4fMlzfpHSgTvxB43ta+H1m6syFUmAyh1yqfXVxKZY0f4JtmBbQpNzpT+aRHMKKQ9UVvtjvNcpupzO5aNkyclJVWis66DMtfA1H4am2gseXSfgHHtXiLfOnGoELSM1fLS+T9oPt48v+RPafJQpahtNeKtZc4dbTcsc4qlS1krenNSBbkKac4Zsosjv8xHuD1aqYvAS476XT3gjLfV7YUCF/+i3PiovD9NGzhA="},{"id":"c19260e2-5163-43d4-b35a-b48aae995f4a","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"认证系统","description":"authentication-system","prompt":"为GEO认证系统创建全面的实现文档。详细解释用户模型设计,包括字段定义、数据类型和约束条件。文档化JWT令牌生成和验证机制,包括令牌签名算法、过期时间和刷新策略。说明用户注册流程,包括密码加密、邮箱验证和用户激活。解释登录认证过程,包括凭据验证、会话管理和权限分配。详细描述依赖注入系统在认证中的应用,包括当前用户获取和权限检查。包含安全最佳实践、令牌存储策略和会话管理。提供认证错误处理和调试指南。","parent_id":"7c2ac186-ec90-400a-ac1d-b3b7ca2f01d5","order":1,"progress_status":"completed","dependent_files":"backend/app/api/auth.py,backend/app/api/deps.py,backend/app/services/auth.py,backend/app/models/user.py","gmt_create":"2026-04-22T18:57:02.317252+08:00","gmt_modified":"2026-04-22T19:09:25.682207+08:00","raw_data":"WikiEncrypted:uxAckj1IXK2TQH//kEdeBLXlO/kfAAqZjShpoDF/LwxGtmBqx1/zAvbXhqcBr9eSVupk3ShQQ44G7JWTygjRc5A2FkInV0uhQsYps0bWsiMbDrwt2AOB/ePL1uIbGoc5ApUbCip0mBf0CwqN0REsh6d830HtteEtKfMMtm5qouIOd2wK9DTeKNNZzGx8w4gSMIpsP5p3aZ18VwD5MWsgk0ypEVodI1RvEcxesO+ct8/yxvAtUoplWhOhExXvEaKsye33rOjbpp30RA+c4dB7CLQLITqpn+wxBlXockOukSc1TbsfdPYx7aBemMwYLdSg5BxjQI88UeuXoj57ax7r3FRkTkIghjXxbpJjAP34rD/Nr6kFlE1ffpnhtqKEcaewGDTam7t2tBNXaqUA5N8gM7qLaJb9G77n5LNWkW3BEI+mzLUgjH8bKerBwqmGZaXckRqWq434EkgNrsUu66RE0kG2tkjy/QgzBqBI70QGpe3lzfJc/eka8Ylgo33DJxnT2gK0imqLhTVjYuKhn/Tj7eKXmEAK+3ddYoLe7Zd1+mAieLK3NzZderogxHrbMIurgTqPWwKg7jvj9pWgT91c7lOpX0hic2OLS+T+g3bMEZOwerdtpByP9pFeu8mniiEJ0k85mEf3l3mL4Y92IJrF7wXc4Sj901JsHKGYyqErz9wUOQ0O5F2grtL3fzSQmJGrEjFXWDpUtGhM2ModNzXCBvm89cgaXU5e9PgQuweMNqPDDcnHhTYQk2uYXAKqGQ81DjmEhEn1M7KEi6FAfh9PIUmxY5Mjw/rUqRvzp8qW9ZC5i0A7VS2KF1YEati0DISkzNcusWc09yOCp3XQ2psLue8MbeIv/A7KoM+FFCX6VwfVIMZmoD9LlpO9Aguy8ZHr+wBKAarcZthLZKxBzgWwddU4ksDMPljtM00iT9LUvs4bU1kSB1rYQcsNJTlTvnl5q0giQTn8gAPr9eNv5ugyPpOxhcSiu6sLYwt1avmvC/pShZjizRD3Ao2wDAzZD48iyoPVc4yu/0J8HjBd56+9MV3mwWW9UGW9js6s1nyrzdsxZAnh9QT5dzYdPRh1EGz7MFqLA5huIrW73zkECwjcRtve3x0tGptO+D2RRvzbYfGJJtVYIJXEA9o2uRs/E57K+wAIXK8oxDXI9+kFys6eoRpHLIduIlmPUm26QJC3WoRx7H6SRfEzvJ8H5b9wYPnjpi0imunk+Lp0qDYT0aSxv1e7BPBJtAxVo4ztOJiTlGapy636dj/I3BWq8Cy3+srfaCykZ/0NCI0cyYtyk/4smftd0/fapjicm/RDEG/o6VGqlhrNuAYuG79wZ140mKvixWDJv+djOQlfglxDlqgsuA==","layer_level":1},{"id":"9b71fe02-5927-4a19-8db8-66eb129ecd9a","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"认证系统前端实现","description":"auth-system-frontend","prompt":"创建前端认证系统的详细实现文档。说明NextAuth.js的集成配置、会话管理和用户状态同步机制。文档化认证提供者配置、OAuth流程和JWT令牌处理。解释路由保护机制、权限验证和用户状态持久化。说明API客户端的认证头设置、请求拦截和错误处理。包含用户登录状态管理、会话过期处理和安全最佳实践。提供认证流程的调试方法和常见问题排查指南。","parent_id":"d907a8eb-795c-4060-9cdd-50d31cf39be3","order":1,"progress_status":"completed","dependent_files":"frontend/components/providers.tsx,frontend/lib/auth.ts,frontend/lib/api.ts,frontend/types/next-auth.d.ts","gmt_create":"2026-04-22T18:57:04.21635+08:00","gmt_modified":"2026-04-22T19:09:48.747579+08:00","raw_data":"WikiEncrypted:BenkIlLPgjbiADCpekIm2LnLFWkzmPKE+C+tunEqC7EVMaC4hkq7YX8EJXa9qt893SobX5KrGOKV9QEeDplCQkFx4neCRj4kZkvt/lObxAfDgMBruR7HQAasniGfUhUjW5KYXC16w7brsqCWR8CWKDQGEZiduwEsaDjAZy9TEu7PlK6Cp8RhkPZ9jhN/YjqaxTAq5i4xHmQz4VuuV2SO1LedFcTlV12iJH/vTW0vFyXgJqY/Ql6+bTZO2yMaEWan9rB7e+IsErvp40JaC2xqL7fRnfroIwQssGEvaqckwf2qyNjjNCcewd62C6FVDnxJOLjQbizlBKmbb8XVzuRLkECZVB1edGJKnDV6FkOkP9y2Al2Qtpbx/1Uhx1Lo92eRd57kA0Icc/KGcHKRjNsW0HfYWAtLctTH3fNIwtmND/J/6zKK4UdDJ8sXiTnjiU4JhX/fqW8bl4FVo8F+kJYUIhbJCQkl+qlYh/MkTCmyG8XukvSZGbvWMPUxyU1gMjDxUnyVdcJmFFt15oXI+X1CL6ukGEn1cRRDiB4yRAq2GR4/iUPUFC0d3aqeRj+ryjr6Cp8YqrHfONdwZyWd4hS8RPEP7NeRuD64yNRSDFTju1oPvBfROcmunn1GaAmw8dV6KtXSJnM9SaiqnYutX51ah9psTgX+UxTu7EU/ijnQYUnDLg26m2/bX4Rhm4MRyE/fAwzxOKEI3uIaqyun0W3lgdaF4bWTwTblet2a9rBOwbVP1xtJ5M095JzboruOAhl29fAT9iO2jeqpZBoO7zBim2s8cpkCt/Zf2Edu2CeUkS180PzKCBtnq0FpPLD9hjXPsn0GjtTEbwO6lSl9JdJ1+J03tiLVJjRTZffLUlA2gM3tucbJOfyGdAom4yffwP8hjrhTBeEyLwKmLISNARiKuE2Cm671yMn1pmt85Vu1rajq9L/GeJdoPPL+4pw0dk8/Oydx0+kOg8/Jyb/8D+XobOK98UfLB6gCaeqZV/2EBEumn1xdhcDpP8cx/kijjzPmOg+tlXa5VzG6SpcY1zw1kYgyEbLZMts5EHUFYp+UCNixZleiG2HvEN5v2iuXOdcZiguCFkQLt9TkzO1v99jj08p64LxUkxY7ovJoeanwleMMBEAI0vWrt1+8nf065/WLA+s6zQWMP7nlMloMXAfMybSF/he7s2xGsrnFXqR0XBU9qRnrLY/9IcroEnKTvicyEUylh+wG8KUQcIJj+fOEVqPGITla0i+36wx46XgzdRw=","layer_level":1},{"id":"b80dc237-1a6a-401f-9f4d-14190edebcdd","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"技术栈","description":"technology-stack","prompt":"为GEO项目创建全面的技术栈文档。详细介绍前后端采用的核心技术和框架选择,包括后端的FastAPI + Python 3.9+、前端的Next.js 14 + TypeScript、数据库的PostgreSQL、Redis缓存等。解释每个技术选型的原因和优势,如FastAPI的高性能异步特性、Next.js的App Router架构、SQLAlchemy的异步ORM支持等。说明容器化部署方案,包括Docker镜像构建和多阶段部署策略。介绍开发工具链,如TypeScript配置、Tailwind CSS样式框架、ESLint代码规范等。提供技术兼容性信息和版本要求,帮助开发者快速了解项目的技术基础。","parent_id":"1bb55e12-24c4-42cb-8f59-ddc303460d45","order":1,"progress_status":"completed","dependent_files":"backend/requirements.txt,frontend/package.json,backend/Dockerfile,frontend/Dockerfile","gmt_create":"2026-04-22T18:57:13.329718+08:00","gmt_modified":"2026-04-22T19:09:54.216074+08:00","raw_data":"WikiEncrypted:w0CkdCdnXCcvlN5xOpiEhEX3U68GI0Ngj6azFMJo9xRSxuOblJXg8Gncvj5vankATp2aJeXH/Zz4LVKH2Ep+cABEA0J7OIJMnt/ZJc7uvMtWJKS6+i5aGsUhX9BZcr1SC4TqdgwXb2je+DFK+fLZJVkri1Ne9BkEqmtSaV5CijFytJ/VSKFHzj8+7gC5Jd3CbEASojclHovCufD4dAVhlK2qcIuY3RBxMsvfRAlLyMLZEWg/gD7LEj0JaChWWR8vRaEtX0LtMoYIgSfbY/nUjR+3xHFig+m0qclK8Q01S5GJH99paLVqVkmt12iwMMiqNZypY1W2gRvZNsT8eanIKu0tq5HqvmbN0y9+nnuwnz/Tg6nUBlKIIS40jp2oYca9ak9bIwBm0TqkMe+M3Ara03+x9PgGRUuYwCt9d0PwpPG5/vOAkZtP8talqqwhn7Qom1nVkhmExMfsXTYsM+Fn2bQFXKIT/rAyT5rY6hwokrvghKwOb9pH7V8+4XckeKWV6L1rNcccOb6Inp4WcVBTcTvGbdy4Wj5Z1VETFwMnDdpOGFWc0jZRJpjrRSGgDuEW8IXpMvVYk3FV5ZyUXZcNyEWZ0Fp/s84guMC21/RYF67/9jD7eJYMeCsU92nWm6QkdVB13XQBOvYGFSu1QejQCgDMWpPz4kkt4yz9mgOtH7HsH8ZUDIml7Vci6pCcEIzS2Sy+BoXxSxcOzrqxD+PbGcH8A/BafN1771WuNzprCCTqbZd5MVXBKiY+qrlRCD1ug+pKS075xfWh/Y2Sjz4p1aJuyVplvOfNUKTlbZVBzQu5DBO+yhXj4b8Amf7OLydq0Avaf0gJhYhX3HpFNqi6aqT9OzPBIPohOUi9RWffBwHMJii2/NZj4GbgeXsWU5+EKE6sGFRGtUiBZGzf8FnB6rW16EA6in7Vd7Ls3pJrra3z6b/k9gqlh8eAVDoZhCPuxtA6yNlXm+RYWLDYeWiRDZoavKVj13rXtaqtX3j1/e1zFPMBZlDPVHyBqCZRiZjlKnty9cj2ASgpu7idyh8jDQSALGu814Wb55H1q08bvFxJf6pxn0JfbRJZMHqCEgInivdDUTcCCPOteDT0O2pwMmyHuq3mI3VzFPnYLM3aYzgJQsNIVztV/dIEJu2UGF/cyjIeROHaxvWYLNXRJZ05iMp8oelEwNe58eAvWt2KOuahqQl6xKUctm90QLv8QR9hr7PjiNAMVH3WsMxkqLcIUPuboMElVq3ErUqd4qE7W5l5Uxi2I9bVcnq4qiRtbCnRY32uC43wILTgq6KrBaO6dehB3eW4drPWqtmcU8rP1DJEatUWEvhpDNrI7T7wG6F+","layer_level":1},{"id":"7e5c3b8e-5aa3-448d-ae52-d5a96a413b0b","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"表结构设计","description":"table-schema-design","prompt":"创建GEO项目表结构设计文档。详细描述所有数据库表的设计,包括users、queries、citation_records、query_tasks和subscriptions表的字段定义、数据类型和约束条件。说明主键、外键关系和索引策略。文档化表之间的关联关系,包括一对一、一对多和多对多关系。解释业务规则在数据库层面的实现,如数据完整性约束和业务逻辑验证。提供表结构图和ER关系图,帮助理解数据模型的整体架构。","parent_id":"243a1a45-d31f-405e-a3b9-694d2fe617d5","order":1,"progress_status":"completed","dependent_files":"backend/app/models/user.py,backend/app/models/query.py,backend/app/models/citation_record.py,backend/app/models/query_task.py,backend/app/models/subscription.py","gmt_create":"2026-04-22T18:57:14.160671+08:00","gmt_modified":"2026-04-22T19:10:23.576798+08:00","raw_data":"WikiEncrypted:TWtD9cYU/QQHU+vD+ojNNl24L1Ao0zI5Y2mk07u9ZlXGce4CUXsGNCSRaLrvDZHw4OUpuZEJrmJBXEQX8GmxRc1IXLTgRlqUGmpzfcUAx8VrQijBe+unqg1mWTkYnMlYOqKWSJdISHyD5K3kBL2cL5pXF+f+lPHzSbWtqMnNnMKbqnSl/TkKh6cr7PM1YBR4IYmE21gHSR5XoQjbtYmg5qNyKF1kMdcAd0LHw6p7zHBaF9cNt++5GyFKLK0zjN2L8YiFsuFBSrn/M48QW3WyTrvDvqdOuR/Slfv+gUsKgbkt6+NEpuY9HPcRdvVJHvG9FZedomH4DrKuI0fUXgICXotXbrcEe5dOkLKgfn+gXmS6sPSZ1XgrSNO5gy2QNJaW+oKBuEPzfuKsNr3X19FZTz+x620YmL1Ccv/5uvf0Zxl6fyQb3qP+lsh2kaMdhWVKyoBv37oNKF7MO+fy+TWHFEwlVnxgmQREyQva5qxLyYzu1GtDpGp79IemnumltTAAzQ7LRs/RsaNcU3BZdYfQspjnhukylPJpNZtmKNN//XWvj023bGiaLyy805c71DcyqZNUs+EdlXmRTABeq9QkVU5wq/gkO4RFSgsWao/8f2i9FLeEZolKQXzoLnX7GxaY3BMcynG+DlwKFEolVOw07cpddYEWzmVKgJUokX/eiMbrokbcv9bLXJpHl2aP27B2FXy5Q2Ot11b2WbuT532RndBI+S9HQpfMmZ2z2Fc3VrWAbk03DwChHEbNHpmQvJpjKtLweVEpClahV8iTSwCkwd43jJdW+tR00iSdjt5vbl1cU7Dciah/B/9B7CDr9sUyZMjly7kzyWZqTkdXde3tO6tu6tWhTigIMXmZVqhW/pgVifH0R72VuM+sdMw57m7Z89ibQ7o2LewlMzJ4GsV90DdDhHQtUr++aRhcZp/669o8mijOQKj8MPHY+xjhZ2oiS6RHiCYxpibVfauiaXA3swWKIQrVCVE1x+jB07pieI3wHrmZf875mdp9rhfrz96+y4zJ464iINUdTQinm/zI4CnhXE3inFBgr8bD3pop0oYr2X5f8Gn2iKyvByc+w5r8HKV/7dd4GrdAosqVWpQweVEYR+MHRZ3AxiW+TCpRWz/PsHgunD4m+AQv5lxwoHVqF0Tqw7fmmxTqA/zC+dxlIJVD/R2WF4sFpA6femHOwqniwaowQerg8bGe7mz3BCSqiobqGSZQ5jQdh34TRP7qUg6PGkJx2NJ6AVIZVYBg9JNOQI67+2PN5AKq6WiT7Rojgo1r8lEx8SORaVptbL4YEEeD8Ec4g2l7Aqkhdxd43uw=","layer_level":1},{"id":"9fe32b83-3697-4939-8b10-524f5ed3e65e","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"Kimi平台集成","description":"kimi-platform-integration","prompt":"创建Kimi平台集成的详细文档。详细说明Kimi平台适配器的实现,包括Playwright浏览器自动化配置和初始化过程。文档化页面交互逻辑,包括搜索框定位、输入处理、搜索按钮点击和结果页面解析。解释错误重试机制的实现,包括网络异常处理、页面加载超时和重试策略。说明浏览器会话管理和资源清理机制。提供Kimi平台API调用的具体示例和常见问题解决方案。包含性能优化建议和调试技巧。","parent_id":"7e66719b-4a8a-484d-889d-10fdc76788bb","order":1,"progress_status":"completed","dependent_files":"backend/app/workers/platforms/kimi.py","gmt_create":"2026-04-22T18:57:14.59267+08:00","gmt_modified":"2026-04-22T19:12:57.014271+08:00","raw_data":"WikiEncrypted:CSdS6kWSLejXKP14YRwDWQpHc4XisvWgJzUb2dJm/yAGkddGXaInKHg6AG6A9w3V5Kpt4A88mghHC49ZAnCEBFkdcYzwoG8rsg51oQD0yMYKzfwh9/qTOohfaDg53bEIiDeH6Us+OaAO1Y8ql0iMmjrgLp9Y1OylCHhUCu9A42xMeh+fRbqaw2jvrjKU9Vd/PpCMTm7f+oljBa2YenytsO1vH+MJygHrIBIFMEwP4wDHRQCGm9i9XjV35+dfjxOhIgTTQEGiMzeNAJeVWdGX3CzHOqsyvOPjrWtxYxywjeMbewxFEBPA4dDmVf1Vjzzew4XPBzMpEXmfoXbyeUWhGhbE7tEFT39R09Ha75iK0wzPu1ux+4/VGTFOtfPkADxp62qkrPp9iL5lHxwQazsKyzs2bQCnu3u8gtrTmvxG69MDkDKrF9Q+F4ZtAlnadF4bsfDmfd+NGYnuFZW+D5cHTHu/EUqApHkByQiwdgH6BcOsdVw2LNu8bIfHCZq8kZtslVIVwCYcAmkmcAXNdpssx4zoDfoSAhvevYc0o6HITT8QasnnhvMWA7agJiGwUENhiApqGMMeOqP22TOMJDUUb2V2VhVNIujymTz4YUAtQq47v74k+XnI/Mx24Y1s9OOWOKvRfKZn+WXVLMwLQTVbl80EHY4/Wra+1fgC0dmHhXvVlq2jdksGrjLc75lQUEYd0n4nwf/f6JsjfM1jYvVSArLWpXoEn82NbB/T5MrfmbEiK+CQD1GfQ/vtvaPioghi7WWdUslv06Qra6l67oxtGKGx7SJd5FTXKH6MXBs4M3x1GItLIU0Dtkd+trliHatHl+wYqwVk2xgXmOGpQsOLwOf99CbQ1BtTN7KPB19I10pElWF+IbH9eTm+6sewgOPimkfI3JhVSl2cSCQZcGQVJINI8TE515tn2AMLVlM0AMQxTbh65Z99WSQvJtYH/TugerzWgFw3pwvBbp+9a5z4qZmBFIW+jxXEcrMkiC9s53D9cGLBS89D/clur1q4gDDkKVtztRvwwORkOSEfzLNhstNJp4WgD+b8H6bbLh70G6rMCXtMSMZdjuFSD40UuqdvHFcW9SE6qEOi06mMSAXOQQ==","layer_level":1},{"id":"cc7a1f1b-c70e-4c61-bfbc-6dc408a12ff2","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"查询执行流程","description":"query-execution-flow","prompt":"创建查询执行流程的详细文档。解释从任务检查到执行完成的完整生命周期,包括查询状态检查、数据库事务处理和异常处理机制。详细说明check_and_execute_queries方法的工作原理,包括查询条件筛选、批量执行策略和错误隔离机制。文档化单个查询执行过程,包括CitationEngine的集成、异步调用模式和状态更新流程。提供执行流程的时序图、状态转换图和错误处理策略。包含性能监控指标和调试技巧。","parent_id":"7416db23-312f-478d-8883-2fc3c41ac2e8","order":1,"progress_status":"completed","dependent_files":"backend/app/workers/scheduler.py,backend/app/workers/citation_engine.py","gmt_create":"2026-04-22T18:57:25.383473+08:00","gmt_modified":"2026-04-22T19:11:29.353795+08:00","raw_data":"WikiEncrypted:l9Ho0Yh0r5bYH9QEePEhka2UMFH582eHsB+P+EaGE/r7Zj5gVUJmL6923rRlAt7h2uvHOVtef4s71D/S/eYpYYgI+wRFyPq+fQZYOCmVnMBRyypO5m8WqfyrsSGvviugw2fw2oH6ruNX2ul1LjPKkntbj4o015R9XwHG0oDdyJlipxqNUbh66IV2c2DO1besCA/q2yTeLh6EiHD+vYeG3HO5H+y8kykMbBuh9eydnmoXBTCsTlNBHH8gzmZWEBBSL7DETM0lYCiNr6WpRCoeZEo9ewLagGq1ammmMjcMg+7fO/xo+ZkkACRTnG2vnKh3if5fBjzo9tY4tGmtF7Fevx+yjLRfhpPsg8vSRZeaggRPgub5zBIulo/4qZhAvIVsyYuAQCqrwq6mBxAEjgWWrWjOThUEQOntbugUY0DitRui7XGp7tDjAIWw570xWzTTUWe0CMoHIPghfvhItcnYfjKK+fapnQTnz159jVJarLbBdjZT5JJfw9a+jlkVyF5RIKu3J03cjGFJeBf74DiMxFIMQkKzgL8jg5YiLQTisI4X0BBQrKHQvHzsD5+VzHGwCsz7IIfcdZuhkg3PIA4z4V7eZP9tOBUJdh7iE4fjhOk6fVDG5iZME72OxpBtFkmfrhwM52swaToCosj1eQ5m6V2GjsYY/+ZvaNf2aWn8vBJHI4IEO+Ua1mLHkPRT9bhDGS+Td2r1R2fmSEEvNQ+PTuhnRAyzRASTsjfWljzNWGNmcbCtClwcIuTSwPWLAoL+ANl303UlvxObHa9udiJcH3pZ86+A7iBLoCg6JxZQuCSqD0W6bPR/vozpiCvGs1TvMR5tx6AtfZGWw4JYl9wN5iPPaP8sPsz40D4PWkYIb4fIYY/X3r2iAnbjpV+/8+h3DQCWEdqRNnkZ4Do+XZGOi1yH7QaPOf/RN00+bcNtR36PnzENY47tg8VfPkbt47YTWHf+qvWfMFPwco2hLzXvNFmYN4dVMld5V+9mzCPJSIb+JOcDoVkoGuNq7aXnOVFdERnypgzvjryBt3W2LgPNlD3k6jR7pJAE1gKKrl00VSdDN9npu4QXyT49p+YMv9m8Jbf9xo1Jn2dqM/cJiKZi1FLR0clZHZ7nh7/7WxoEqbKnLWbboi/qyuTABO07nyQfmyXR/24bRp3AIRiM+7Bq8KL9ZFZs05JmFBtaRCAzWdBdM3TElVkfFOyZRZeKaBlk","layer_level":1},{"id":"fec685a0-c9bb-4048-baf4-40b56b2aa29c","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"集成测试","description":"integration-testing","prompt":"创建GEO项目集成测试的综合文档。详细说明API端到端测试的实现方法,包括FastAPI应用测试、数据库连接测试和外部服务集成测试。文档化测试环境的配置,包括测试数据库设置、异步客户端配置和依赖注入覆盖。说明如何测试完整的用户工作流,从认证到查询执行再到结果返回的端到端流程。提供数据库事务管理和测试数据隔离的策略。包含性能测试和负载测试的方法。","parent_id":"fc6f24c3-594e-4153-854f-19250834eeb1","order":1,"progress_status":"completed","dependent_files":"tests/conftest.py,backend/app/main.py,backend/app/database.py","gmt_create":"2026-04-22T18:57:25.616681+08:00","gmt_modified":"2026-04-22T19:12:37.820526+08:00","raw_data":"WikiEncrypted:0j4RRfWJQdenLQLpT+DwLTyjMVG5sQffpuJQoSG8tbOVCukW9eq2bqobYYACR3zRn/N/FP6n0t+ZzbrLBFIDPbiPv5CjhNbth11wsRap1Da2YyKifcvwg7uyuWtSILKRIkP8wIVHX2/TkxRFQi9/m+9Qz3xVjYcNeOD0G8UWxzsSDqYGS90xX9MVwW3N24hbwwcJUE/TAMETdbhwrekpr33ej8R6IT6aBvn7m0Y93BRItueF5HJcUtKCizqJqcsG+BIQ1fodLPAsUiqvYW5dwymM89ASYiZXVbCOJq19yL9K4p1ZW1LsAc2J+6D8SR9RJ//D4gGwBmMTqxyP9bgn5eNt3ILeVMXWLXI9oxaDrdVs2Y4dXNC4AK1j0pegZkiNREUo54kWHFqCuQ6dG8dZB7BmnFEIYjwEeU29KdF6i0dyUH3/h5QjdLfc0OV75zuHQPaMs1p5fn3vo6ubXz/uWtxEF/IoVhph+AyY0Gci8TxdouTw4RZOm9I6mo5m4Y+TBsxUb9RO0WCrD/U7tRgIZcrxFfkORsKadNWm60Q4SRJeImUzxIVCUGBjr4km9AMwVsl94KSXmkBCNOFppeijE4N4Dl0n+RekVA2NBOau2woOIr/GwJ7b2h0jV/5/3x5hihxUSqZJ/q4RJmV/8aP30XRdYwZ6qiLcaF6Ic8kwpaS+9cvzGsPMcUF4GCS6ZSZoq4tuGj1ZY/iFI0w6Wj7rJHWbeBhsF/fHqFm2ijbrvn2nz+lH8Gxb3FM+re81hKNVU3BaZ3fxOAldPPvUxDw9tttWR+oH3F/gRkqefOIsw95LNYf2vIq/HXfNygn6mUw4aYidP1SY8flPcCMl0mDm7y7W6l+DKUJuok11v6h2AM09f0VV9JnGBMgwCic1FjQmfF2FWdqlsEaB0X4+aPne0DgPyIivgCLEq1dyneDm4TxA3CEVODVKHIu4fxPjmDtwbXUvPQe5sWkG7AEkp1Vuul8T/GLjJc/DHgUFe51MPFk3+8i0FCKo0x0fS8Mqm3spHB5W1tKYAIyexOgSkHzqgRdgERCTqUW461c4lKbFO2WIJqWgt/seMgxHr02Yk/pvMU08AaMRC974SfB7kP0lEQ==","layer_level":1},{"id":"b10c2334-a850-471a-9851-a1c698e3a485","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"查询管理接口","description":"queries-api","prompt":"创建查询管理系统的详细API文档。记录查询任务的创建、读取、更新、删除和执行操作的完整流程。详细说明查询任务的数据模型、字段定义和验证规则。文档化查询任务的状态管理、调度机制和执行监控功能。包含查询参数配置、定时任务设置和批量操作接口。提供查询任务生命周期管理的最佳实践和错误处理策略。","parent_id":"dd12cc44-0c95-45d5-a726-e7b8bcf2531b","order":1,"progress_status":"completed","dependent_files":"backend/app/api/queries.py,backend/app/schemas/query.py,backend/app/models/query.py","gmt_create":"2026-04-22T18:57:37.836674+08:00","gmt_modified":"2026-04-22T19:13:13.115817+08:00","raw_data":"WikiEncrypted:9mSyqcDNXizO1JyGDCrbHQv26RAimH4+rntsA1VX4dLvyYb4Jxu6TVndaXonw13RPqBFh6ddOlJg+Nqb71iBvzQ67YQrmYbtLXwkDZnvgAJqkSUXXzN4PlgowB/jNfTrmLqrUYAZLwPYTiSEAGfTeU+8GlJTtvE8xjWhZI7vP2u+cMth1ze2EwqO4hyTykCDaW9LISygpROFBJLghTo+K5E2Mdj9HWXWfqf/8/vypj1zjNrQHVY2bFNlhZv90+sCmTY3eQ0T3wfE94jKejGhh2P8YYJayGzsEoDFm7r2Hsf+rQmAXynuCLYuAHl7rAzrWRNjmvkxiuyI4AB9he1sozIJzX5M+HadHtd7E15MFYvAEb0KExSxSfnZCkczRCRPmNvQmwYBUaEJJ6brnImPFBEN4bxd+c2SId4g/BYudUL4Qio8g2zs8r1bZIz0rpQEHWcVoHGQK73Xp8ff7gvjRh89yEr8lh3zxI29eZygWmcap5z4kVV78YWgF0dC9at69s2EVtaiIluQIaBF/gJZuH0EjOqE/TnxTq89sb81YBRpRgHDImzngt8tBp7eiF/e+aO3qk+zH2RyGTCyRN5lqSlHoMkfeysXKgBAk9foGnD4D2OBWfndyN16CuRtGa5vAV8YS0pNw0IKN9TniA4PAGC9lTuMbqabzVWAnbBdqY2vR7nC5837ooKWljHYFMFWUfVSbOfFGxn0sFW75OelscFRx7FM+kaQtP4JrepRQcoDml7Bnt3bTt8SHThmpNMJwu22c3bruKQ++qcVCuvXKDKAK5qSxiReKiiJC+YluwwphSqkRxThXUwS4aMI7IoDX2FwLw1dYo7w4TeEi5sB8HoBZ5sY99Q0vowXVWj7Yq5HhHv8aTxTP3HjRr+Xi07oriRJlJifIzNlRPVB0rCdJV/cBlGkz4Gr7KUWUFvoXwDpWGM1dGbNQgkE4z0Um3fU0D0K10jDKeEHA6O7MnZE3L5Df5j9bEnGZb670xqUxdMxSjDFcUNKYrceZxYtK+GB5j5PMZuksLKt5QRTkzcKMQ==","layer_level":1},{"id":"0c1d3542-92cf-4796-8dba-82caf2f7b361","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"开发流程","description":"development-workflow","prompt":"创建GEO项目的开发流程文档。详细说明Git分支策略和工作流,包括feature分支、develop分支和release分支的管理。文档化代码审查流程,包括Pull Request模板、审查标准和合并要求。说明版本发布管理,包括语义化版本控制、变更日志维护和发布标签。提供持续集成/持续部署(CI/CD)配置说明。包含开发环境搭建和团队协作的最佳实践。","parent_id":"2148d7d6-25c4-497f-8894-c4eb7a0d2bd7","order":1,"progress_status":"completed","dependent_files":"docker-compose.yml,backend/Dockerfile,frontend/Dockerfile,backend/requirements.txt,frontend/package.json","gmt_create":"2026-04-22T18:57:39.374243+08:00","gmt_modified":"2026-04-22T19:13:23.08237+08:00","raw_data":"WikiEncrypted:F3QgleoEfoy16cQggYe9Czfqaqo1gAmvHQcnSrNK3eg3bg6SLp6Qtm0Hd5JwzGY3jDj+dSShAoxbjZ6YvxA0ahjJ7+4npTrro/p0OlyQfTnVZ5PYAZZMMXhJM063eUYiuQW88+rNBbc3mKbsAdZboYndBnd6/wybElFnP7TORa5qWxWMveWf6U7O01ocA+ZnrQqoPWB5xrRfSzD1p3yGFA3fzKobYoxAAA3xE1ALEY0vjLN7vajgFVllC2Z6uKZOE4Q6v+W+r18Pr7pLXGDJM7b6pEfFCqcEMw2fWzUAFYSUOjSyg7j9TjP6Lae0AWOmmxIDmJIIiaY0BCv/iOXgg4F6QSChC8bZFyWJATWTNhfmHAMU6tYpDXqDsfLmRpczznZ5nzTtcX+87Ndpqchh+NWU4y5OYvzEeMRCh3HcCUDs5KrOoCJgRneV8BzAxfk4gCwSo2t2D/FYg1XWGfb6hPdmWzePBCYhhIGeDQRyAdApe2Js8UhXUOD3TM7yzFPMX9q5U9ZBfejCp8TyW2thbRzOL4s5Fto9PpXg38HbnE59l0BmptA7z5bTKTp4DHA8d4NhgCeDbfNLfSJXbyYaD3RqsQt8szzbxB2Q8mvEi8iUZUARZR0lyq0oEw3cYkZRo7Dcb1n5az2OJRZwHmvRYAm7KceVqa92aKeBUlVrfA7nOuwsVH+cEXyzE8ApglbJiQfTJshljzVBvYPkz0BxhBcYrnRIjy9TlqfTs67adZyWc0wqEVZeZhuYHwwN5yID2imZ/nq7cqiZCbV1dtBUUM6ayhVX2h/cBSLpHKAwVHAg9bb9V+RxXgtSYWYKCHsAD0ymmu/Fzk3/QBvrrFiVqL4b/5CKqk6Qi2dh4pN4tZg3zJZDuyx6xzWSrsHeYGnLunZQWtHn2lr2vR6+RqzRTDiOC8SizuAjeZP72IbQf5Xyyf65hS6yls8xztLA9oDCNnSDgJ/RiNDOspfgUyj2xemyoNqf2eJ/kCDdo4/VXxMiKVfg3MHY+Vi4dVlRcSEJYLU4AYuGqtqVhTIi2ESpnPXgDPAIT+ZZPqg0hLIIPx+HNqo5qz0qqxvliLlcAhizNT4MG3p3dBRI2IkylW4gHg==","layer_level":1},{"id":"109a8fb1-6619-4bc7-8481-e28cc2127d24","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"生产环境部署","description":"production-deployment","prompt":"创建GEO项目生产环境部署的综合指南。详细说明生产环境的部署架构,包括Nginx反向代理配置、SSL证书管理和负载均衡设置。文档化环境变量的安全配置,包括数据库连接、Redis配置和API密钥管理。说明生产环境的性能优化策略,包括静态资源缓存、Gzip压缩和CDN集成。提供安全加固措施,包括防火墙配置、访问控制和数据加密。包含域名配置、DNS设置和HTTPS证书申请流程。提供部署后的验证步骤和性能基准测试方法。","parent_id":"803bfef4-3985-477e-a61c-915a246d0061","order":1,"progress_status":"completed","dependent_files":"docker-compose.yml,backend/Dockerfile,frontend/Dockerfile,backend/requirements.txt,frontend/package.json","gmt_create":"2026-04-22T18:57:40.355193+08:00","gmt_modified":"2026-04-22T19:13:58.697418+08:00","raw_data":"WikiEncrypted:1PcQ+NupkgQiLQ506NXlaizhNZeqS8C1INCZPnjb6ozpR9JAOMhzbAgtECu8k4VbH9zFlJ7iHctHtUva/hpzUaHUxVuG/rnK2RrcCUGVfbG0WKRpjG+EsQheC10ihbe51Rb+uXm+ciY+/SOxWBmrtK9m9jwSBQZQx2vXLOPvachfZTksEB+Ak0/Y5DF+vjL4WN+ym2SAYi7dW98iM9YCG0AepSeRm5DxENKRvtPR7m+w+vL7F2oISKi67pFiAATpFXSSgbkKjENZIgPfqc+pJfUREOrnHANiCx/oVaX2aOb68pluM8RGtAgyOfMp097D2Nt4PR/HN691JphbIyyeHPc82yGTjeuFPfg5XqFkhM4gvd9EdBLE3/6bh/MJhYjEwmN2RRhz7EYgLjSss8gEdDu+YLIEbHIaIC5fFS7gss/cJULC0/i3v4nnobE2Iqh3WdeIMAmynsin0sjO60LhvRmXlecODQ7+1EeQpMWju5LwTuVyX3dnr4CO4rJJxEBNYA4gaJuef2xo/ZXoun6cEIHjUq7qVNVOBZw2QtqBD2CyY3NULBP+nDD/NwjH/Gh98gl2Q0am/gLAHHoKcZfkSub1o1m1ZWTFgYn6MqyYv95NuqxR5+MSPb9C6/rwkYduPmovFsS2Alznry8TjsLdD5iSNz9MdRTR9iNV/A/TIIovgVogXlgCwnIz6EXZUgDnTeBTfTHxC+gwTrSKXUigRlIxEGksBJkzktpQA8y9mwnIfHgRDI4c8WEQRGEcKm2N3/ZPVOyBHSOHNpXK/BX8MCQqGoj0x6jusO9pJehw7KRprktlMbB0LmAuc5i0RYf9SnhsGs23W7ktzhS+uTjRB4kviSWZdHs+hgh1Pu0HsyBZxX7pemalNElQsWbvLwFNsJY1L1yjHNbBDYyXC7gZZqdb2l9GqKVgozQuK+qUUA0Vj2o6Cr4CxL01vBYMFpHALv8KnmAI2CuprX0c6PmsXV20FAFF8tIxoGfh7qcvquq/d4ONxZ5FZ1hJPYzi2xeCU0p8gFYEYYlbGMXdLCk0L5Tu0aDmTVkkrZlOHSIneYJDGZ1W/q+U6W18i48iBLvFBHD19jGHBgdzRuX551rbt4axStOcEGuyFrivbWK9R2zdcffHbiRtxe9wSFXxthLGe23JR42EF5PmZmMtlUhpV6alxUZNxguf+q/ouSz7Xn8+o6amnO60N57RyJjJO7O6+TgfHeroxL7CpABxkru/vXEJ5XTLqZBiECMMi4keQOqUbNZX8Ioct03e1YG7OgHtDSayZFtZf3kwN2ToVkxIRpLHTUHxSWLZu0YiAI2MONU=","layer_level":1},{"id":"4d5ac6d7-8812-414b-b8df-68574cc36d7d","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"配置定制","description":"configuration-customization","prompt":"创建GEO项目配置定制的综合文档。详细说明环境变量配置管理,包括数据库连接配置、AI平台API密钥管理和Redis缓存配置。文档化功能开关的实现机制,包括动态配置加载、运行时功能启用/禁用和配置热更新。说明性能调优参数的配置方法,包括数据库连接池大小、异步任务并发数和缓存策略参数。提供前端主题定制指南,包括Tailwind CSS配置、颜色方案定制和响应式断点调整。包含生产环境配置最佳实践和安全配置建议。","parent_id":"b2bb2e3c-5661-497c-97dd-15bf6ee8b7d4","order":1,"progress_status":"completed","dependent_files":"backend/app/config.py,backend/alembic.ini,frontend/tailwind.config.ts,frontend/next.config.mjs","gmt_create":"2026-04-22T18:57:48.684725+08:00","gmt_modified":"2026-04-22T19:14:17.843426+08:00","raw_data":"WikiEncrypted:aC5ZtUyEKPSxjzg//aVllKOp167MIMMk/Juev/hVoGY4+Uz0exYT+x+CbyScHHV/Mh1WUG25XsdoZThZQEtO0sLOn9+hGddmRIK1DWfKa/hxxPvnViNvXvltvNYiLNOIbKgkBxtXovEsADGNyhZh4ndOp+a/upcB9NJ4f0miwJacx2Kc2MyCkDCgUM/gJEjyoB9tRPRJVPFRvprzmaiPXJ1K8jd+KuXwrM12ogUA9iPZOtJJQTeEXQVIItOZYzY5bzAZ/mnRyVIB11vegQ+E6Lzm7D1Nza2QOZ2893Bzu61StECZsNlfecc+xEh82aNHrbFip7Fg2I3E3fqBs5EgoNXO5Wnvkcd9fHCw9vI9kMJucTIPcthOyWAFiP56zu6Hzknqy5N0o2gSAJQdhzzEvIUUKeeGDZKSY2EjFsH531gWa+Q9m0FsYWFcErqBlQXG8K06rnNNeMwR71GloOifAK0ySuik/7BjJ7xIR7HF8FOfSaOhI011z3GaEBxZsMfPc13v0joKHld4p4PRBXAfExWLlfuM6cmB/77U4xdBrHVyMcS8NPKuT+TQCTzjr2uUKzxUcjbz+yGbjVFGKMaVzKH0UQfLKeSDUnb1JNBrQWR0PFAuZsPZt730dAHdElNx9MenV8/GY+XPklTXdGMUtflT6CKf3fLy6ppMVbMDph2Ov+1Xh3z35Y5Mz3rufJasyOE0rZQMNsUu3wHtnjYprxfzvmpP9En5z4KoGEa/282c8xoLp+n3+etuGCN2aor1BSrro/ZpPTduyQtqj2WdfXDnZo1In/RRAh0TwqPze8Cd244OFfKDDAEICSSoZl0S/2l15QyTbqDNVnRzLndJswfAi2yfaVOEkBHvZuDQZXmmlf4VGsJYihVUwG8Y8bvegjLxgcD3LFGNKjzuKP1eAYLcotF84HRaFwNG3hz8Mv8puFMKyrHVKIC3UxzBVaialQqdO3oFRcrLdcSIx1JlY2qhK3BsrUtj0OLURUs0dBnCQS4ndq4NTLSQECFIifA9l7GSPXyOkGkp0+H5QCfjOl7L/3EJ2o+lTisAZggIZSZ57Ig3WxOGp4tgBh18ubr0TRNi0u/VccTbFyT97irQVq+9KNBKwPZZazaWvJEuqNSsdX3WGjh17VTbbZgmTkAwLyuCf+nyKkGUkq6MtRYwX17XkQuhceFpBmYkoxlfSWr4odGaspQ3c/AJ5zFNMigymGGpxOynWRWD5Abo/3R67eqZ9rZbmCqQbF0fkFPxdi1prJr87dubXtG1Zkj8AE/ZGXYjwas/l8NeDYxLFeir/ic+yR2tcU34lFAY/B86IHf+EBSbBYigErIEadb9gw81","layer_level":1},{"id":"7c2ac186-ec90-400a-ac1d-b3b7ca2f01d5","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"后端系统架构","description":"backend-architecture","prompt":"为GEO后端系统创建全面的架构文档。详细描述基于FastAPI的后端架构设计,包括应用配置、中间件设置、路由组织和生命周期管理。解释数据库连接、ORM配置和异步处理机制。文档化认证系统的实现,包括JWT令牌管理、权限控制和用户会话处理。说明API接口设计原则、错误处理机制和响应格式规范。包含系统监控、日志记录和性能优化策略。提供架构决策的技术背景和权衡考虑。","order":2,"progress_status":"completed","dependent_files":"backend/app/main.py,backend/app/database.py,backend/app/config.py,backend/app/api/,backend/app/models/,backend/app/workers/","gmt_create":"2026-04-22T18:56:47.08233+08:00","gmt_modified":"2026-04-22T18:59:21.004588+08:00","raw_data":"WikiEncrypted:N3UbzwGjCC3xD6BrTM0cq8Y9ccVoDb9n1FwE8c3MNy0X5y4Ej4ZnP3KnXAR2aRWtzrMaJkrDRdDMkztKRYWc3Wk1Jzwnfsgqt59so9pKRU5JZY61Wzqd2PzzDU1e10OntPwETdHfAXsLLggvksYCugHgi1fhnyRiDR3C7ABv3U/j9Z+xFbLaU1X2+FqXtTpJMe0gZJ2yDe8YIV05/f1NdFiyt5NDWdtbC4Ib9Y2H52fXoe2qIveG8dUmBzkwbHVu2VM4E9bT7go9y9+pSOyoRwO8iQIgfY0dKsQcvDtHpO5T2plEfrmFKSlwMCiiivasNoz+7ylsz8JGo5nnlfBqTtNbVZX+TrC9x539RAxL6rEZvRxbFucfobivfqV9UKh//2yYFcZ/44GbNjqNQxfF5WXJxxR5cl6crLZO089slohFwSj4xFVRarnzEA+wA45v8N+WhlaCWCr9D3DeX84ssYvMnAzwz5wUApwFIB0+dzG6CyO1yNQOBdcUhAFEHAZDzWi4UMgnwXCHBvsXx9keqXfSQYdPjZC06gegy0IfbeW2XdHsznPqPyE0TNk7R2EJ7Xm+Ky8I65Fbs56g/2k/ETmTt8ja8RfGpRbQpjW231dxIdRNdTNH2W7VkpjSFNJ2KD74a+UFY5G8eSFAvJWuEMLVCQB69C+TO9ML3zzNA5YE6/MzhYsbd5Vz6YyvCBTjMSVufoTPK3n1k/ioOdDzRFEZc8mucNTJBrEu9OGgb0gUFQFXMEsabcloQaO6ZbOBLefSgrAdlSolWRG7Bu4XyCzXOYvqrGKNPsTZQOTbVkOP9Mg6xRN/8jkyY8z/5TEh3lWnKh8ZkLlumG4QfdLnF2LHVp0IX+d/5HvT3GBtoXN+zJ3hMVEVumiVT7qCJz1afiTQZhPERVAirDX6cZ51CSo1FkRWS55AvnICqVpP+fpNLl8zLWvuuOLeA6Ab3x/0rI6b0LNf/UAqeSW64LGHL0YDWwB+G2c5oNp+bYuOVfgmngSUmDdDFHHL0k/KEoU9VN/+5f6KypjM2MYUKMLz9pwbX0qvUZadQCg/thD3v31zZ8ZroTFTEa6tAyAfqojuUXtJ/SI2l+VlpvP6dNjUfsudg8Hr7AQQescm0CN0tOYTOJZ0P+2/RmPTHviifKR/54LVuaTLEJGhd+3K5IhDTvIOhayZi6E4ARpakMmWdVfsGPv6ETkH819SOX3alS7vHgV/auGWsJcRLJinONhxTppcAf3MFf2nq8AGZR91LDCZPwV0hO8YTxygvxX/+uNJhqe5oMGvUjJvTo6xoBkb3JATSdHKXM+lEG2lbnbFjUN+bVRcAH+Qw5FdSl1cHbm/QJnwQ32GeGmAZmQ2TTHE+tdQgZ6yA5dDsvSJGTsAPieIDKEvIql2j+9eoR1X2j1mD0T1D4APKUfgD0zkNE9QeQ=="},{"id":"159f2ccf-71b7-4d1b-a4c4-c15b23a4126e","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"API接口设计","description":"api-design","prompt":"为GEO API接口设计创建详细的规范文档。解释RESTful API设计原则和路由组织结构,包括URL命名规范、HTTP方法使用和状态码标准。详细说明API版本控制策略和路由前缀管理。文档化请求和响应数据模型,包括Pydantic模型定义、字段验证和序列化规则。解释错误处理机制,包括异常类型分类、错误响应格式和HTTP状态码映射。说明API文档生成和测试策略。包含认证和授权在API层的实现,包括权限检查和访问控制。提供API使用示例和最佳实践指南。","parent_id":"7c2ac186-ec90-400a-ac1d-b3b7ca2f01d5","order":2,"progress_status":"completed","dependent_files":"backend/app/api/auth.py,backend/app/api/queries.py,backend/app/api/citations.py,backend/app/api/reports.py","gmt_create":"2026-04-22T18:57:02.317509+08:00","gmt_modified":"2026-04-22T19:15:14.078939+08:00","raw_data":"WikiEncrypted:xKp6XrvgQ6yDy/qZb6DBAyzRhY9oY9wND++XWs8abaIu3G2LITgI+Z0UQBWYGKhuTqBsuTOr5OLyKixlekyR1Xfq7usrLYQyUqykz50fvTC/eHiVp24qJjGIE/yM/QXCeZCQ0Io9avqxwyMLsy7oz+0kRVW2WjLzRO6OrvbiUY+8yPMkSPi3UCtyM0GVLTpD/1yhEPFlJimDIuMsn1U/6MxjS0vcd+yEDjCBtrAoR40VIxrMz5v+dKD/pjfzoR1p6GEjf7b6zpzZGPhEtFmhzIahThZIdAaGShFz2YepbNwjns9s+2XJPvMdDYb5mBTcQCjoCHeUfqpQ5eRpjJ47qGFAfFw7lpv7GfOixRgEasUkVhJaX2fS+R0bGg3mmJFfg8tK9XXCm0OvNdgj+aJ6EUKT7GWXDkWLJd4NWEHKvqO9jfXu7YBJxM5tfR1i4e7CPz16fSaCPJtKsI+HgIoTf1JQ+3TrBqGncHko/5j2BznPg08EvIvKnECTrZEH1ATf3+Dpz7cY5x5f+D+oBDxG88V899fkfZSrXbY4YZJnhO691mRZPnKUgr3TsGO4kK0/eZw19HvuCyKxuqWlpHvV2nWGiFCyQpLul9lskV53bxNE6LCdF4LpQgsnxp3NRkXRo9kqVcTFEVZX1D65nknWMQHMOr91wEbFu5y7eHBY/M1mlvT+FpX/uzM6NvEAUkyKHsIpLihbh/obM0dUOwFr/y/hUOLfs5gw3aMosXT7+uwJRXkzxJlNzCyzWUQoMQbw19Mq53uhogOLGeVQNV9+lYZlsF9DEbUg7pWX1HOTZw0qj0G50JKHtiM0mGIwmRxZy7HDNhqrMGXboadds7LRAL8up6y1uZya4V4Y43X61XVoJJrERnU6caJROOJ+2I5yEbNYOvo/g+dDZf6DLrj/5IPInHUF/rwxMPX3V17obui48Yk9OOgLp3s7TEAU+t97lkEPo58h2oDARfsbxrnrVd0I9z2/IJSBouFoAA5+wGUVU3BH0Wdc9bwTyjJg6t+iiz5Z14RRB6hnBI45qD3U9a5fIJ1mWWGl2mBaOfvJck5hTbq7v+Au4iOMprm6m9SIq8C9D5w/PtHslYGeBDUYeIOWciDfxj90/WlNjcdIpE8qX8PqRFKdnwDCBOD0xewCH5Cw+MxNEuc6RRDsFu6vhFjjYe/vtb6bNc674kCJn5eQOGFAo6+v5wfkkZhG1S1q5EY8GJf+H3HgtucPikTjpKAbpBkkiorIBJ4n6Gdi9Jof2lYe1Ina6yJPsnNyd1MyQgdpEg7sStrvevt6IaMoeIe0vQ+WPecPhqsIm9X4wS4nI7Knd7q8WUsBQfphwlTzUb0igehWxKagmMc8N3x64Q==","layer_level":1},{"id":"9cc59a5a-f597-4707-b994-b6c49514d553","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"页面组件设计","description":"page-components","prompt":"创建页面组件设计的综合文档。详细说明仪表板、查询管理、引用数据、报告导出和设置页面的组件结构和实现。文档化页面布局设计、导航结构和用户体验流程。解释页面级数据获取策略、状态管理和错误边界处理。说明页面间的导航逻辑、路由参数传递和页面生命周期管理。包含页面性能优化、懒加载策略和SEO配置。提供页面组件的开发规范和最佳实践。","parent_id":"d907a8eb-795c-4060-9cdd-50d31cf39be3","order":2,"progress_status":"completed","dependent_files":"frontend/app/(dashboard)/dashboard/page.tsx,frontend/app/(dashboard)/queries/page.tsx,frontend/app/(dashboard)/citations/page.tsx,frontend/app/(dashboard)/reports/page.tsx,frontend/app/(dashboard)/settings/page.tsx,frontend/app/(auth)/login/page.tsx,frontend/app/(auth)/register/page.tsx","gmt_create":"2026-04-22T18:57:04.216604+08:00","gmt_modified":"2026-04-22T19:15:16.963169+08:00","raw_data":"WikiEncrypted:olK3r0l78uP98ua79qDx7UWSmXb2E1v0sVKrAVzGYuJht4EngrBdL9BIbx+A5ZORhmqTgQcMGu36vXG77tjMwegJ9MK9XefvgLB5BmPaKaPjkXjzcGZIN3exIMkWQZHqPl9dd/vYCxP8xt0A5tqhFIvR3+w071t9O1V91AN0BqNpO/fUxL6lSA8LhEwafywlDa3vhGkpsg/FKiN53IDLy8dPSGiFG/Vz8mwss+4NonABky0MwNkdQbJ5ZkwuOqwJDkqpISGBazjjBx6d/ZZkBi9WpS5temsZ0qRK6NZMX8CXGV3QdpCjX2oNclGY1dOGXQix6F55Lh0STLmus+oXmrYyyMudzh7ch9FR+UU+InB1D/T7ft2F50hl/ynvhvJEtQU+Kv7satde3sIFR80axCozia7ebQYJMvFfWXyvyiNZi9pX8/1w8nGouSbIM/Ufe0ER296R+KbaR1x2e35Ajj315aUMNfs3vGLqkIkyGNmYsO3rtsa1uMY/D/HWAZclDtWnBZrFTxFtfjp/lBDgXrC3hezzaP9/gt4F5zcwc2Q/oqa7PJrvCp8fsUNTPWmztKzD1nIDcmonVN1eU6u9TKENaUxs+oklWWy3veyJjYN2oZOSrOE4Kb2iFm5YMPAsKwtd/wPkt0fcV2tDKr/EYqaunXqg8WHkGJmATS4fGwxIpDAo33k4HfxraSabeL7zflskEaEZ7Peso5KKLiY4GZ4cvp7I04mHwYg9VuiKfIGl5STSJwec9Ny5UcsiFqpunEedpK7K9BV1VwG701tGLy9zEAS8LBiu4SLMquX8elDI1Y8/8xfZ0OoVrqn/pTD0CqK+zGyC1alnKHfCeUmIG4/HE4zUhxLARSjQlJjlIbiAQ3E4JGDwE1Ag4x0Lj/0jTDerfaq+2ObgPKSxSTH4aT5imnxqQA5AcADQFe1Y+5BOVGDih6MVDCIFuhXxTKjr/2putI8zkjRlOMDAovunGSu9D4+ZmAXYYV1c8MjqVy7sCXdclIBTzg+0HU1ub0jqaeh2FEc5FjoJT1KKjSoa9GvgUyIvcoqDZY0JeYfNmEg98H1DYBHCULg8tlCWmQTP83Eu3Umyar/aJ7b8x/Zl5zAUeJf1EFFl0+V2tubCNtyej4KhQoZTJE7AmAt28D2lBp6j6fcj5Dd77sfZ+o2M9cEZP/JsFeR2bO2trHzAabYuUMeAb6BJZX6enXNsid175JkLlBhRgVKS1MbOnkO/mDDp6/4TqsWKKG3R5Z2+wYd94TW97PCIIAxY6/aXVCEOBvAjJ9QrxO5gjYvaYvX/kXL8/rGUAswSgn10l8i5Ayp7pFuaLD5U/K5VPPsAMn2wPmQ7sUQBb4gBSsv78y7arVTn1bJNNwxaHaDeERZvOWiqWMIwWBIWrQs83mti+09IyPBsX5NWUJw4f6HuTDD6zEdVpDKXlgfOHNQ3l9I9iFKoBAFTFaL7boH1XfECuootroao/tWjr5aktn0j5GvzoA==","layer_level":1},{"id":"f70f5d9b-d7c7-4dc6-b36a-5f4508e6acaa","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"系统架构","description":"system-architecture","prompt":"为GEO项目创建系统架构文档。描述整体的分层架构设计,包括表现层(Next.js前端)、业务逻辑层(FastAPI后端)、数据访问层(SQLAlchemy ORM)和基础设施层(Docker容器)。详细说明核心组件之间的交互关系,如前端通过API与后端通信、后端通过工作器调用AI平台、数据库存储业务数据等。解释数据流向和处理流程,从用户请求到AI平台查询再到结果返回的完整链路。介绍关键的设计模式,如适配器模式用于AI平台集成、依赖注入用于服务管理等。提供系统拓扑图和组件关系图,帮助开发者理解系统的整体结构和各部分职责。","parent_id":"1bb55e12-24c4-42cb-8f59-ddc303460d45","order":2,"progress_status":"completed","dependent_files":"backend/app/main.py,backend/app/database.py,docker-compose.yml,backend/app/workers/scheduler.py","gmt_create":"2026-04-22T18:57:13.329904+08:00","gmt_modified":"2026-04-22T19:15:17.452724+08:00","raw_data":"WikiEncrypted:gWB8HBj+8+/15rQhXgtMjCy7qJxJBEw3w9EAqc5Otte14+SfcEK3DSbmZRhW2TgD6BlsxDXHpLcSijck/Nymh09xxwlFe9hXY5fsIsj1IZW9egIbof/vmRQ/HkxJSvNy7WDeHEuuU7TDae+7kls5UELL0/oUUiCC4fjizzoaOlqMuOvW/DImO8fA0xbQ2wAt6L3bqB1pcGWvnB10d6EKKhQYmUj8xv/2M3cUBtDMkUdyKbAuduBIInt+jZnOGnXKcr1w3W66u8elz0iqgWhMCsMFd54QokMhn1Uyguz3NcIfEVH6IlKil0ufINdb2d/QV+9aukLvJ4Mq4YDlUjQ2iJ8oxAYYUb8eIjBnsLHPfBnMUyXVAo0tbyNB7wUmRb/L1VR7ipOMMKd9mEohzKGpsTxHpM8my7x8Ryc9N1guO/H766qiiNij1HNCu4rWVW12oeTTjAoEKdLzLyn59Fc4QjYVkS/I+r/lwRtMNN0OGjWXGXea+HUaIHwW1088PW3eZj3FlyGEsTiYb6ysJAWanY2qoIhASm31o8s9DekhdxTRbWeBCNzGN2ufTYGM9ygKNDHF5dCbVW0/Vtc6FC/TDiflhIaDY44Gvuu84O4l7SRBl0jKM8uCFHOgx/NzDlnUdNFaSOdjUBxQ7vVyi0Q5X6P4H/ERPzJQvOa0gmXW7S1ry0P4/CRjOCzq0Afw+mAf2j4PHM/4MWFkvrFkycbEkHWoWnPY+DWYI00TVSdTWjj4n4l9wbj1XDhXavLdg00qHVjv0dTq1ghcZKv3C04T5MfvVQjMXCKPQaK0KzwZDfXydWnlyLPgM8t95lTETMkjjEpIMtiv2w1gJda4ChFrJq27oPmVlEoVzGPuuk2B4YN3z6A5y3ehJpejis1/EuBjd0vXHhwghZeUv8mMD4smRPUMA8GcEeKVs9TQgBHqVuuVDdEAQfE7w1ILarmq3trYAuc+LH1KMUyPD0aZEbhKZdMRkFYWLhfyYeuazm9SfO1BqtEuTVoAXdLD86TCQNmxxTZPgU9OB9JluopAMt1oszBwKK/88UqwBqgUZle95RTKVR6QPn7zAGZaDbfsbYvsGUfNBSH08o6nnraxB0Vpj+r6WaoZLCcxhqSWoTp60YlMt7BDsla0Lt8r6D+a89AtCqZOHLJbrEx6T4wsmiykEi1uC/9XhtcsTHuytq/rHeDWR/bTToB+mGjBipoGscvIFMgUJonYCOjLDyBPGpeWispYZV21bIJQtFIYv06NKYU/IV9ZISLel9AttowTEXv/Y/U1xRtmaTp3BgGI4iRdwXYMJICv7Jyr5kYd2CrLJQPzEBdkJi7+q3WNdTCWBPd3B3hL/tTL2lFNDOc1sqMga78PcdWWHQfheORW9OfH3cDRiq85LJziojysfwkQVqHY7niPkDyphAwn9F2xZZl9ZfahAaU07CgK26wyavgkYOAU0dlWI6WrAKE2gQOzOxBL","layer_level":1},{"id":"816a2805-76c9-4f32-a3cf-96428208081e","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"数据模型","description":"data-models","prompt":"创建GEO项目数据模型文档。详细说明SQLAlchemy ORM模型的实现,包括模型类定义、字段映射和关系配置。文档化每个模型的属性、方法和业务逻辑。解释模型之间的关系映射,包括级联操作和外键约束。说明模型的序列化、反序列化和数据验证机制。包含模型的生命周期管理、事件钩子和自定义行为。提供模型使用示例和最佳实践指南。","parent_id":"243a1a45-d31f-405e-a3b9-694d2fe617d5","order":2,"progress_status":"completed","dependent_files":"backend/app/models/__init__.py,backend/app/models/user.py,backend/app/models/query.py,backend/app/models/citation_record.py,backend/app/models/query_task.py,backend/app/models/subscription.py","gmt_create":"2026-04-22T18:57:14.161024+08:00","gmt_modified":"2026-04-22T19:16:35.092798+08:00","raw_data":"WikiEncrypted:klcgW2PbPxJambbKMzvFtzxL0nPpsuPMPxr5hj83lS7zLbVY013R4TC8XNEUYbb5qxIyzNIzP0UIL5gcgWgb2I1H/zkTZLUIt5Q8FaGG5df/WBhqzCqyAPPa8xIyWPfzmckjD/ciao/Odmp26hYmiZaUfY04+hQgG/SvVqQHJaK3xfn+IMLkqHHgKMNRTHzng6LCRXTHlXRhXHX+mR1t3xxlS53M1kGU/3OC+gxRPXiI1do2e7R6mweCSy7q+DHMOad29LVnmoOlnK0nhNr/o9GqvDhXE1m/7AFT/B5q+cXCwr8ysfhru2lFLdHdaaZO0pEpADVKc7xmWmzFTaOfq+wVN95ixxIjo6XLFN5+9Rwtye27zDvkmouaHrmGFCw/QKbsVlcrOOLbvyPV0sxnayDeP1+/IwiBVAy/jUFRK9MzPUgslk0toI1LtXVmZKzKIKHms5iOx7bIO6u+m51Z/Ky1Pn+2MQKpmk8s7Cz+pzvIs1wbLo7ksk0vIN5HKvmxGPj45VwsABnnkBO5vPAUQhgkpfU3CNAQB1OCJ3sudMcnU9S5ByuyxHvhuRsJtzaJ3LxTcD7qHwFUSfbYbcrYLdtgFs+H6VY7Jb8z7OR8Yc5HLD021Y41z9AlTHo0Lg7UHLakKDN7h5j0UpjgjNu+miZY5QLbGz0Riib2pVXDNRyfreEe052k1IackpfDqdZOOGg7ZJRFtm4oh0B8ZBQZSOAFJ5HMrk/1DRPDIi17sXSDGCXLriG8XNCASQUUUdXEwCrmriUbP/d3hIMu3maQdE5zf7Zsf2JTcaE6qIVzQQnlK2cXvqDF6ehoYQTqX9dr2MDcOin7HvkxOz6ZsBWG1iU4Jae4K2R3H9oE3ifOymUv+VzBlrj6ZDTscoF0cgwwd8+YjeE/S2QAQLyZXp2mUBVkRVbjJue6mC1nCdvvNTMhDFLS1FHZfZAfqGvHikcoZoYLo6AFo4VEMwqirkJVitLTUYDLtQgw66mTfWayyYrAoN69gHO+XrIvT9UF8oXQMLnli5wNzBtvtLlFR4aV+xIHZUtv/19ysS8XspHgPFsia5qXRPVH5p+0b3drONQpmWt/kg+5IHl4MDI6rjdegulFFkPBcki/NsZYajF5M+UhR+Bb0Z/+wgcIdFrQifArRXi0XGPxQ9HczdG2R/v35Fihp5Tuj0SLb4YElp2ZLc//zSoEIPdloAbbDYeiPHgx8/cE2NvG6UdUK8AHaMNOATafLYSc0vvxkeMfvf37HN/uPGyIH0nPnQEGIGJncuz195GmTfQcXRjjZW6w7VogwQ==","layer_level":1},{"id":"64cbb894-755f-47b5-854e-c26c7821e9b2","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"文心平台集成","description":"wenxin-platform-integration","prompt":"创建文心平台集成的详细文档。详细说明文心平台适配器的实现,包括API调用封装和HTTP请求处理。文档化请求参数构建,包括关键词处理、请求头设置和认证机制。解释响应解析逻辑,包括JSON数据提取、错误码处理和异常情况处理。说明配置管理机制,包括API密钥管理、请求超时设置和重试配置。提供文心平台API调用的具体示例和错误处理方案。包含安全注意事项和最佳实践建议。","parent_id":"7e66719b-4a8a-484d-889d-10fdc76788bb","order":2,"progress_status":"completed","dependent_files":"backend/app/workers/platforms/wenxin.py","gmt_create":"2026-04-22T18:57:14.59287+08:00","gmt_modified":"2026-04-22T19:16:36.925806+08:00","raw_data":"WikiEncrypted:f9V8T4at/ASje76Nwqwyb9VKSms5IpkiE8NM6ot6FBQ2Xm/W7xVNoD2HxtG0JIBY8fzg5rfp4P19y9GFWE3P7uI+Ntusv89zObilbespnCZi9lgSyzvJAmMy2ZXU36uG1n/fT5ySk4uXzAeM03IITw2ki55c7blR7ctykkDRBz/+KuT7ZF0DOBoduWD36B8eEgythbdQnQk6QKdUO3zYxMKpkeC+pXgvEdIShh1qARe6y4tQ2QvzqtKnSFLAxfQRLFpZ+M74S7WgYzFsfmB62qvpFd6iqQ/c34o/BAn/Hk+hmijJtD7JrbAb4BZHqO3M37l90T1UeUJ7ca3kxzzUVlBhJelV2EEMzu0TdG94oRWD/uUH+ghzekg7V4QONvz6FgX6S3g5dlnRmJ3uGVAb9UkWyg54V2vm4Flr1Rckut7P+eI0PwFYHVVS73QDzbs8rxY0qTDqnSVx/avS8kdcv0Trke0k4rFSsbgl8gVc0YnalN7uE3aWOFqG3L4Uuv/doKg7kRTsuNOHcemyf3CxJKAWqRGnZq6kAZXhAIPB+TVfZtUZBfqXceE8XhFx0TzfsTc/CyJPCD9cMLl/dndwGVEVjImXOv5Z2ILdMDbyiHuP1LNmS6opA4RZav4bChAA4vMKcLsvlpy1m2JXX7ZrbSoaoyPlArsF4y5k76zolOB0rT3mchlZ1RuKELnC3Zx5UyvIIpY7A1/YhLy4OGGFi09mrAxAr1U1pmnM8Sefd0ZG8wLo0buXQ1yyE8pVZh/xwTuD5FtS813EB8K7f1KSJbjHW9PaaCtfFr3QVUDhXFZQOe/pjryeUUC/FcKCnLNd7YGb6sjFaYvmITf6kZQnmulBS/erxUZbTiXDrI5vHrG8/FJATWl0MvEiv4xMovu4nmOJtjrbhfUlI/8+JzGdmo2HJ0v9Nle1SjnYXJuOWCTwFL5+tcYW9KBcfsxRdQ+8L8vJmfBQ+utNXUpmYn+DimavX86+plxrth8vKszohk4Yyvid24iSykfrvEBlbR1p0hBCSfT7knct0F5qUHa+Msj/zGpopBE+WShRW5LA6Pm4DVHTlwZHViJfpOH1tZZU","layer_level":1},{"id":"412f8cb5-54c1-4f32-8966-fa0e5e75bbca","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"性能优化","description":"performance-optimization","prompt":"创建性能优化的详细文档。解释调度系统的性能瓶颈识别和优化策略,包括并发控制、资源管理和内存优化。详细说明异步任务的并发限制、数据库连接池配置和事件循环优化。文档化调度频率调优、批量处理策略和缓存机制。提供性能监控指标、基准测试方法和性能分析工具使用指南。包含实际的性能优化案例、配置参数调整和故障排查技巧。说明如何在高负载情况下保持系统的稳定性和响应性。","parent_id":"7416db23-312f-478d-8883-2fc3c41ac2e8","order":2,"progress_status":"completed","dependent_files":"backend/app/workers/scheduler.py","gmt_create":"2026-04-22T18:57:25.383759+08:00","gmt_modified":"2026-04-22T19:16:37.81704+08:00","raw_data":"WikiEncrypted:9uOBpMbLX4DyZqW4us3Wm3Q7klXxmeD+JMMoGiSO2oNryWBo1gw9kLkawvxQIW9CASZSiyAjiA/zeT5DwD4S8PDqEsVNE73i6DrCAWvPET3MMPQasVfJTaLge2tk6gbYlx3R6JKZcUoi06jS7okoLDzJAFru3F6zguqySliKZSG/00oFVcDHXlZZc0L7hiq11k1jOD8uBl6n35lAvst/AgFv9MUzrhf43XDwtHiMAxjCAkwKnrWOow44Lwzy5fG3nWKZLSHE3/CUmZ17U3GmEqfG43ai1yloAkMou+sTRIk73sRdXPDze0HhNdNfNXTaOHTxW0gT2TwKGHgG9Jt0Y+1qWq4Nxj/oJTP/hztBLy5SEPndXXqeZTkBaesRTr0adQv49fH0QoFV6D6dxlEOodMiFhvvYd1rvR+HwbQbqU3W5UDVnKfPlMXHsz0a9NPu4aGgk5yevh/s0AP/v2R8mgz3N41lETHxk6Q71f35L04S7u7P6MlorIwKzkeijhFf8sPrChMYX7dwf8dWba+Nwm6fgb+9AbSZ4VczhQInnbiv+GUKiFnezNVSHUUXqTCvRIuhF52jqnOqjPei/1oeS7xAMmB67o3v2so8T17xg94F9FoEl2XuuwA+RFDarVZKACDTPxjt70OXdAoxoERTZ3ivU93OANsuuWx1x0HgFdBj0q6seNC8cbySc3ZuQBWGu1YWMmyIYGL5l6L7CNSmkkl9/pkb1TSfkyM+Hk3VLXy8VIvtgHxGX2OU8yX1nX0DCGS9Yt3dK17lAVH2XF2mo39noQPG2nxjOc+lO/O9cL/CC1Bqzd51YLHapcRLPPbuaMT4ljyLvQ3GMvKHB6c0sjdaKjykN96LhXpO5PAqs3N2edkVKVMC6lg5fTBMZi6B8r4rup3M9CPDs4ehmVYTtnZpt5zKxyXP8QfTO1FfDyNQRpbXj8TyLOwwqEFvZPQlV7Iupj3lpi6crQhtpXBtU9M+vjP31rOrdFpBHXCckE4T9QryVWvQuNDjsc1+tYKLgF6S8ixDQFCHEAfzctdMM3iiLCeBy1szBZEZTE+OS0ERSbgqusmZBkz3ouKp6suUgXLJp3Kystu+dAVWhA6BAD2WHT76h+Hbazixj8F2qAoxmUb+P8Z7u0grbWJksg6u","layer_level":1},{"id":"40ac97e8-7ef0-4198-82d7-d2e332be9d34","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"测试最佳实践","description":"test-best-practices","prompt":"创建GEO项目测试最佳实践的指导文档。详细说明测试代码的编写规范,包括命名约定、测试结构和注释标准。文档化测试覆盖率的要求和测量方法,包括行覆盖率、分支覆盖率和功能覆盖率。说明持续集成配置,包括GitHub Actions或类似CI/CD工具的设置。提供测试调试技巧,包括pytest调试选项、日志配置和错误排查方法。包含测试环境管理、测试数据管理和测试报告生成的实践指南。","parent_id":"fc6f24c3-594e-4153-854f-19250834eeb1","order":2,"progress_status":"completed","dependent_files":"tests/conftest.py,.pytest_cache/,backend/app/config.py","gmt_create":"2026-04-22T18:57:25.616954+08:00","gmt_modified":"2026-04-22T19:17:48.81542+08:00","raw_data":"WikiEncrypted:3lrOgsvw7LaJXRS/Bz00iEsb4RzJUAxxpmqaBblS/51b52WVRQtf4YGBQ9MC2MYcYNyKxRGZljd03HKmm078aoRU+suxjG3HIqwkvM6ruyPxwlLdBEn6ogbrhm8eOKuGWjiEso+PrOqSXW0boT4qGE+/L4MkPiyeOB9YnDB49/ByvDHsjeesTQqGwpa9FmIQtVqpqDZA3Sx+x8TB+MSqT5PMZ4egRgCW1ODcidZrTriWvseFNQDFLzo8g2H/HF5CFLcVsZVK+kWI7b7oWLmNlIKz2SyVJuYqvCblPzw+/773Wv4RVHYdK/pxISCon/L1KjPyfZjddr5sDjJO7rNFbBshPxUIQhtXnnAEqLOpzZwHm2a0bA3vSR42bZZoSAY0v3aZxXQ5IvxNT/aNAmiFzd4SkTlz0okDXnBsArnOUpLLRfjLhxt5ueRUi9Dl2edsdycd6XznT6/oAUHTf05zhhKC6JR1OsKiFQyoo0il7LYjDxXJM0dZycDlENYwZ3Bd/82Qr+7VNmgpLdnB/7z9DPG9iou6TIpztKrdSRYqWBcl75qs2L/R2Sy6MOyfSwALTRUi5udf8sNlsHOk5y2qY0h7vd1GzYwtxKQ63ypJLL/8fAcPKmH3FCwKvrN3zuEu0Meyq+y2v8eYeXNozeOa2zum6fI5sw7hnzX9Jqy7fyQD2yBFqyylQ1ZW3nwFd9liZLnWfm7zbLDlqSgzGYGxvNIe0X2rhwsthMZTLEXOZVDW1/NqHg2E9qXOfg7XhQRHKZz6Wu8eZ6MGyCvq6ft2SJI2vltHD0M4somneHGSMo4jKY8rMeGl7qkW1V7aaurBnh0xp6cOJ25IdbN4VEDYm1Oj5nlM4wnEQzWiVyEqA+3oU2k2k9Plm/xs6g846/lBJxHL9ztsKmm0BnrUFR1ttgTVRAFvHpfkmSgLGGB/9824DJpxXMiWnExCaZzWbFYPhXnDF6Kh3SB6P3NyVwVdC9/pkf4/UQ4Em3rvbVuWICjGSScoDtOu5MrHDaUSbtN/XtqOptA1ApGbnTSKxOm0KAck4ApFPdk6aRLdC4eRf9XWsWUaOFjkqAoU0Kp035pHaGrCrx4KmgoKBNFNbS55CA==","layer_level":1},{"id":"41a414d2-e13a-497c-8a03-212624dbf5fe","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"引用数据接口","description":"citations-api","prompt":"创建引用数据查询和分析的完整API文档。详细记录引用数据的查询接口、统计分析功能和上下文提取机制。说明引用检测结果的数据结构、置信度评分和品牌识别算法。文档化引用趋势分析、平台对比和竞争品牌识别的API端点。包含数据过滤、排序和分页查询的参数说明。提供引用数据可视化和报告生成功能的接口使用指南。","parent_id":"dd12cc44-0c95-45d5-a726-e7b8bcf2531b","order":2,"progress_status":"completed","dependent_files":"backend/app/api/citations.py,backend/app/schemas/citation.py,backend/app/models/citation_record.py","gmt_create":"2026-04-22T18:57:37.836961+08:00","gmt_modified":"2026-04-22T19:18:18.055339+08:00","raw_data":"WikiEncrypted:qByx+WzXalKNnda2aYUs+Mi+kQm14b3+m7dy1helFRbYWzECOnPEzHl0ufCZlQvKjQqSxHIwqppPXSdvXYc/8Z7buvJqBV/Q/faiP/SBsAH3RrfF2Ppti6QanNvcAtBLcojqJkmfYWFej1OYdagzBcSAu0xIIA7qBG1LwkdNG8crxSwebM8is42SnI+aRvlbF4UdAqHHPdfUvnzs7zIODnildIoR60AvLFeNEWO4prQkFyS16p1YH96SousrFgBu2YZZYrNOOhDXyrRoW3TE6990oaIi3Xe0B7jFtC4pce8JamwzT8B33FAJ6utCOBMUkFKq6l0kD5rN2owZaBtGc4Q66Xcb62y3arqrvxcgE1ox1nAp2D9pNqRy3W1ePW2fWZk4+3co4z2+ANQ1JO8q5eAFI6Q6zG7hM4rvNlL07iD+hZeqd5+vGw0KmjkN4VMqJC5ypFf5UCF4qHgB6TS9ToQUAWk6HWlwBR1DVpCcK1kyGcunn6EiivJyjJLRYUXI66AmajxIzRHtyhxBnA5Q4hZNgr40jHMoUbWHmiQTQr7Ca4/VDS2V5FWlm6K1fN8xWRIUCI3VtH0A/wG+QBnJmF5YeqDZ37Nx9vkiaO+hutXsGHsjwj8+IZO/gpn6UT6QpOGaeqGl37HW+lMZSRUqFFaLg7jxlzSa3IliOyZI7N6xjFFFTrLV1ShVsDcWDg4eXL2IjJ7QfXjmD0vq5mH8mkZK6HbhpNH5kFXDuMYWPIHAXUk39ttNvut9r22RLpkXYWtxE7XoqXW+k+Edmt2eNvZImq/e7mUwa82Cae86WiRtz7B7TJeOf96EvN6EQBg8E8KT5Ckd1exHhFtySkw3tYfChBZfD75zY7tUcZcC+McO1Kf0ORqTZtnNjVnot7wirysiS6WKQ3O2L0CeBUUV7DlmFd8xAgJ8YhtHJKulJ2B0yCCswfb+PR6u9GKFVK1EcspZe5BsT22vlhQyAJxlqiAi9xRk+XaCWoUT5ymT+KMOXdLoB4EvD2ftgXy5GMxZIK2q1QtLAYEu7nOBsq2aNAg9i9IsVJ6OlCN7VMzDinzkoC1IAWKxKE67ZeLq/VfPmCqLKGl0z1JfkfC/kU8KFA==","layer_level":1},{"id":"2f7fa0ab-cd3d-4f45-a1c1-389d5a0c2561","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"开发工具","description":"development-tools","prompt":"创建GEO项目开发工具使用文档。详细说明IDE配置和推荐插件,包括VS Code配置、Python和TypeScript扩展。文档化调试工具的使用方法,包括断点调试、日志分析和性能分析。说明开发辅助工具,如API测试工具、数据库管理工具和Docker容器管理。提供命令行工具和脚本的使用指南。包含开发环境的优化配置和故障排查方法。","parent_id":"2148d7d6-25c4-497f-8894-c4eb7a0d2bd7","order":2,"progress_status":"completed","dependent_files":"frontend/tsconfig.json,frontend/tailwind.config.ts,backend/requirements.txt,frontend/.eslintrc.json","gmt_create":"2026-04-22T18:57:39.374817+08:00","gmt_modified":"2026-04-22T19:17:43.353691+08:00","raw_data":"WikiEncrypted:F3QgleoEfoy16cQggYe9CzRj7niAsR4WPU/tuSJ2vBrZ49vjzLlsqusYQAR0/FVC/Mfa58hrglY/tUSn8LBYDGZlVr/UyLd9/U09SEPTjzLPzOXWOffceGYhaM8E9Cx6c+FHZRQ0fYUDQXxWM6dQF6WQbUmiZISlUpgtOkT5wX0oc/n1YeDUxEpgiCWnOdIBH6SDoRDdGUFrWdqF66bEdplp0Yy3VYVBKKwUy1XVXtLbdv1T5/0TVJO7EtkHdoheIwKCPIYHsaJqBv4U8cMNegSJ1Ti8bsgUakd6FM/X0ybfTay+IyTIqYXj/dS0tWMm22zjLqYmw59+s8gJ7AlgwXhqYhM5Ycwh/PNrDHHFbgQdt45DjAaMWjrjXrbIvM+kXuSiwxW0dEvQtouckPTVohVcRWEPbg2o02/8Y5DzmxpceOC7KgybfaaCQeJHbg9C91itVc+0NvorhivJt105EYs1ndSOla9hoFppywKIbIM61cvqgHjsJF5DrN1pneGmTtYxijl3J/QpJHn5QuIBR8ugoHdJ3EVjUR38vQReVBeDxI/VMe8k/l3EI/Zk7paXexGeTrJmmnZI1I6WbPN1TPS4nLFZWwkA5ZA7wMkdHiHdnt/FZGccFyq8X9Okog2xv6uqH6Kwz3hseWlrSygNPzDr0cFj2EQqDH7+xZDEkAvTuFLHn+HzUfBv0vfZFa/FRg+5/agC9EcMHM+75jEvtSeeHXHOLpuuFcyOcZ0IQP99QdzSEsLaISCINs5SKkzKAMQnJv4PEDo/vFAKBulCSqjXmLeyhcKmCGqafN7IMWUWF5pmZuXe7wL1F6n+x3+RvScmsWND74hQhn0/Sn8kpFHJ5xV/5t0GDZxWybDFRRuFC8oImKyB+6h8CstsERuFeiIjMv2TS5lz8+eb1/yCUkGO9X5mUpJZXfZ+0vu2SkEN3WR92tM5Di/78rDlulwZ4oykT8NzYMTzpX4WlgUfazNKLyIhSoFZcWUyOFcU8IWCoOdrBFn5CSpe0VXbOlP3pAU1Lz6Y016JUHo1t1YE9xU/m7OTxhMexA8jCoSswgGT+Cjs9u+KP1ji2RSyZHQ6","layer_level":1},{"id":"2713d5c6-c6b0-4a38-83f6-56940c2bf695","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"监控与日志管理","description":"monitoring-logging","prompt":"创建GEO项目监控与日志管理的完整方案。说明应用健康检查的实现,包括服务可用性监控、响应时间监控和错误率统计。文档化日志收集和管理策略,包括结构化日志格式、日志轮转和存储策略。详细解释错误追踪机制,包括异常捕获、堆栈跟踪和告警通知。提供性能监控指标,包括CPU使用率、内存占用、数据库连接数和API响应时间。说明监控工具的选择和配置,如Prometheus、Grafana或云监控服务。包含日志分析和故障诊断的最佳实践。","parent_id":"803bfef4-3985-477e-a61c-915a246d0061","order":2,"progress_status":"completed","dependent_files":"docker-compose.yml,backend/app/main.py,frontend/app/layout.tsx","gmt_create":"2026-04-22T18:57:40.355514+08:00","gmt_modified":"2026-04-22T19:18:41.773778+08:00","raw_data":"WikiEncrypted:HL3VqGjXq8A3aGeLxAjVPg+lnGgkT5PNghnzp51sgLDjuJwvzPZp6KDp64sXTqk/PvlUYRCEe6pgM8WKdtus6ThN6y8bzS650UmrWXRgW48k0tbm2PEPd8NIqpzEYwRwyuzLECVz8rV+mgvy+OHzvrt+wEQeT7z1uljirnxeLZWJjIrFBr8Q1T8WQ9InM3Ttnvm7OYO+YH2Mqi6IC38bW7w4WGqApS+Byn7/ZrJhR3B/XWh91FPwq0GRUJ9cqrNuIcYtnWAfVIHWOe3iET8Jw+Un5x7ZRepcJlINGIIMBE2hO7UsbC/K3z8rkz7mx6uoPTPpxkKkgoQQ1OAsShE/wpHpRYSOJEnV26GFo4bXBTk6xJYw7daKnJVIYDSqSrtJQI4yPlTR0OtJescXSLobznoVSBo+L+PU/nRvotp5KzX0SUpbkqNzGychqcBuJhpS1eaEP0cTBjvjC4MJ5Lsye51X6aOdYYC7tBWHxtgBO93tkpcIRlHk8Dfmzh8nKcD0rPHYGnaeFpzXgGH7ZfabAwxiMhej6OXWsfnbHGg4gheWhXfokxCMKkFnzrsYcuFtB0BqUeadjP5LArbJJGeyVEZovmPxtzQ5z/ayxJs+oyc0oH2SMXCFtIQv2h03ZSOCkohVRs2oTJnMI2p41dY59B5qiSOp+0qwxTWL9NnpqM2gjlLZhcgwF//R7ag4ndECWejInOcA9Ayvx6cPAuH6uEoFTcUOCLaOZ/u9T8890S5KHS7PcNM8dEewoFviz/mRiernAhd08l7a9lR6zck842Ywi5H8+fuuHyt64Pwco/+52MhD01pkK+HWSR0EqPc8URlkOKjQSW0jd8E/9ow54aZA0+1Vto3d+Lqi/tAOlhRF34rIJgTEN3f/udCEVycON9KNtssYzZVNPdvTkvzPYdUHIcnYmz3jbQhnYnrYvuaWNvO0cPISLZI9szxLP8IhmUc9xT4U3OvoRfFka8TABGicEUAF6vpH6H2rCqnWdBydaTpRTa3VAR7QJ+M0aPlzeKov93eljMa/djYoQo+w8Rm3A9vGmGXJKfWdZobh+fQgc38Z/0yRCHnIPnY8o6CDBfwbANfjDRpjyVkqWq1cuqYNnMul/jpzDS9vMSbMnefISRuF0kH8XS5C7pIB+WFn1vxeYFBYED7ZI4FD9pxxS6GWGcYOXEoFcqtR+lEiMdj0Q09jo1nEuMKJbPQre/RG08O0boFCzF9KOSnS0UA09/82vFQyQZKHNwpz7OkZZa2cX5X1L/pdsDlDRoIdp2lzyAEOidStaIWywEj11EYVNbG7sK562p96Sr15ueIXP0s=","layer_level":1},{"id":"c8a468af-2982-4d9c-82c0-313b5d2ee89c","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"第三方集成","description":"third-party-integration","prompt":"创建GEO项目第三方集成的详细指南。说明新AI平台接入的完整流程,包括适配器接口实现、平台认证配置和查询逻辑适配。详细介绍新数据库支持的集成方法,包括SQLAlchemy模型扩展、迁移脚本编写和连接配置。文档化新认证方式的集成步骤,包括OAuth提供商配置、JWT令牌处理和权限系统扩展。提供插件系统的使用指南,包括插件注册机制、生命周期管理和错误处理。包含具体的集成示例和常见问题解决方案。","parent_id":"b2bb2e3c-5661-497c-97dd-15bf6ee8b7d4","order":2,"progress_status":"completed","dependent_files":"backend/app/workers/platforms/base.py,backend/app/workers/platforms/kimi.py,backend/app/workers/platforms/wenxin.py,backend/app/services/auth.py","gmt_create":"2026-04-22T18:57:48.68499+08:00","gmt_modified":"2026-04-22T19:19:17.361215+08:00","raw_data":"WikiEncrypted:WxCGFGSl9zR6IDeUdkqKNACFcGLyx5HNn0UQzHtkOJVoVN2BLrMtPbbNkfhYYzEQGaeK3o4fBODff6YTXKxAV6HYhCHjmdXJqBaBsEPBB205lzDAllfBZL4/6ljz+/HZSgaWAxPECD1m2GUFvjlKHTSXEb0x5iVOg9R3VDjYQgKl1SF3S/AuJj7QBbGiwmkyPrqO8+NXDlKH7pI3G6me5ulZrK6M85KSH8nVAnqY/Mvjc6UGQJBwS1QyKJVzzK2JwKGhQSSLemxD887b22bSe7q1IEDRYcpqkbfyICB0zF1Gg1h/cAfQgzjiG0+MQiC5CF3QwkFteFbN4KZDEjCTcbIR+HqI+yCs8Wue/MwdoTYQQKpR9FyClONZn5T/UqHQaVT8SemYF0QC/kawyEZFH5Tkj/6RwS+taTWw1fx2QC69xQlW04ManahzsBDwfz2W4RkufM3ZfW8HAN7SorY7oYr1Vam2p2u/93jzHmi7PO/Y6VphQNACzUlfRYsPALVBkiqpN4TIiicrjOvhgATUxqpku2MrAtbXTkiuiiQBamK/XrF4JNlh+79VGoHKWniah1nvo+dzl7jll4j0woKwWKIzmfPXLS68HekW9h5BH9SUeuyAEW/pvnGAUw8Od3CgsEmap86k42Tg9pRZC/sT9bCWUbjO6ijJ9IPAgWE6bkLCu68L2BD1yKE3gfLg0hJmELn79lHaFCUc5jlRdJ4HEsalAFGS62beLGkude98NvSY5pQoO9jbTj3Da8t6Yj8p97WtE73vmWW9Pu/YyAtxJH7Cdwvv9mbvLYVnLzUWGwsaiaXmDJiqfSHiOrHQNyonHQa8N/LYrbB8qBSUmLyBUhgI1YfYMa+zNfCaeHvRLlmVZpui5MOegWen1Z61WhPc6WoFYHyFAdgS7k21cC9jVay3u87SjSzooFT3MJjN+yMIjrQ5U+tVFbiBQK7IGPLFHRmvFBrBwSFVBI5VnCxn4piz7VBp7mm3e6N3CJUKShQWQT9tmUIRXLmmzsKTit/kRAUrycFjixIK8Hm4kSwtVBREbX3opXE6clXKGVB0bpSDUzB8Ae7wOPE8Gqo/ewy3kh2mdNnvI3+MfOOgPc+Ovqu3KGFBH/MYKl2xIUGdYtEHfHRCIFFVVknX8+Q+tfbkmo/Tw+kJfq67IQO6VT1WjP/q1G3rOhp0Ah5h1wmxVfdIaHSUOT03yQTBm/tqCXlgUW7rZbTc2wF0zdUUWleQoq9pc14c0F2SNFCSaQNLhxvfeC2aH8O9nuZi+kVb+GVV4vWcxcz/7dfTkXpogDwy6uqVHySe/TAn04uWlCabYSrA3ahnl0VpBmalBU1dtaTG5apgcCJ9PULyHWKf+iW0vle9pOVgZQ4oUv0vXxDpZQkO1SOBCfKG20swUEItxbAxIVoMowR52maIRnCKh3g18NF3JUZX2ptDny7A2gmsYWU=","layer_level":1},{"id":"d907a8eb-795c-4060-9cdd-50d31cf39be3","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"前端系统架构","description":"frontend-architecture","prompt":"创建GEO前端系统的架构文档。详细说明基于Next.js 14的应用架构设计,包括App Router的页面组织、服务器组件和客户端组件的混合使用模式。文档化认证系统的前端实现,包括NextAuth.js集成、会话管理和路由保护机制。解释UI组件库的设计理念、组件复用策略和样式系统配置。说明数据获取策略、状态管理和错误处理机制。包含响应式设计、可访问性支持和性能优化方案。提供前端开发的最佳实践和代码组织规范。","order":3,"progress_status":"completed","dependent_files":"frontend/app/layout.tsx,frontend/components/,frontend/lib/api.ts,frontend/lib/auth.ts,frontend/next.config.mjs,frontend/tailwind.config.ts","gmt_create":"2026-04-22T18:56:47.082624+08:00","gmt_modified":"2026-04-22T18:59:54.994275+08:00","raw_data":"WikiEncrypted:rJ/rIw0gVau8jPGqKFBAsMR2XDSFypkKHZQ6YBOgiEA0UOTwucBLEBMXoN0zA9Bs5lWPUuoWW2QeM8S5V6dxWDHhuxV1hyEK1ZWidiLWXQ0Ty3myBMpAD2c6K4k+WBXTzbB/LSiT+eBspY4JDrFCERTHesMlZxmLkXoBxHhs1tNBUepc5vLakFhznlq0LnlxeL4rgh3sn4K3X8SUOtyfPLTTiHiYFX1izFe8OECB2IMK18WRZ6wEf8jE07MjvB84altM8K2LDZJuJzb9sVMHScnXdK5tj0h0e6umXrJaGJBmvoTYNrHRS8X5aG6wxHFk2++zjoVcgl0sn/xqIsLYfqvsbh5vyhZbve/rnSnstsC+sb3Y37uzU9GHpU8FurekIf2vv+ay2i8fI8+VjXnE+QwD5CCdilkDU/9HH3AB7DhbDIwtq4BCfsOfZ43Y2qHzi2YQIjEXtFPC1VmHGRHr8xER65CLZAA8mbMjuchuGtp3BACUF1qd4eAqJ/FgbGOAK91hIqJ5RCKNCK94pbyxdxHrk0LQZ5Rf4snzR04i3q8QgjkGVcb5g567ADjq1S0CD6cj02MH90QJKtQxxq3S77qC5VvUfuqb6qSVzyBIHgjg5XOY4yyA7M2hklmuVlFPgpiCV+xb2G0p91QgoWdUnfUvJ1jeyMVi7sTqisb6pzW4v+DOhMfQ9U1z47f5h41KqZU+i+xLwpeOZYvm0UNtd49SW9uxOloegNujRtGRYoooiPmjed11BdUxZfPR4zr4ZOqMq4zjmz5Enx6kM+9X8hZOPnVQ5CnZpK/WSKI32z3A9cFhmwVRLL2c8w/4XKbLREhUc7DBaViUi8oFpl9P987cnlqqptMKQmCIZ0tOKbkICKr7JfWGIAsxT81oc8qS9qUIeXlBpE5JdUQp1Ie1EoaX5wxX22DELkU75cKmIBHu+67dOrltjMRSKY3ZBmjzGuNgpHMZ1/9X8mFHbppwoTxTgbfBeYCWbgKe7rOhN5mO0h9XscRtoFe1djDYATYo7A09YiS7rQ2wP+FA0m7wEbLvAesiRZYI8ZDzVOgGqdkP7olIYqRcgjkJ3ZEYVOP/YwXprUfvhj40s5uHRQgN1DkVo4GaV9bQFpSHYyuVvJxLpzL4PIa1TC6wa8n9zFrUoUr+93RnWxM9L4G2oS4S54a+tIpxJWsI5jaKT05KwrFYm9u3iOaXy2gZsoxdtiHRE/Esu3FvYkFxZpCgJXBU0L5PL5lgpfZGzQxP6184e3cD5E7zrBWnFpTCdgoFMXl1q6lBqJQVQcpijAleq0R2Amj2pwxLHge4ttMUkLwDlNK5xdQgith4x1GgzvXgndvlRn+E0U5SY5IpqM88QcfH0T3V7T0CEYhYBCSGHolS/i2X7yPjY7JlhGmEgHh7D0ZXq/+GRLa6y+jz0mq+BjNauuMGqTkhaZbR/M85ZNbyJ+s="},{"id":"9eee7fab-6cd9-4ef3-9415-2f8137f1d199","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"数据模型设计","description":"data-models","prompt":"为GEO数据模型创建全面的设计文档。详细解释SQLAlchemy ORM模型定义,包括类结构设计、字段类型选择和关系映射。文档化用户模型(User),包括用户属性、权限字段和关联关系。说明查询模型(Query)的设计,包括查询参数、执行状态和时间戳字段。解释引用记录模型(CitationRecord),包括品牌信息、置信度评分和上下文数据。详细描述查询任务模型(QueryTask),包括调度状态、执行历史和错误信息。包含订阅模型(Subscription)的设计和用途。提供模型间关系图、索引策略和性能优化建议。说明数据验证规则和业务约束。","parent_id":"7c2ac186-ec90-400a-ac1d-b3b7ca2f01d5","order":3,"progress_status":"completed","dependent_files":"backend/app/models/user.py,backend/app/models/query.py,backend/app/models/citation_record.py,backend/app/models/query_task.py,backend/app/models/subscription.py","gmt_create":"2026-04-22T18:57:02.317816+08:00","gmt_modified":"2026-04-22T19:19:19.281909+08:00","raw_data":"WikiEncrypted:klcgW2PbPxJambbKMzvFtzxL0nPpsuPMPxr5hj83lS7k/WfePQWgQNKbEysjyVqFS8lXlJL069Py2sSf8yXxHJzOE+5qAqcKClz711a5h2iXkF0al3mIdEvPGsZtmu7Tz91D+nYN6P20/dGDD6jbfsmIRXLEv3fpccmaTd5XoNj9Y/QT7a0xg+LkG6A8bzQONV8mQsFJZMj512PXdrzG8bSiiAdWa7PEbotJVKJRklAdLNFr4FCsLXLsdS/G2RlAo97zi5J4mpx26Q7SGM1kx14TxsQYatCIUYP+iZ14NDPPPp8tQ+HGPLEonVbYiRiktzJo8Iaqbcsh59qHBoGZBndXEwalDBXzD3sIbYrhxlOU509HDe43hiq+M5eC2SEeTfGdYZ412t/aAJFPEfq3dRl0wa6Oiw4t4rox0s/f+rCg9Sr5yX/vsXrADGwowbhqt/EdTudfsdha7MXN3Kn9e9TckQ+HOqdgqW3+RnY3B08ijVPZchn/Y7XZvx8GujUHSuM3x7EDAXegqkPwkfR+hl4yb4BuuY5AC17g3hvn9yDvBN3m/Ky7mJ4Q9qvXfFaUqDE/FxwPWjugAnVOgAmWns1elVLUS/2nol2BUAuyt1w821y7+N79TkQ07amTWILYvO7I/5v6hStaHk5+ftwpaWSLofuk3Nwbuab8ULq6lj/Tz3uWkEg5/nIIYGVDSN2y9JKgkNta0J9Y4C25DpKnPb2HPfbnD/315IEyxKddS9DpOHC92uT3Ad5v/X0LgVsefqUUep4D2mVUeevDE2o9NuUqYyWYu16AL2UaQOWH86Q97+Accmt37g5580pjdVyzcgVL6Lat1P4W42Nk0vOIncIdM8Bt+iWUIOjmIWDK6nUytRrluJ6RAGeewol6Eb8eMy65WkJ8SsDWFj936G3imq6uoKEsmn9kHznKIfsvt/p4IAv+IbkliuuV7/VNQdN4jgfgKyRp8b4aQd35Qrv1ypuTdf2HQS3w/t3nb8hBriaZZc8a93LekZYaGpvrws/aq3ESXMU+csJKfWhEGhH4VT4cjy6j+jHgihUuUWhO1X/Kpm8iseqreG85CZeU9woTd/nPIayXO14kUEfNVL1ih3p7GtsFGDYpr4EcPU7JpPp61gLhfdpHydKVDLW4eaaCuTR4REP2Vs2BxiwN8GCpJcCbpPeF52n9qB1DE3uIVBITabqC0jiIHeVfb8caysKpddelqqLNn4GGL7BeUFq4RKhlVTmCGuBRWABBDEhDRHXsScje6Av8bOxE/QFVlZmFE8EkDM/WhG9hB5BQ+8u4pKPxsL3/n7xyMyrBnO2n+nTEGscgwlEnpxknYpBTrtZQgU5YB8KRGG+VnvSlbM95vPmdiXS5y7JM9ftKUbdxgnxOVrM2ydbK+5IGvaNB27szvHBbvqtxiY8lXfLRhMYcQEIcD0AM4Xl1qH90xIrHF3lHogdPgwXR/UWGRDWDXKVUEXGRLQHTlRTCJ2oRBGig4FvIztUbBVqouwtJBqoGXoMET7JbuNkxdHs7pfsL8sV5FksVAyNxvV7rRG5QAMaIvUMdDT+XAhtcrNagCHcb53sPxuz9ldlj9adP1H+FxOw7","layer_level":1},{"id":"d9e45b2a-6443-4a9b-8ed3-4c3c04773772","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"UI组件库","description":"ui-component-library","prompt":"创建UI组件库的详细文档。说明基于Radix UI的组件设计原则、可访问性和一致性保证。文档化基础UI组件的功能特性、属性配置和使用示例。解释组件的样式定制、主题支持和响应式行为。说明组件组合模式、状态管理和事件处理机制。包含组件的无障碍支持、键盘导航和屏幕阅读器兼容性。提供组件使用规范、最佳实践和自定义扩展指南。","parent_id":"d907a8eb-795c-4060-9cdd-50d31cf39be3","order":3,"progress_status":"completed","dependent_files":"frontend/components/ui/button.tsx,frontend/components/ui/card.tsx,frontend/components/ui/input.tsx,frontend/components/ui/select.tsx,frontend/components/ui/table.tsx,frontend/components/ui/tabs.tsx,frontend/components/ui/dialog.tsx,frontend/components/ui/dropdown-menu.tsx","gmt_create":"2026-04-22T18:57:04.216865+08:00","gmt_modified":"2026-04-22T19:20:16.229471+08:00","raw_data":"WikiEncrypted:olAPfnTddGwpj/UgpBjR290L/aDt6lN95BdSKeT8WhG4FkaSr5VQgjcM2U6PO66OLKAu2ioz8/DcvdqWqE4E4YFgDfGgf1gF73z0iFS1Lme9xheU8JswyW5u5XItoWtrZRbgOUvDZU7ThOr/Yh8ZmZ0pIEeM3ut9Q9dEITnYazuFmTXvD9cMIh2GfyvPBZ4HBfLuAR0DPWsA3vKqk9CyV0uhz8m4rW/+UJuuwkTDTpvOhyzkya7wMaEMa1VScHtXYyp0R4eexUAygV+abPOlhRuKw79lQi3toHGUdLIj+vtoi6Iypv79X5oUrHGqM4MC+je1PlSpNDXEmSpUPXv61GbBICefd0Wtvh0IvZxysgJE0qVoHq79bYr+VayVdRxX0Br3Rx4eL0CBTWEnYFBTW9/9LAP306Gxgf7mVpUToizDpUov9MlXnxnKI6LFqvcZGw2U19uV7ZKhNlWWBSAXNJzA5CUxAj91HoEWngiW1xAgTh4yDTXyj0AU+g0rnbYPrppxsnOz5onCD6EVGwd6hl/OoKcn8w0qcDETMbd8E0LBD1Yaa1v5np9UprT4/d8ywG+WZVYqxRhDoePKKhZBiM91IHtg2WOTUKl+guC7TY9bH5A+lIU8iD1VUiwX/TspZ6DQbYM0Cnw7XtzjGPXhTWCI0uX5hVnDeUXOOD53TnhVyWxzBTujboWK/U9DTFonOODri8zRjhjmebkH2ENpg7ImL9TfUqMzD3tgOnCChFQQwWVhlRqTlvIyCMezTaRSn9151kWhqgDhfcqz8TpNURfNMLPKgZfXiOnzF1lzL+y8aGFp7yklvo3p306zpCBewYyq9nnE+vTYL5JRj8JaihcAIoR1Ih3tkPL6sg/2kGmSZfff7CEJL7xqQbENgxlngrBscWo/DGdxnSUKKqpheMnEtsbOOwc+o3GbGErrs0d2i2xqkuxe9t4auMgJN2mvXdatnVxj5IPNfw3811hnroeBbwPI4Qu8LmBND/i1tpYDjdBBW2J9UwkB1ljyf9H0kh6skTV5iyRMGVtOYejub1IyGPUba9/320ZMvSH81lyMpezzhX+uNQUTbdFk6sqeVF9JzMsQPB7g3YC8Z9WSVGNqw0kVeHuMlfOAPz2KvYRQVsW4U7YE1Y4sVWObTnd8SmT+HE+WBazGRltpQSP3nD6o4CyLkgVUMOT5W1GAwsMUQFN/G3K7AJsn7XMyo3HJk2ChXFfP+mGSCG9gCT7O9MTCwRZu8zAU3TQ777OWZUqO90CymDy4V3WAWszKCmr6H1H1TWuWGjWR1fz22mDh9wzA81RAWu4VksKm0utF7ncEQJsnnf/ClVJuSKoaz6PlwoQiZPg9MHsT+pSGkJ0DrSgIKWj2WmYV4hDs6phOOfkOSCj++y0IYEBL5S5EiVeu","layer_level":1},{"id":"178b681f-d013-44b7-aef8-dee7ca8c22a9","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"核心功能","description":"core-features","prompt":"为GEO项目创建核心功能概览文档。详细介绍GEO平台的主要功能模块,包括用户认证与权限管理、智能查询任务管理、品牌引用检测引擎、多AI平台数据集成、数据分析与可视化、报告导出等。解释每个功能模块的核心价值和使用场景,如查询任务的定时执行、引用检测的置信度评分、趋势图表的数据可视化等。说明功能模块之间的协作关系和数据流转。提供典型使用流程和操作示例,帮助用户快速理解如何使用各项功能。面向不同角色用户提供功能介绍,如管理员的系统管理、研究人员的查询分析等。","parent_id":"1bb55e12-24c4-42cb-8f59-ddc303460d45","order":3,"progress_status":"completed","dependent_files":"backend/app/api/queries.py,backend/app/api/citations.py,backend/app/workers/citation_engine.py,frontend/app/(dashboard)/dashboard/page.tsx","gmt_create":"2026-04-22T18:57:13.330097+08:00","gmt_modified":"2026-04-22T19:20:37.649775+08:00","raw_data":"WikiEncrypted:luoNp8LvFa7zGThvIT9T4gfD6KbzJyTAydbcRfVv0b4BXBAF9G/unmbHb0x4cm5f5JJeO16NqGe5C5EiggSB7moDpXZy5USSfpQ0OUKuil4F/RTioDPMtSUudIqxEjBd5ipsj1OYjuKwvCZDE5FVOSWKxyx9ABPAcj2ixa32ZPlrgZPKHD2EYG0qdFR1nrcOheMbdmmK/1DGPMM99yCcIRnhsKb5QMmqrZbFGo/SPCSXi/Ykr/zd7+VtVKLrPmeZHpaF0yafvwU0Sk0I8ne8YmfwUGC+cOZoDCI+UsE8IukB5R7bnolQRLXvVZdnHXG3b2DA0X4DxManWdKVUyJxI0KTcxcdI8DrpJB05TyoF/regQH6cLt9xm5r6oC5exXEvwhmfMoZVqZ2jp/7LVs83k3vKksFtqXA19YrxFliE+fS/7xxjhqFqC8gfV8eLWpHyIKgVobBj98SpzX6tRTXknjSX2kDpfKG1ECCqYBxGl+EcY3ofgMb9dgxDxdOaB1JU4iP8j0YMHe0vASK6GweE5UjId3SQsH4Qeng09dL1TY2uNr/6ZVhraG1xTMRFHeZNdCZtAWnUddTk6fiDJmF3zxyL7Wvgdc5vmSbfg0rFxTdxZEiGFHJoEQatTaUtv+NOJGQtnOuYzrhs6G59N5Bu3hnsT2Umn6jFryaWwluQaphllqim1VwA12J3iVLQZFZEVBhalhF5GQBDtl4zt6GFlKucztFwPZXIZTYJavPQP2xpvFDInehpuQcHilp3yHcA8+ucs0ani/GPME+nWnxyEDPhfu/FHkheybrXg084bLJG7E+o9XWBTlbISgl/1CuVNeWNj2ph9KIS+TAcq3NucrKast8dNM3bayIymMoemWvEcvuUjoRlABuJKAeAUQ2q5dFWVAZFvlK8de95GljY869ODymzBXnPjMOSCmHwZ51ImNKeIgHybmDkzT72uKyQcN8nty9DJKz0k6Kq5kib65fLCcoAEe/w3d/WVpmBqxU1Uh3yD+z1nEm++DCQTmK1jmwwLT9krps27ULzjOvLuJz2cJnCpSFeB2fBYxH5Wjbp3UZFwDWJWan/9xl9bIREOaZ7mmAz3b7YgA+106K4LV/Y4vBjtitwaM52IcTqvh0m2OKpQSYopXYE7hsG/CXE/MACxmx37ut0KpTFaM7N2GYOE8Z9hIsM53ueQhPuRvk2Hb3E8xbBs3mMtYON2ubkxuPdcQJmCKSHZTgVqD7NVWXIyDL87TjV3YTbdGq6KlIptH7MxMpAvL03xq0ApGXUlF7gt9qwpHG/tuVSmjPBAUAEXRZ3bC+1E0tvaaIPWgP63+hhl5S7970UrDCXrtzeN3l8mbplzeN8BTm9brd+gziF3NKUZgA2yM/OiHjy2jbsu2NmXU8Q3f57Q1MnJglrwSgciwgohkp5Q5XIBOCwyJuIcAxO6LJWzaXB+WOrB+xk7v3h8qxpMdfHMOLFTY8M5Rf6MoGtJPOtFpc1+0Cjg==","layer_level":1},{"id":"f210509a-2381-46fe-8c22-0ed768e6ca70","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"数据库迁移","description":"database-migration","prompt":"创建GEO项目数据库迁移文档。详细说明Alembic迁移框架的配置和使用,包括迁移脚本的生成、版本管理和数据库升级策略。文档化初始迁移脚本的实现,包括表创建、索引建立和约束添加。解释迁移命令的使用方法,如upgrade、downgrade和autogenerate功能。说明迁移过程中的数据保护、回滚机制和版本控制。包含生产环境迁移的最佳实践和风险控制措施。","parent_id":"243a1a45-d31f-405e-a3b9-694d2fe617d5","order":3,"progress_status":"completed","dependent_files":"backend/alembic/,backend/alembic.ini,backend/alembic/versions/488d0bd5ab01_initial_migration.py","gmt_create":"2026-04-22T18:57:14.161329+08:00","gmt_modified":"2026-04-22T19:20:15.066432+08:00","raw_data":"WikiEncrypted:veTYwq2y4io5qXerCTrkkH9ryxDJ0NroguMukGrnBDccbI7FhPTNCbxlVVf68ZQ7vvUzAxXQ43tY1R33KAEW91uvBjnohUVl7Q+D/Ap3SUnHDpWQWBM8qdBbgPmANzeAhinu9R6LqaM1BaUXXk7OeSNamXR7nrg52Su/KTt/eSPCFzeoio7iY3vORVHeaHpWs3y+ipYf26jobCXk1cLVjF9BgaKQ8UIEk4JobP8DZM0dSC9SqfPLDnh2e6k9t39q8JNd3MaV90E75j7KuZKZZJemgTfwdUTe5c8Bn5uNxCf3TK5iuMxVxJXD1PTU+9tadOMcx1qXGJ3cb3nuH7g/03z2UrmsQfIRf/cZLGBqAdGIWhQpJph2OlUELy5WTq8t4R//c0t+IUVTKieR2A1E1U7uGurYXL5ymVtiLnckbHeLMEoxbJYmh7d4xn4mWF5hcIhAZoHvjqh+shIGNwCfXJQGswpnJNML0vWzQNhsJ1YcTUnHC0P8Y6nj9VmXMpUYuJLWuIRuqBS5lkS7ChcQMXO4wfad4N4RMHX9jLYytT/wOoty1KlEalUzriEMahdDn7h2DsLzOJL35SLwpVZ4NVnInvcE89IzYO4wyJIz4NdOfBYseoIv20Qv7qO8B7xhRtTudUyynfaz1jmkT8F9UgZFcdBJqg08MH6VdQSdrlKqUGSGsFAxtB9OC7yv25xznJq5n5SUwpLmFNbQ1evjOqqsogzLg8vClnH52aVuE6TfQWggj1wDjxG7jUj/W7mHSOsDI6AhtMWrP+2OVg2Wx9Bab3iLivdVe+mg93LH2FvF3a8iAdcDRseeNZj2UeJJfYnmzpsRpm+CeJo8Te1ab8UeOeGebDsnVTTILuEg+qe85nTuG8+PmgOs+n4r+781gUSlyoOO2Cde7QwES0X+oENbplkkSZM719jK09dcnbG27vqoeh8rTrzO9ygM48IjQzZWUJCnAIP4Y7VVrcTBiyrZhRLikSrVvSCvWU3K7KW18pnOJh1narIx0e6w+bctledpWsEGZJ9fwk/ae5P5ZvACxfM/2LOdDFqshVbr6grRIeSNNf+tl5nDunlOTUEY8fGNt4lsr35UgKwPwO9TxACZGCsd25DuEtHum9DU/GYli2+rgNaOokcc0Xtw/R8OuN2pa7ohiD6ACjJ82m0kKC0mBBymsNAdo3bDXv3yoyI=","layer_level":1},{"id":"aad61788-1dc9-4682-b743-47188d7aecb6","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"引用检测算法","description":"citation-detection-algorithm","prompt":"创建引用检测算法的详细文档。详细解释引用检测引擎的核心算法实现,包括BrandMatcher品牌匹配策略和CompetitorDetector竞争品牌识别机制。文档化置信度评分系统的计算方法,包括关键词匹配权重、上下文相关性评估和结果排序规则。说明引用上下文提取技术,包括文本片段截取、语义分析和相关性判断。解释算法优化策略,包括性能提升技术和准确性改进方法。提供算法调优指南和实际应用场景示例。","parent_id":"7e66719b-4a8a-484d-889d-10fdc76788bb","order":3,"progress_status":"completed","dependent_files":"backend/app/workers/citation_engine.py","gmt_create":"2026-04-22T18:57:14.593043+08:00","gmt_modified":"2026-04-22T19:21:33.125768+08:00","raw_data":"WikiEncrypted:qByx+WzXalKNnda2aYUs+KheFrHbXoPYZO+RPCaekyWxjKaQvamRZeOm1WgroVS/6iDS9zNWvth+zIYrS27hz/NZLNtt2wOUw9iZG139S+OGMVcZB6KjsnlgG3fSA05ZEdrwDfB531npeEpy2DodlOkcXOSBrgCwkOsVexBDKcWLpthBpfOCieqWsVnTfFkeXE8VvMLROtjojlr+6So18lnYzdEekhgU4Az61W3QEoXC7l+uPBNpRUYZY9z5nLzihT1o3I2bDEfAACTaa8pU8izFltiEd4qID4z3TepI7IuVr7oTmPOaD4MzHULu/4RaMo7/7c9Ql0uGsFrxRb3QFXiiH6QZN++2X9EDTl4UjA5k2E+OTcO2UOZm2dV7vi51lNAZwI50VwaIjtaC4LrPpX7fklk3hQ1qbw4w0FPjYlraJyO5MLseOOQXmURr5BCOhJcWoJmwJadtfh8EAO/9XRK9CRjr2wpAuL9kan2Vkympa3UCszQwVox0j4IkjJNYlG/7UEt2ixgTU4pQi+CAJy+8tZGJxY1gk1MCILniaClaQQImJhi6TfYfWSVywTFVltVcxPd7kMwK116KQmtoiqet/DRC4rzqeEbb6w8I7HZu+VwSR1TXSN8teXJTRMAFEgg9vh+vAOconlX3LzcgvFPohvC+teXJqTADp9XvqcC6Xh41G53xVAvDteT9pJCZHMZsfgh0bErBFzrvnwSObsmJEkvWnlDRN5tdVsRIk9LGcvaflUwWHmIof5SJj7wO9fSm2BN0gW+VA9nbQxyuHt8YqvNR5D0WUpbY1/UDw6IDhAxuTh/6yyVt0HqirNDXslgDardkMaphsQs4xh1fShhK/PcG4y+xX5VsoXIm/tjfGAEXYIeGOHtVuldTaCxG32uSnuT/aFQh7H6df2Hhb7c576oZ6QbZ4HyiG+Qa9iZr6deKWdGhtuuUzj3FBAXLtLont+yuXSCWi/AWM5MOMVyScyci2LKIjH5haWCCgS4nm9tLvifzUgmdeycw/Dhve3nrgdkgO+bIRuUQxXRIxZRUdxEr0DKIa2tEKVeorc5pIX9m1bErC53nDhnrLBj4/ZQjHp33XcgGQ+4jAfmWK/AN+P/SNbyF/K6O0qlKXPqQk/w5bOZWCtcIjrDLhyxZce/pYHOGotrSGaKOK56cJA==","layer_level":1},{"id":"ac0658da-7670-4e41-9e57-02d9d0d50680","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"报告导出接口","description":"reports-api","prompt":"创建报告导出系统的详细API文档。记录CSV格式数据导出的完整流程和参数配置。说明报告生成的触发机制、数据筛选条件和输出格式选项。文档化批量导出、定时导出和手动导出的不同模式。包含导出进度监控、错误处理和文件下载功能。提供报告模板定制、数据格式转换和性能优化的使用指南。","parent_id":"dd12cc44-0c95-45d5-a726-e7b8bcf2531b","order":3,"progress_status":"completed","dependent_files":"backend/app/api/reports.py","gmt_create":"2026-04-22T18:57:37.837258+08:00","gmt_modified":"2026-04-22T19:21:24.246329+08:00","raw_data":"WikiEncrypted:ncgUburcUGIQoFYRn+jCfmA+PbmqRFCE8+U3jYVfh2v8RywptgptFxq5lhwZlmleN1AF4e6ox3Zeepx7xqM72ox5n+mZFIvpPAV03iG0mffmuvwBURuWuzG3zvukYRbxLcpPqC6v5q4GXBz/rMTsxy+K9HHfwtlYbga6fioIcJrpFn0O0GBCxvse/EcV3UFGM+LHRaRVZqk3N16dHYVOsIgwcqfz+E1qQhOc9tSvlN8OuLBl1z0ukshjGXiX/lY4xzl1WSrFXAIsz6WAKGLPlkePgVZg61CHsK9/iXfq1HIs4Mv/RYwTtuMKkyWX9+6LO+AwRjoeHSIVgJsaSwMBS+MqugD6JwGdLsb5/QGcsDSVaN1zVhbjD1WnJ01uK+tBWcULWdYGAkgo7z9A/YQJsV8PUfXfdZnfuydZi59mopfaX1AYtCXeceMgKnzkAIwjWUl72+Pwtvdke5upjMFgEvoy5NrCoYwpZJBePLoPrgu0kURoy/vWGZ77WhOuGQs2/cn/pk4vCFes3r34gglCIF0tHwwh9piyAabHid5VZ3EIuOlhBHL4O9L6zlAgO0opbo23bXt7kIR8MIjEqb8b3SoUIGY7PElOLeOIwJiz1LzOnccT84S/avMIJR7fgzi/me7mJyD+8h7mZ3GY/AsbFhi2oh6bcqc7U1UcO40wTqx7myMlwrMOo9zRr+aAfR/lqH6YwFw89kkxflBfKQ8X7UwPI/vFeOnYkPFjj7XzgHUtclkOUbxztMSSnhxiBMGzbNf6/hnfqT9O5xO2ZF4ufJ0t2Ci/vTK7mqMRvj6sBAzXnGCig4zpnDKbOrVJnB53v6AnqjeFLvqUCGI5Aj9nn+g4LDMtPzzDhfm/HtoxVJjclFmC6FQSLvHlh2FILylIvVxzY7rDDuK3bTiuZFz9Hw==","layer_level":1},{"id":"7cb1d921-44ed-4e22-8bf9-baba7ff8b7c7","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"新功能开发","description":"feature-development","prompt":"创建GEO项目新功能开发指导文档。详细说明功能模块设计原则,包括模块划分、接口定义和依赖管理。文档化API开发流程,包括路由设计、请求响应处理和错误处理。说明前端页面开发规范,包括页面组织、组件设计和状态管理。提供数据库模型设计指导,包括表结构设计、关系映射和索引策略。包含测试驱动开发(TDD)的最佳实践和单元测试编写指南。","parent_id":"2148d7d6-25c4-497f-8894-c4eb7a0d2bd7","order":3,"progress_status":"completed","dependent_files":"backend/app/api/__init__.py,backend/app/models/__init__.py,frontend/app/(dashboard)/layout.tsx,tests/conftest.py","gmt_create":"2026-04-22T18:57:39.375268+08:00","gmt_modified":"2026-04-22T19:21:46.916012+08:00","raw_data":"WikiEncrypted:5RlNBsIi2/3o8ATlHuzaYHdvL7yJfTe6YPwFoSkO/GgZuVYfBQkVBn+1kZzpIDRMGSVFSuoDX3pyax2f9XzBxnZshkSCM7o1L+HEG+sgCns6EUSGCL3KfZi4emTF8vVzG/3m0dhVOWuoQXf3TK1+2Aa07EE3HJ86aWB+sLQgQyR8gNqS+MsRztbE2Am9O9oDis36vAh+tGO64VIsi4ulSBpI8D3+dRXcAI8RsujwrZpp57UaW+DyGg8Jkp+WqqjfbgBGcdkno2KfRFyWKqKLB1g6yKJCePYymOK9DHhXbdBC9mKr7EWc8T394B1klPsqywIZs0i3xy/Pr5FAuzklEn/8jhNkYpmgcVvXaZ43AUVTklSOgSC0cwvfeHd91QuNSlfIbIs9Xh0pfhmz3XAts9WErkdoph3YSvrNDMuXK9CMNhlu9XMkMCQcV7ac8ax69JKW4uVaVHcjbU7PbzOeQ7vSALRtfK0zhIWlbQ3za72G9FvDogDqrLfOBzEXA/cyXMdpeZP/ICE7OsSCRM/i/yU/K2YUAWnRJczSn3cX/8lGIDywmB0jU+IL4l7eteQD1Ea3elS3ne3N6t0WqX/T4jybDWQQZqz0+gOrFuJUKTQ18p+2afWRjcYZnqg0EMvXxpXaT5fmpQAxMA89c70flWUr6l+ZNckhODBS1Uj27vp+ECTFfhbNt2CmqrDWIfdobRz1gXYkZI6LR+xgc598+CJDjc6zizQcychGbsbPqawU+CCQfSVqPCknZLlSyJrMF+FMoe4jm8fpOVyOOfqGN3N04W6++aIiU3eZIMB0F/1pjONP/d5egIYaJhs0Zo+ciWBBR+Me05kVd2tyIK0ldRH8iELZu4XmfrNdcGPMTZsENU4lZZiJxIXbeoKUQF7KbjsDcLTmuXfl4jcPFk48k+LwUEE2fyaAoG6SFrEaDZiZ/yR292cbQegdgcOrF+/QZDnMqVS6yFRaljF5C5+ce7VNxUOs5dzK46Z/0xD4kXHkT4QT5/727yUeQ8KmshnsLpu0XgxdbjIAKa0UpiByR2sen9DI8GblyzqpH/4jM/yMtc2COW+MLDnPw7xho10kjKNljBjsabSa5q/wLnYGofR/pA1CmnrwERR7Cytik91YmtyTb1D+rjh+uOjSVOB5NqjX7dqhFcV0CLE3KfUAkA==","layer_level":1},{"id":"c45e66b9-1ca0-41da-a796-6b98f394faa1","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"运维最佳实践","description":"maintenance-operations","prompt":"创建GEO项目运维最佳实践指南。详细说明日常运维任务,包括定期备份策略、数据库维护和系统更新流程。文档化安全运维实践,包括漏洞扫描、安全补丁管理和访问审计。提供故障恢复流程,包括灾难恢复计划、数据恢复和系统回滚策略。说明容量规划和扩容策略,包括水平扩展和垂直扩展的决策因素。包含运维自动化脚本和CI/CD流水线配置。提供运维团队的职责分工和应急响应流程。包含成本优化建议和资源利用率监控方法。","parent_id":"803bfef4-3985-477e-a61c-915a246d0061","order":3,"progress_status":"completed","dependent_files":"docker-compose.yml,backend/requirements.txt,frontend/package.json","gmt_create":"2026-04-22T18:57:40.355828+08:00","gmt_modified":"2026-04-22T19:22:39.446821+08:00","raw_data":"WikiEncrypted:AymRokkaRBrPd1uk5umiX/dhQHTPPm6nf5mmDPb1IMODwfEt6djM0VBnDWaMwjVsVSuT1AHD1pfVap7WBute6Wsiarqj6ltamY09KvCs0YHxEqOor+0AYSzm2Aa31Dwx0HGTr+fgwTnE6RMJGGceRIs4cV8RBM7Bs4/C9pv4JQQe1wx9fBauJxXtVZgZmkTwsQz1zMKUv/my9/bZ9Q05aHGQVWSs0vQA7fX+eO9Ugt8ztv2KzLkMHjTPQwDUKBMhAsMb47fa/gUJRb4vX0XaRC/0NRwbOvV9kGE8ZhfJSMzck20DTmUNjRs+txGlORGu4UWZsefqLmppB1YdHehQUnSp53vMgJwj0a9BHe2ZU8oB3BBo+Ky3c3OR4uaa4rfHtatz+jzKcC5vq+Oa2OkwFMyKF/rHCf5c9kOD3c9bg/wQwxJpIov1DkWxrbmp2EHHUtHsb07ASQhGLV3SX7POirp4b3urFGRk2MwXKxT5U/gA8wytKMJhKBFtElvFBq2i7nSPeaH2xj0DS3o9rwbrhRJQvLfp/3z63umdp/84RCiI8SwdYgPx4ACqLY3QKXNo0LIi6rsgWkaf5Suyks+dimQcPtAllHZGQA/gcWYidV5oQCnhHJ/meCiKRy7Xb4GttMbIeOA7UNNRQ9IFS7wyprb/n36lYgLOcuQ5Nugly0pl7iFTkl4qWn4pZ+zE4TR9CK3+3oihqJMUeVFdsWHHzgVsh4Vy/4MCPUipHMfRj4T7BabCmkvaTRaf6oRiYEXPGH1mUXCLnQuty4lKx7e8LxdqWP6gciPJOcuRMBYoeBHqUKw/JNmAzN8FuypXsdUryS7GDozcilxOcRE9j3D0JEW+8ONdKF5sp2JAXYX0mhqtfB+RscbqU7E352fF8vZAqHdS0XHCguOUcPuOk1PjzrKK0PJcxoWJPrW1H76CpfhWX9kC1CX6ji8+oMevPaHlwwFIAPnELyRyWOvaAia+6TeiAn8YTup3BXf//p7UweBdBGz//qMDvWoMqMf67cKrFP45gozQjSEYzm8AQ8g0Rqx6ctmJotfnBMpbUoG/k/Br9U3yCwUW5C4ELNIkZ8HMTq1vu6GKzheTjgM5lsZSBhR9qgPwf7cYyU47KLN2oWaoSG9Ke9U5fc+blB461oN7Tb9p1M9jYYAi3IfY13WbWR9kQ09yQOl6zNP4E/TOZ/WG1O8YhDVo6O9b5nQ7Txck2F7a8+Oqzy404YpIRpdXWJvx2sNjxCQXQVXqKKTnxP5lOsLb4vYZqXeHpIw8VawHDzn9M5zowBo/3M96UZSy84F3QEP3JU1nF98Rqg82vqB50gv8dY6wl5RPTzKd+Fi2","layer_level":1},{"id":"243a1a45-d31f-405e-a3b9-694d2fe617d5","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"数据库设计","description":"database-design","prompt":"创建GEO项目的数据库设计文档。详细描述基于PostgreSQL的关系型数据库架构,包括表结构设计、实体关系映射和索引策略。文档化SQLAlchemy ORM模型的实现,包括模型定义、关系配置和查询优化。说明数据库迁移管理、版本控制和部署策略。解释数据完整性约束、事务处理和并发控制机制。包含性能优化方案、查询分析和缓存策略。提供数据备份、恢复和维护的最佳实践。","order":4,"progress_status":"completed","dependent_files":"backend/app/models/,backend/alembic/,backend/alembic.ini,backend/app/database.py","gmt_create":"2026-04-22T18:56:47.082886+08:00","gmt_modified":"2026-04-22T19:00:15.768242+08:00","raw_data":"WikiEncrypted:veTYwq2y4io5qXerCTrkkBwdhOAmcMQTz+AJLOFORhNFeFTAN7xitXLxeRr3T1sKrh0z/6NFLuxfhi+XJSCnWjMeU87mDaFkK5qsmaAL2Nytxcgn44oX/Np6gDk6NBpKTXXQ3c1JIVHY3Iku4YTIdYCMibjywixwZ23Y301pMlyPcNfQmeyLtgTS9lP5U9xDLKsnMUqMie2ljsUEPDjOoPxSnQNoCL2G44jqE4BeEYCl1h3JsynQMnvjVq8UCOhctZuFN52nO6JzrF4QcwRRO4A9xL1TaAtIZ4rg/SoqWB45m3doVWQm5F9X2438sdweLxVi9rjFhD3YsPTUW3+FCCzo3C36ea5HWhnLG1HaEDoZet0lVePafwfQDmBH4SRDIzbxrRNYtlqnAT0fkhc1nMAQz0IEGTS56zXt+EYxvMzu6xYqWFcYFRtnmeW4SPZ7fIro5U+rBcr8RAhA+oRG2JsVDkhv6DqzoyIv2NQFFMusIiiBmBASHXvFFZ8AUVhKC9xd9A3bJmY+ukIHDAeRXE/IucwTAoEQNI9CMXeyfmQK03mWECRg4ZW/mruOTW2OBUuDhiPMO6x1SJ1/xRenkwbw7G+81ZCnsnBRLKd3cZF4y0nCxe4j13LGQfd4iAX5i/DUX9KRkwDMblN/sboCGkcbSLtph/Xvk148A4+6695W71mrzar7yhfM2lOT53Hsyt2sJWiczLnDCfT2QTlB71ufUZ8Q+ypkZO7zlMrvkaUHamxgarKXmrkr2OSa/8IvsG/WUhqSV62U/SXuacIVOKNM7SY8D/zzYphGfcHavI9R9O/oEfUb0yYdGdjhTVWuiGlGsp/cByp6ofrZn/LfnNB3vSSya3wpDOVy3RqCdO+/d2gEs+jlREm/N1+eDZOKMpt8hFWS8xUytmwHZca0aN08mhhTQARvxhQUGnF9uPVIUhyDUF+KcuKANnwSAWIZo/cRSzlSrqPcRAA7XNJQTg2OLmkWpWQwVtTbYCyP5hSM3zmFlpWvBfR30gpl0P51+0pFCd4VDI9/9HW/Wa7Ih95wP7HNKXXfex6GwbePYI59yynPdHHuqHaqbkBTKMJS82vrAmldAtjgxXDvf9xwDjPEyA5M0Hw35tVS6wjNh3SS2SuwfT9oDISNn6Y+5j5yrJjUF6K2Zri0unxCjM1JkoAvusyG+pHY+Bb0QKmNcfyXBMYTSI1PhOPDulcDYMyq9Zfb+Al6euE24vTZpbjKoEtLO6rGDYV8KHfz2jIxcPi0y8Ou+WvMTWKz18iFQNUT"},{"id":"05214c1a-d804-4f3c-9048-20ba4de3be0f","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"引用检测引擎","description":"citation-engine","prompt":"为GEO引用检测引擎创建详细的技术文档。深入解释引用检测算法的实现原理,包括文本预处理、品牌识别和上下文分析。详细说明BrandMatcher类的设计和实现,包括品牌匹配策略、正则表达式规则和模糊匹配算法。文档化CompetitorDetector的竞争品牌识别机制,包括竞争关系定义、相似度计算和过滤规则。解释置信度评分系统,包括评分算法、阈值设置和结果排序。说明异步处理机制和并发控制策略。包含错误处理、日志记录和性能监控。提供算法优化建议和自定义扩展指南。详细描述与AI平台的集成接口和数据流转过程。","parent_id":"7c2ac186-ec90-400a-ac1d-b3b7ca2f01d5","order":4,"progress_status":"completed","dependent_files":"backend/app/workers/citation_engine.py","gmt_create":"2026-04-22T18:57:02.318113+08:00","gmt_modified":"2026-04-22T19:23:12.0556+08:00","raw_data":"WikiEncrypted:qByx+WzXalKNnda2aYUs+GYu9pC8LLDNo/T55ZeqwzWiy0YK6xVbR7KM5OSxCOo+gfTx84RuGsvrF5lK6ZjnzUlze4qrtyk7B01ZVL/J3jiDE8Gxh30IZ/Mj1EQJzBgbS8ucLS5EtWSrZllyPWZCYSwVaerdIDlttJxjgqNQQtMUDRAB8OQ24T4T6PetqUxBLI88gXxpodDV3Dc8w4vOKW11rQu+VT/oxsfIcoPAneCboh21Q6/+B9XbFXBpIBt7aIzWt2RsJttqoYF8wDTsmjCiZLq28t5vrQ4Sa+eKtDX1DpNAFob+BOs4Mmqm+YOjaRHvq2h+NjRwAcgLTQbRRQ/mDN6TdA4dbJh61tmjO36v1VZj2vbzvIogCKkVwvxYe4ZRLkOADLelhTgAjFcFCSPmrGGDFgzUU1yzAstWjYB79RIQhz0PEg9hhOmte8rH3lRWxxSpVe/1MQMLH+ka77X8d5lwXEAhaF5uHt039diwwi6YY3Y9nEB8XYpv04beBhaMWdLq1Odojzo+Y974U5UY9gXk01q5QVIuHK8idcl6cuKp4f+gwGZ93SgjTqe7FH7Qd5ge6PFGZJ+aslEcmtnU47e+zHyzdHWI9hPG0YyJELRpf0i86RxY/f6hXIyFAyeWy57ojMY38VpwsiGGXz7IigMFtIyIlbqO3TH0+IeZ2t36ENk9y6nTzgXcWWxCA2LCTHmO5+jPxWISebAAxo6WWit6n6LUWDxLf4MHwWbZckVKu52y4K3ne9qu+I27rwPoTazIsCAYy0QPtt7mW2mn0ZRxzPvioUK7EGgjO1OqsaKLZJ7AjW48Ru6wYZhBlriVoABBGkI9BaUnyYEcZ/6kpkl+TnRlKY32X1A8JvIZA/8zHmSDEG+FinfzfNFH+SSj3G3HzsxoGnVOm1qN7i3rDXC9bliZ2XpBZR+f29A0ray4pNhiBI+dBSKZouc+bB57XZ8Ct+6j5QHeCfiWa6AQMVuv1Tctr7qtXTrcVzTlYsU3vR1mVtmmkHd3yv3ZBA4l9p0DF6KOSKVjAPn2GSA2EABrlnwJ2lSeWHoObLJBidT6nmQ5S/6IkRgLb17Pu1ypV3eKj6LNDOzrw4QxJ/JtHZ1rG/1Tl0wmhAjxmAzcu+aahYjIdYSTWRkWqqVZ66nNp4pYZys0zblAnLMLM//kJadF9xEEaHjMBIl92IgL0L97/jGIU2YXsapNE1OQ2D6+aqKVb50gv5zx4AKT4tdiLO6Z2sMBrNeyDhml+vCgXp+rFjFXUx+qAUz8Wm1u8gAPUIO3m0CnCZSt+R07BQsJkHMGDzlrGsy1wvLg7Pe6UI9RPBmOUN7UeUe5c09BjutdT+KuUht4apd+u4wqgCGkHyo1Wy8W4UbciPoYT8wx0cLPnCGR73GfwoGnzPtfxWJDXX3SruBGUXeCtwl8UnbIlyDa4Ks2befkeGMWpHQ=","layer_level":1},{"id":"bfca5ffe-8905-4ac2-a0a3-8e4dc43533b1","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"数据可视化","description":"data-visualization","prompt":"创建数据可视化的详细实现文档。说明基于Recharts的图表组件设计和数据绑定机制。文档化趋势图表和平台对比图表的实现原理、数据格式和配置选项。解释图表的交互功能、动画效果和响应式适配。说明数据预处理、格式转换和实时更新机制。包含图表的主题定制、样式配置和可访问性支持。提供图表组件的使用示例、性能优化和调试方法。","parent_id":"d907a8eb-795c-4060-9cdd-50d31cf39be3","order":4,"progress_status":"completed","dependent_files":"frontend/components/charts/trend-chart.tsx,frontend/components/charts/platform-chart.tsx","gmt_create":"2026-04-22T18:57:04.217105+08:00","gmt_modified":"2026-04-22T19:23:03.405364+08:00","raw_data":"WikiEncrypted:LlssVZ/E1BSUywfa50FLlWyGoIcJJpOig8KP9B9gWWZZI8ePi8CsG3BxaOR4OzxCxm2XiNUDCA5f2LMwvnigr2SlxVBiSNHUI0lCgSHHEunmkPytqBjFiqGB9yeyKz3O1rSdq/m6COB07RDEp6Tymfd/Ich7hNqr0ALiJKFBMfTRXbiXa3naRlzgzc5/DA1h/zbAk9yl3yaR4bB4p7xAmyNn8C67e2gxrbHOKN7NK15dfXoIag0XHJMVwFE6SysJ+lA7fykhvt72SHx+Qz58/QamURb9OcCEnEtIYqW1uga1vc2HXP+r8Ud+tutKATUxPch908D2Dm70HjZTkDqV0y1RwdTE86O++EPgx5Sx24CmB2Y9KEs2TsxQBSxas2iX7ajhgIL9xFqjJIMVJWfRNeL/ql2x/OsrzEXUWybjh//bQHE/MIs2lMcn3jl6uggDT3q//Zf/jugKwK1117XAp1NDps6xhJqmTf0/J+uk2DjJwURNZ+f/hL4XNyCoPSOpjPZMIk+ikrqwiBBFy1t5dz/fksmYdzw5H1wSGwFPks461EvwT7SUd3qHg+HDEWwCrMRQLhaalcb+5T4rD4Gg7Aa0Y5Q7v043Oz/0rnmFckpB7QJrcI4yUtnbNxu/HuY+NFZLqpJUrNC6MhZKZ1Nf8HjXwbb7FvRLPTJk/0J2UGgZIuFkHqA398GnAHSj3OegnBaKM+qHr8cVs0wfQ/uFwwYzgoBd9gSGURpkSSDhsDUj2XJMeQPghyYg+au4UrCpUVHdYOyliR8OyttchGTRgDox14AD6X6BPxBueyw2Yb6orKjSBwNfDa9SZQK8dW9S0BehKqK7QLDHIzpglx66chB/QwWwaryM7BrEciDg6cdFeFQSmV+EgDmEePwse6hlzNdSXObTbNXLDwYhtifAxjK0xxhVUCtc7HVAFKLROk36HCFnlzxLpVmtppYzoprXfyXDe6BHU55d+PSdjtYyR7k/tigqJv5jX3TTrH6McF+/v+eR2ZtsTDIjWz+prq1cMVTVPgIB9WoT0+dXP1fMoZ52xLzWjSkgrQ7djAo7jyy1G9plvOx0bdGh5lBGOz+/zY7MTLVaKx8eK6kKS+juFkRK2kCuesUi79TGLidetA+vkruZiHzNPBJWntDodb79","layer_level":1},{"id":"b32b024e-2d06-45c8-94c2-a07fd25ab9b3","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"快速开始","description":"quick-start","prompt":"为GEO项目创建快速开始指南。提供完整的环境搭建步骤,包括Python 3.9+、Node.js、Docker等前置条件的安装和配置。详细说明项目克隆、依赖安装、数据库初始化、环境变量配置等关键步骤。提供docker-compose一键启动的完整流程,包括容器编排、服务启动顺序、端口映射等。说明首次运行后的基本操作,如用户注册、创建查询任务、查看结果等。提供常见问题的解决方案和故障排除提示。确保新用户能够在最短时间内成功运行项目并体验核心功能。包含具体的命令行示例和预期输出结果。","parent_id":"1bb55e12-24c4-42cb-8f59-ddc303460d45","order":4,"progress_status":"completed","dependent_files":"docker-compose.yml,backend/requirements.txt,frontend/package.json,.env.example","gmt_create":"2026-04-22T18:57:13.330281+08:00","gmt_modified":"2026-04-22T19:23:49.7972+08:00","raw_data":"WikiEncrypted:1aHEw3M2aeluf1++NE1n1HTCzLNrSYNKzc2IVp06RQtbuAYcMEXChpU7tJYS1f3O0opR6Xc/vB+l5keQe8PgCR2ncFv/1Td0hCZ2YAOzrKcU9vT19aDXTs2wVsNg8N0rDLvVcTazfsrfReCkGbF3t4KIDlALR7E9k/sBc8Aux87FRnIqhElZnBvZS4Wsv6VYjg+rG70cB28ZbZapx09FwTbE2gyXj7Ck6qNOm9h31VGeQcyIEPfTlQzKWWghd2UuAwraAXbHFeTBwAzBOPZrVi2iMW5N0fiPvrofJe2RpvtWwEWsHguXuXUfXywdpjyOFbnrTokbwQiiwiHpgbktPSypaW3QvBywnGVAjxPBIfRlzbrxj8jlb7w8YznQPbWcNRuYRt3TVMBHR1Ix8UjdR2JwWu4yZQUrLOP4XJ1OVoFdLu5sT3ug3YetVAWSBGUiyUEIQ1U7QoHxxE03qI1ZmrGvC069cAhyU3YQYL0g+PfXWWo86/F20gUXppepBbqg0yRPlCX5asdDuL0vX+i9vl/JI2++deWQKu7cSc8+5tRNcN2AF8Klslhvy5KUEZpxqPaEhxXek/RE8thYcMjT02y2lWMR/WID/CEUm0DZjOC2fQE7kYjogUTC8OhNTBXocpLyn1sbyngxfQ3KsE6+Ysl41LroJ5F7SN8+p5pueRZgBbM32odMU1Dil6T14I/HwltMjiGAjHurPSB8hcX7nNvkuZnVTE2YMjB8ZEhhf76D9Pp1naI3P2d3j0DprvLXRiuPSfO9NBTu6LwZKuMaI2MBEDI2zoV182wdrC1T6AxOiHNcJXYiaF+ZOpMA0vabvcycoioAE6VOZ0mBQ9ZPVSZDf/Dw+VTsC8U1qruh1IAQVYCHsnjY9etX9sTaj6Izd3VrvgQkqP5vAteSDZTy1FOTlTi3X6RzcurXdR3833HqpXa/a0ot8JiRdmjIG0ydOuPYeZPs+Jb0FMN85H71mBziu41udCrikCTPBRTVz6VCG9yBdOgeW4s/iNhPqWCnDibKpfJy1GfSyZ7Jja1d4K91Y+IHVrJxB0+TZPezxY/htgMg4W7MtRoOOPc0eRqst/S2zmEgxMMpU4dfaQIne5zGbUEPNb24qp3HsO9v2un6P17rFQV86RwmPCnyngxtddo0vUNesY0s594GhsGEquA9XlaYa4/KNM14pVDEm4ReIC1JvvqdiQdUAPv3nMXASqe3FxbJmsi93mpWhKWx2q4W+P3ZivXtxhQ5OB4Rs2uXmArlB3MNPKk8TL7wHCUAsFUcCGoPtT2+ov9ntDTLyjKsb0YUGjYUqAceHGdvLCM=","layer_level":1},{"id":"15d6170d-716c-4d2a-833d-81211a59027c","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"平台适配器扩展指南","description":"platform-adapter-extension","prompt":"创建平台适配器扩展指南的详细文档。提供新AI平台接入的完整流程和步骤说明,包括继承BasePlatformAdapter基类、实现必需方法和配置平台参数。详细说明不同类型平台的适配策略,包括基于浏览器的平台和基于API的平台的不同实现方式。提供代码模板和最佳实践示例,包括错误处理、资源管理和性能优化。解释平台适配器的测试方法和验证标准。包含常见问题排查和解决方案指南。","parent_id":"7e66719b-4a8a-484d-889d-10fdc76788bb","order":4,"progress_status":"completed","dependent_files":"backend/app/workers/platforms/base.py,backend/app/workers/platforms/kimi.py,backend/app/workers/platforms/wenxin.py","gmt_create":"2026-04-22T18:57:14.593273+08:00","gmt_modified":"2026-04-22T19:25:18.719953+08:00","raw_data":"WikiEncrypted:Djug3Ni1DAZ8Du2qShzHYuqLmnJOyyKhsCPSfayzAMoLf9J+rFBy0LePWIX3b5/HwiLgJX57X2JBlObsW70y9DLEgX5HDr3AAeBC0dRNACFssPk0GwpdFdJeFwRtoB01ZTdr+IpYAK2YG8gaEA/2ysFJOZzxpGWgTq/Xz+KO29a2kcDgYhZKt55uqT/Mo/5Dzr6AN+qXdLL5xSqrvCkpmhYMFmzgIBOUjJM1mUISEtf/X+gK0L0wXSj/tCJnDQtGJqbFK24HEYyVnkon1GdnIvb+Vptmby7Qg9iNT/xb8JEVTTLM92335BfpTwTp+D4fNkMNyZgXv/XW6K5J/dLRulOCKwxJxPROAyvyEc5xLI8vh6BGP2+b8TgKAKILZTXg3SemKVhGrB3MqY5+V6whOOMT57LHnuwJUEMNMJsswFiLp6UbAlqwiICRF/Jnb+bvCliw4xXvKC6xfAJDQ5qAtcTCAm0uGslNVB5T07pzLFy5EaJS00hqRmvac/P3e8UyL84KjzdngtT3yZ95yCL30Ad9SpGXt76JgCgVJoPFcnPYlJZiXneeB39wFX7rogSSye9XkcBSqEt2ZITemhNHF6LmqSUdnA3rX85S/zDmRiN2NjS1Zjvgc63JjB4B8gQhw3rDllzHDLPBIO3PNInvmVsx5zHOjAMZmyIaoVRtB9BQQnUdHn1WvHdRXmZfs3eVA7jNhDQmTtBzdGtJ31XIHAVkP5ul/NXNBKUytIXDS5hE2s/ZR6j60eIWBlCNxNIlj+42vefIOU+e5kJufMVcBv4aXyQHpunohRHMUYteEC24NO2CW1FNZJlH9BEyf8RKg1/0hDEmySMxcj/S/pXTp5i8nevE+JleHdj3zeyWlGqJsJa7arZV/Kj7qPJYtN4VHoDGp7MFHgKvq6W26lje9EuwJEikt3hqjinocemgMjzEx+PMVPovHIBXyCfuSOtygo59bhUm0pvPgdMmml9UgZeVV1GMzDT//kfmf33ZL+aXTl+zEmnw5fZYwccKfmY6Q++ZjfSQLB9eV6u79F0yq3zh7zGb4RqRR5ZeL2vQaNsL3wyqgNppyJMYSC6gX+ce1aAimwDx4Lr/EnY474W0FXvid1Xc6Bg6KdPrwnKJJpi18zMOmECI68RvMpwmM+92oe/m3dj6YGItooug3uasMy8SPsvLi1RV2F0CQD5CqYs=","layer_level":1},{"id":"7e66719b-4a8a-484d-889d-10fdc76788bb","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"AI平台集成","description":"ai-platform-integration","prompt":"创建AI平台集成的详细文档。解释适配器架构设计,包括BasePlatformAdapter抽象基类的实现和扩展机制。详细说明Kimi平台的浏览器自动化集成,包括Playwright配置、页面交互逻辑和错误重试机制。文档化文心平台的API集成实现,包括请求封装、响应解析和配置管理。说明引用检测引擎的工作原理,包括品牌匹配策略、竞争品牌识别和置信度评分机制。提供新AI平台接入的扩展指南和最佳实践。","order":5,"progress_status":"completed","dependent_files":"backend/app/workers/platforms/base.py,backend/app/workers/platforms/kimi.py,backend/app/workers/platforms/wenxin.py,backend/app/workers/citation_engine.py","gmt_create":"2026-04-22T18:56:47.083138+08:00","gmt_modified":"2026-04-22T19:01:25.986259+08:00","raw_data":"WikiEncrypted:cSfKuI0KndKt4dAjXxIdxWhPkO4T68Pn8OuUxh8ycrOmN2KINuisMW3IOEhdPiFABzUhI7mz9/HfHXQ4zprU/+/sSF8Ao95qt3TWu3d0gdIZ8D5HcXKzDDRDEpL2lUuKjanJ/DyOhfgoAhj4uHHG63PE4lD0nFGhUjI18Mx86VwV/XRvw7SpcidXldGAtwv+SyiBV8jhNv2SsuEp+c64Tpsb4eXXnhU94tHe5zw60yhZPo02AbhrX3CGRvf/e0VDakSRNs2IQLOmYpaXPEbGq1F/Tp1t9NRMOPMypI2xbgsTvkk85z9kGHoS4hRcf6dLIMVrvkCYibs8KhQCMJiDZ66d6j1Vb6dqBvmrKsh3E6WjAycKEMEg/CipCVwEhG2XXjejCze2wysxwhQJevZcegdqOdbxsBBToWhLzOtdumIPjl6TkkzVv7AjaDWHyidsMW21W5FkdxXReA8iCr3BmNkZXyR7BhBY5u4N0ckDpZ5vNXZOYz/nnm9EreXPLovPEB1I2LfReGW91QDOmQpV3kBPeM35QBnLSkx+FIocW4tie7t+M3QfDly8wClVZXt/3Cc96RAY6WNV+YYLMUbk0eoT2oS9tJcllSD2Lsr8dySH9dDel8ELle3TYDsLHrUqx9cVuNFviwe3oCB46zQMNpYbQncs/EfiGeOD2OCtiuPgMJQ2Xr1wz61OtRdGzstiiPcgemrt3kq6Go6kTMDJtBjYxdl7n3V3dIrWro0kMRmrET0FmRL+jOKdnxGBCgn7UUfNtuwY0c63ALecJJYYOTsfyPzHs3Tt7F0jPFfGp1xYzFzaJAtquIYdoM1jeFRd09d8N49nwaK2qynepWbYA5daLMXJ8Rbxlou6PXvIVY/rHl++n+9Z7OO4mAKy2SYe8mpLZqP4Achbv5fwrx7RrwUNcTUOJeXNTIimqqVLvfg/wEfrBk/+isWw2HJf0W0xUuYy0gIClKgSm/wR4uZD/hM7pzbg+Q8DUglIDaVcUMjJnFNqcUrsNEa21UPlYd/E+drdMzZhzn2GHjMnKkAbr8P8DXkbz3RU36nbktlWYlN/hx1lsA9kSsHRiH9Q/xlpT8eBA7rmQiEasbHbaD0PaL8Dyc4IxQ9ZhhWSSUIT5m10WFLz1U2+/9T7i3LHiMeZeFtY4g60GitHBRVqf9+NDKKdYms75Sq8isA35viTaKBUg9/dkwXhZR9TePkcei9/mcG/UOI4dq9SqLAijVNzhLtDoHXCkVCZ4JYzrzIK9A04c+Gtu0xPcRFLewPDEmQPg+3v+cy6McOQdOYqV7rvdr29pmRLOyVxEMs99/vMu6XDAu6UQr44Zq3RVgekLzY7aabJQc/YyWgvzg9dvnh0cw=="},{"id":"850fc702-3eb6-45b4-acd2-ae87fec1f4f1","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"工作器系统","description":"worker-system","prompt":"为GEO工作器系统创建全面的架构文档。详细解释APScheduler任务调度器的配置和使用,包括调度策略、并发控制和错误恢复机制。文档化工作器抽象基类(BaseWorker)的设计,包括通用接口定义、生命周期管理和状态跟踪。说明平台适配器架构,包括BasePlatformAdapter抽象基类、接口规范和扩展机制。详细描述Kimi平台适配器的实现,包括Playwright浏览器自动化、页面交互逻辑和错误重试策略。解释文心平台适配器的设计,包括API调用封装、响应解析和配置管理。包含工作器注册、启动和停止流程。提供性能监控、资源管理和故障诊断指南。","parent_id":"7c2ac186-ec90-400a-ac1d-b3b7ca2f01d5","order":5,"progress_status":"completed","dependent_files":"backend/app/workers/scheduler.py,backend/app/workers/platforms/base.py,backend/app/workers/platforms/kimi.py,backend/app/workers/platforms/wenxin.py","gmt_create":"2026-04-22T18:57:02.318442+08:00","gmt_modified":"2026-04-22T19:24:30.529121+08:00","raw_data":"WikiEncrypted:NKkj/YX2GdbxYjFllBaf8+HUgx0ecKFsz1U3HC2t2IRMbOkBCmoy+IYbhzuQ50K4pCdPARIwK+MaHbv1MDUXpcbdnvRL12GKezTnD100oXzQKqQkhZdDDC4nIEcp6weWLVGVffqyB7aL/dnt1QlE63XyJfCwizBlecxl1GDUB4J0O7SsTFL5gI6enVYO6hOzx3JrWnEFzirRjA6Llz1iFiFUWxTjxjToT1qkxAdREq2omi39vXL1s84I0XOA1blljfNNIGbpkxXW5uohJBndl6l7Bh23wF8z1LfdAavsFmljavCWqoMvA61Dn4jpJT9oDmaKXZpVSuxfMF2Tu3WOJZwISUGfGBJfdbfxNalhuFff+bhZR8P717nLoM3Do7T26rsqW/pr0NJL6aKxrlPCeIzL0v6Ct+HU3ULHT0EZpG8HfU2NhGWAJd0NhX8RZ8B2ZpfXZKioKtCkLhl4oWV6P8/k5g1nWpnP3wpmDwvp+/pGBeaBcYSenwJWgcGR2s0KgVsEW1niM8+JLaK15wFrmKcxve0hIWBiiPnbZKscAYq+5hj3ecte8h/JevEf+SBNB7ify6uUGU3Eve1jXyPeBrc2FCU5ueeJSFD3wu5pHlkzm7PP8UAbQOfwmENf/Mc1YRNi1FJsujdQuJ67474CyCTkCWFhTC539/6iIa7hN8505GK99PxHcIrsPMMT5LU69r67SizSnWV5bawiMPKGosexCHZfhT1jrScQCKeuld0X/lE1WXXm6ADBbfxZrn/iEFK0zc0WKsnT0X/nQI+ycJaw40Cm+soCO6/KtdYYGEzpzViqi2KxYhRyey+o+yIi8V7nVsxUCbaVRVmYNjnK9yX+3ypdVpJLIJXQ5ywaUWfvqgCrsC7Xxba9P9D+0kmnq6jH6ZdIuubXp0yOtWxe+VC4bHAWoc08RoBdLrlTCGz/hmgi5S83hM8MN0gLdQtc2YDsc0Pxw60+ylnkdlKIefBmb7XPiD8RMRvcbDUCGnrpenbxf5eoWLjLWfyULNQHoYINDdIFvn+skLu10KkfTf8lpRNqd2uHkHVhTTCFh25GlRLAdL7QDHMMQbmwG9OTjjt1tMlW6aYJ/pvYQq95uzmLAiICIsf+nT4xgqoFK+tTBmOst7Z3+bj1YJCPtUNyIzC9a3YYfRwlB1ckjgJaCURcXpImdkDtdpl3GP2mtDp9O52ZDAQVAJFVA3wFFV1G+qjqGRl4cDFK7BlAs04O4fvskn26jrDYiVqgdF0pc6eIiPhAJg/1BabciIggV78rEnEg6jiNJ8AhUmKLo8JOvL/C3m1mfcaUbU2K7oK8damVm8BSmIypHMJfCXzHIt6i+CaJLNlZ+c+MJ4WhqgPWc263/SnxO24wH0vaRtqmZ8g72SSFVa+6E65bdhSUaIejIDvILaBv+73KdUt7LGKPmqKoftErfP4grQZACnS3zQLXmnMfdzurPkKbBI1PkyT/ase2Czwu5/LwHWdWDcRQD2uZmUjndsKpbR6/xVu5Ry5VLB/ds2BrgjUp5pD+Z0U995X4yVwpr9oCNxR5r1sl+2Wv3jotf19l+safw9XwODQoURKcJiGxtJDymJvVEyl3ZEe1Izwl7SF4UqtboIYt9vEoEIilsuPPDtujnpN5pkY=","layer_level":1},{"id":"fda0598d-7e92-4b8b-be52-92ae63c3cd98","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"API客户端","description":"api-client","prompt":"创建API客户端的详细实现文档。说明RESTful API的封装设计、请求配置和响应处理机制。文档化API客户端的错误处理、重试机制和超时控制。解释请求拦截器、响应拦截器和中间件模式。说明API版本管理、URL构建和参数序列化。包含认证头自动添加、状态码处理和错误消息格式化。提供API调用的最佳实践、性能优化和调试技巧。","parent_id":"d907a8eb-795c-4060-9cdd-50d31cf39be3","order":5,"progress_status":"completed","dependent_files":"frontend/lib/api.ts","gmt_create":"2026-04-22T18:57:04.217601+08:00","gmt_modified":"2026-04-22T19:24:50.486294+08:00","raw_data":"WikiEncrypted:/umMwKXdJhL9PqhJYRia9MjBtE8cuBSpJq1+zwyrvadwhOTuxc7+NNlJizkRp6kCDsj6HmsrrrEG2WmrNf4m1kFf/yUci2Hny4otqjfpEsOXzmFIxnq13zAZFe1oDyDiHr+shS0DH1omM5IrOroFuNXfBlFlOGXuTKJ3wXXPweq+qAH9QLU237hwRdKdI1UNXoGFfFF9nN8NzOmkwSQ0cRMvswtunfw+yphm0doFqLjrjc0WUZSar4loGeSR6pOSLU8ebi/JkrnwesaWnoYc/1aUJBPdJqVPSJQzjx/Oupuf/R1ihtrn81TMYOUvEaKLk9wVSfSvjh8RGttelGQzTBNJsssgMeqcDh+Qz6r4RJeFKgaRlhn753ONhpYFsHOA/05biB01p6yVuJUR2A3I4tqTlKZ+Xt5K6nvklSqGfR/asdknv3YOkVZOtQxAjyjg/RmmtnslgrEae+k+pBRvgZU23ZT5nZ82LarvwQLYZlu2F16oJjpKEgT3cw98zGHlLEIanqAXIOhT8iR7Fwk6TTKreGcH+oOkFNmbXHWXAD9ixtGbLqoidjzBlLLSR1++hGoabWTFiJX2LKj3lCJ4z30snHEvEL5ruTnKMF+gXM+truHTDsrO5CuaWyGY3WYVTndJKFLi2PQu5FclItm6ykG7LLkMlOXcKTmOrUqOgQhgDR0R7UuPYneG3+8tP49Gar78dBGx+Da8xGwikj9K9OJYAlHpVo05oKo78qku6h+6J2fHgIvIyJPSC88Whc+HpqTBbVMAIjzOQDzTTafDZnEoLWczvFoR5x2aPLnYwXdjyljOhmpXH/bShHcfo1W4KEEzka0Yp+DMZpTiSyhBpA5iLdjZdU9RnTUPTwMtqSXfdbbf0fdgem+z3CsmiuZJVViFNkOfRqL5f7Np4ITC0v2XwLSWr/bC7gbUZiEQdwEr7jwVO1Bk6/MCzvN1G217W7/RUPA4Up/Iu6PRBPcC4dd2r16kpGY4U68Ud7F6iXnc1Twr1Znb+cuTdO4Fu2DQ","layer_level":1},{"id":"7416db23-312f-478d-8883-2fc3c41ac2e8","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"任务调度系统","description":"task-scheduling-system","prompt":"创建任务调度系统的详细文档。解释基于APScheduler的异步任务调度架构,包括调度器配置、任务队列管理和并发控制机制。详细说明查询任务的生命周期管理,包括任务创建、状态跟踪、执行监控和错误恢复。文档化异步任务处理流程,包括任务分发、执行优先级和资源管理。说明调度系统的性能优化策略、监控指标和故障处理机制。提供任务调度的配置选项、扩展方法和调试技巧。","order":6,"progress_status":"completed","dependent_files":"backend/app/workers/scheduler.py,backend/app/workers/citation_engine.py","gmt_create":"2026-04-22T18:56:47.083393+08:00","gmt_modified":"2026-04-22T19:01:21.98587+08:00","raw_data":"WikiEncrypted:sdobwW3Qsckh0ghPExsMqpKHfb7wkdNKzzEb0LwnbwF9AAuPJjv0n5NuRZJh+4pijYZVUlvqLsi39+IX2F1rFb7Cwl+tLWWt9K5KftV+sUHiNTzLlGKWF3ccgJn8PQtwlQ/LphP+9lXOdbRwRrPunFdPPGQ0aAJhFlEdp/Uwe+VmzNLucpMk0Woe3oiuwIpQxqHN7uJ6SP5oNFOoR2O3akimR10a3xnvM7K3IgAIgar1wOmOjYXPU9RedSg1IbRJanNQ9va6o94BGLn2gF0ocelg8C5G7mDGVDD502YOU29dp3lWEQNOuYCrJk/7TcUByfGBJWVfEkAlLTY0PRe+lgOZB+JY/52NeqPb0YHcEM7c4D3T9JvQYzGfe+0ofEf6J56As2Hp1KCTduTBIJiX/T6nzvtKxNeaEMufFNPPuShJhEXDigeOLj8qrNmqr+12QWLAS1zKYmD/SKSmntxDS48qfwW61UHQqKQf2LIZkbu0ckfQaQKM93kBUMsWCNhwXNd3gBKq21bSbyy9vWXeeNaWyMB8PolGGLpIbfsXtza0XBz2QXUjCUWIElMcJwhGT3XR43eMeHirubUg822Bm5xRQOw8pmZlPx/ejjvYwqlYGBBFa1LHLKPoI8RxWL+uYSAuOizhUunkEApUeb6ym6bQ8hYLWQZcA7WeW+rGfVb0ISK6yJ2UWZRDtPE/t7gPPMaIvRWsJMFETsKRL32YueXkQA0p1wF5lPO+RISBhVee1QdaBmSuk6BQHRDsKykEzXjJy9qXY0FX2IxTKr0YgzxlBPYNfCzi51/dbpTQw4PfDvtc+MKF3vTaWUbSfaKtIfbty2WPnqJ41akXcqi+dsUzGkmJBJ53UnWewV0zyLKHTfZTzKkckjZwk2xdvAubvIGr7whGIDapoukknBC7aQc49edSDQwGwQq0YfKhsEyrCwasSLn6Rg/Kfb9cSTywyxcsGx0uEpRl/O66en83ohl1052d0eWy32YvtbdKC11dnf8CWvrf0YVIIiigqswse/6kPNd42EMpHDcB5olJ5S8axdCj4XbLkTdRHVNs//ffb8+RSrE+5wsCgBx5XoaeXB5jP9rQBTXmIneoqiQtwjaen6gUmGXd3XXrRKecdIZTijNMN2ri+x2LV0Et3sF6cqWzz9fNHSX6uNs84JTM0pf40p8RK1MOhe2MQxueK7dz7qC8AM9ouZQU4Kf99iI4Ri8twpaiLAfmtJm8DgeYNdN+7OCh7aJnZB3tCd2DLck="},{"id":"dd12cc44-0c95-45d5-a726-e7b8bcf2531b","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"API接口文档","description":"api-documentation","prompt":"创建GEO平台的完整API接口文档。详细记录所有RESTful API端点,包括HTTP方法、URL模式、请求参数和响应格式。文档化认证接口的JWT令牌管理、用户注册登录和权限验证机制。说明查询管理接口的任务创建、编辑、删除和执行功能。详细描述引用数据接口的数据查询、统计分析和上下文提取功能。文档化报告导出接口的CSV格式生成和数据导出流程。包含错误处理、状态码说明和API使用示例。","order":7,"progress_status":"completed","dependent_files":"backend/app/api/auth.py,backend/app/api/queries.py,backend/app/api/citations.py,backend/app/api/reports.py,backend/app/api/deps.py","gmt_create":"2026-04-22T18:56:47.083629+08:00","gmt_modified":"2026-04-22T19:01:28.273743+08:00","raw_data":"WikiEncrypted:aJsVvsatVVT+Oax9nwx6A6Ka1Sy3/Ax3fbdr8CSPsFxZ2EWp6i+cqnOOrevL+QtbDx7fZdvtUwLW1B9c0o5HgMJhUO2KzsEGP7YsoxlDU753ninxqx1EdyuVgBpfmne2cuiP4lehjMSmZOUjZVE790YXol5PjHzWmKLvGdn4Ak3rcW8kVXk33ATw7m3S7a8cR4jfCtEyCjRkjwO9HmnCUuhWDHm4UBxIMuf64e9rAkWXgXEKmWLVQn6LqfAn+xOYtQNFa+I8V/i0q264hwIcYp54hi7mYo9SwN4UQgBvb136/8gwSUNjcRRGOsNv15tPDC2o3dD6nltM0j5yS87WC52EQFNAdL2LcLM0N2T1bR0gt6wLhrEbP4wI3frsX87BoAoD3SKOnwrpht8ligcnzudjGEB3B6ZzK5iy9iw3aW8IQ3vhQPLcm7ZC9BIiUGYsrVhh1GuQkAMRo+7pIMn+eXtSSR5kEeywAPYB2fGoNC5JROiSbSex7ogUbQSEchN2+vLngdjXQJprQppqPSC05z5kr5MtDzNosC+VAGwd8GUtN4PuMmHXmAEbAfV91lnTahK6I23pFsqmUpPL8F4+9X51VP7HNOjstUQZp27paBBxwQVcayi5YY5BWkkoUh/JD0k4Kge1kf6FpcZTvjgupvFlEFlMW1Qon57sNNm6gvTa8cBf50cJQhsz2y/jmBahK3Jb5PI8RA1wWAfGV6++BrMXYPHJiBmpdPVIrhqKYvqshB1aSuat6W19wgWmssQVogSZ1kpi0qPvCcLQ42fc9WHkr0ZUrWtckhrI8699i7I8wv9wo1qgwACzolsUvaYCCsaNbLSlsI5/GDAACQamzbenzol0/5GVe/rnqNIMtV4ihfmo/xqx2NXcy41ZX0dLBguZH63BzbVtwRXVUAPBX3QtDa6ANXNA/l+rTUD51kAHggAKOGf8DEhJxv+/VJRKJS3i88eDpyaYO/D/CASbjlqsFulTxe6og3RyEnPZkvImOCZA2bF7FAhhL+HGAubI5jLbMhy6JBIOD5zMtOJTn4m/SDKHFFqPNrYptBO/pb3d2xA7/MrGjCt4hxP0ua8BPswc294GZB4PqXWN0jmJQEI0Uo1jJsoMsU+gsp5xeLhOpNw8GFvALa2qpyvGKAJfQb8ML7uQhVAL0OCgx1VG323JeuNtDAAJ4BByXluHnybL3cQrJaNDsFkh7JINf42VvyIYvyNUUpUt8cnguhiUwhc5sPsvmDDBY/N7Btrl8iQnQJf1vbecErl3PgLxKMIzic31zBalQ6ZuLj5/vt8mYAbOMcVrYgqeurv6HyFaEOBN5L+LucZBCIEhb1BN/BixGceuW+FkzhKUw62/YJ1cIQ=="},{"id":"fc6f24c3-594e-4153-854f-19250834eeb1","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"测试策略","description":"testing-strategy","prompt":"创建GEO项目的测试策略文档。详细说明Pytest测试框架的配置和使用方法,包括测试夹具、模拟对象和测试数据管理。文档化单元测试的实现,包括认证模块、引用引擎、查询处理等功能的测试用例设计。说明集成测试的策略,包括API端到端测试、数据库测试和系统集成测试。提供测试最佳实践,包括测试覆盖率要求、持续集成配置和测试环境管理。包含测试调试技巧和性能测试方法。","order":8,"progress_status":"completed","dependent_files":"tests/,tests/conftest.py,tests/test_auth.py,tests/test_citation_engine.py,tests/test_citations.py,tests/test_queries.py","gmt_create":"2026-04-22T18:56:47.083879+08:00","gmt_modified":"2026-04-22T19:02:30.71425+08:00","raw_data":"WikiEncrypted:a51tw9+B5Xez88YuZqi4cPV0J1CJauHTZselAZQ9sG7l4kxIBXkREciZA7/pohakdE3sC1hkisnzN73PhaoUxdvybhz7kQc7mVyOBWRNwkYr9a/q7TEjfGgarJg0hM4Y7AdW3t8VKH9A//8yxExBcKkoxvED2GIiyB+1EESq1gKkenL7nxoYRe3whjG2O3FEBFv9Ho3mg7vmxi6PcPHokGL6lWBoXxLWE8hq+zflPrMRmWTq2inZhXP8Q6ai6DoYAyJQB4XMqSkNrYEplXBOCsKBx+oKOWJoLU2C8mnb7RaaaRhGeC09wYIpTTRc5XpWu2ycDep70W27vus7AlQW05t7Amij1i1J22fqL3mu1n37WaYCtDCyglNddHe7wqWUzbEovxQ2/6B6RVqFV/GY589ZvOaaJ2Nny8/ffhDRS7emYnStcTWnyQMjRoAVBSE7DYtWJtF8dceDarKg/QwIGtYDCW72Ol2r7iPlDt4TJEq/IfVQh1fH4OD8Ag81wOMKEB2ajvsJAsUvbLSOFJCtjFc4/cxSYj1XsU013Tg6vedTRNFE5J1t5ffc7D2E2eROt6sxRZxpJ6orusy5H9IAexDadlKHXdmU9G5EiCA+oSBNzMsutb9BMwv0HB0oH0SgjFHvvGgeKXMomIEKVf2gOvaMLHvvgL5n0hnYlsSvHFAzNCNL3l+UAkF+gBIQkkUKeCed7m5qK+jsw+aGjXfTMKxOn5yvjiXqpjvo38W+KLTNvqCmaWDV2qt8b1ZNc1lJneqkQ6OfpmZgY2OaO3E1KqaTUUYvc1IlB87GdaQRut9+tqTvMFqYUFAAcdWggWayFTqfuwe76oPDyCaKaDeg4vh8sam8rcWSuc53fUeglmB3OUJ933wCTT2H20PH/cH/V6Ace/olr4H9lhuk5kB3ZFmYRM2pMg756Q03uVUzingRX7Nq4fLBaNHKnkdm/ffM2F3sr1wL23SMuRvxc5pJHZn7ee+wCsBurAIghI8+OT3wgjn9NQVeZDwaQJUXrN8AO2oRz11HrW8fiD3ddCg3BcWcqoDwVh+4uJO7nov+41GEqFhjROW3lrRAlqa43VURj6tTsyCYg3uYGq9S07gY0Ifcnfl2e0GJxARoyQ7LuOsGqVScUdSUKqRKYzOpqFszHrfLDa+W7yy+AwXpWEJd7pexY1rRiO1IX1P6Bofatp/bBwvitXxtqwW0vGRQF/J4jWHTsitHw7AATgLA1P6qPPvm6FUZ9SKBtubz7s+Z2vBq5//3W6qs0c6jX3uWzaoJ0ru8OXhFtOPye8yc3kmnBcFd4EEV7xALnxAjhsLW0uI="},{"id":"803bfef4-3985-477e-a61c-915a246d0061","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"部署与运维","description":"deployment-operations","prompt":"创建GEO项目的部署与运维文档。详细说明Docker容器化部署的完整流程,包括镜像构建、容器编排和环境配置。文档化生产环境的部署策略,包括Nginx反向代理配置、SSL证书管理和负载均衡设置。说明监控和日志管理方案,包括应用健康检查、错误追踪和性能监控。提供运维最佳实践,包括备份策略、安全配置和故障恢复流程。包含CI/CD流水线配置和自动化部署方法。","order":9,"progress_status":"completed","dependent_files":"docker-compose.yml,backend/Dockerfile,frontend/Dockerfile,backend/requirements.txt,frontend/package.json","gmt_create":"2026-04-22T18:56:47.08417+08:00","gmt_modified":"2026-04-22T19:02:23.851886+08:00","raw_data":"WikiEncrypted:0IKfLNOWe9mZfG1jVts3w2XbjO+yyEmY3ONOjrL5GjA8+3c0QEcD9OcCVHu+1BW/N5NkoQV86wegqQ9ShX8iSWP5bp76U/WBxmHscWo6tU/8QlGuoaRAEg/ev4XsKkgC39pvYnfm60PfrpsE2hL1E2GfNGgG77OCHFDgZXXNbh9eCPmSkW8+YhED55nkyvxbnzr0nBnhsGtTzPUxHW1tar+GAvP539R0R/4mYQI87YJ70yZ7fcsaLwgOXmzP3dVnnmpLv+9t7xJhpCjyQsxfBfLLWBAADnbPD/XDJUZjfKYBtzZr96wOF29GlOAKy0gF60uUu4ldT+fyQn+nYlMXrNBIPA4xv5zs0p3ITXaqvMn5CnUdObefSllD9bB5ZAVzibfAxAovDM6bFfCBg78rOiHHyhVisNUpyQVL18Ac1DA/7UfdJH03ceu0hk7swBM7UlLgBUuU8iZZq8IKLS9Hunf/aF7T2fuFhJB+LGpBGwV+3OqWaT6v4reKhSW8vMM4x8XvLXgDttwPeltWlMknvzqijyjE0V38mue+Fe5fYrBZIK66PxnXrGUJKvZPwyAC/8LX6Hk+M3aBWjL4QkiKpbN2/pXCR6dXuPfbswZ11T0xSYdC8m+8SmC5UAgcryC0PI+ULgqxDhrheiaPIpfOAGnPr19iY2++A+xP/IPZf4L9hg2cNxi5vJ+ws54SmeQDUdbzx15ZECbSbz9xkShlRAYOjuUB9/IpZFt7EqH7sEqsFlbF73qp72JFPp2/QYv9av/WYcw6WVtRJG4TQ/Z9jB+DN6yCeSC6B/CfK05zoF7yrVWkLPiSSVYGDSBndH9EFJEJJvzO27tkS1wZXs4fTTE1StkTjYn8EBSRVvIs3Ywg1+iM2SFYcbn3I6QCeqaXuMJ/yEJikxIeLJGlNjiShYEo2FOMQ/6Xk+TlHkxUTepgnzhxFkPq9S2dqK/ZunroHtiqObkTqfkqta9V4EreMpXtO/FUBsVUrmehzAmO7CQ53L74HnyAqFJYWA/iDUt8rABkFT69QahL9BU4bts3AKhAypInqPXBL+0v0WyatxLd1jURgSq11VX9UBNBi4li3TcoFc9wsZpX3LeH1vNueLQzvssRDfTY9doM+g0rgBXDOO59DyyOKrDDMU5D52k5KI5SJOaWWxEa24TOoTfC79Jpc6tW0zAhdvdLD6u/pfsZd693uN4RM8fMtZPh58w5rcXuEHrhVI/0JThluHDnb6a962K0mt7d1makwsRTPZvpvaTrgiaoByIyOuKY7q1L"},{"id":"2148d7d6-25c4-497f-8894-c4eb7a0d2bd7","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"开发指南","description":"development-guide","prompt":"创建GEO项目的开发指南文档。详细说明代码规范和最佳实践,包括Python代码风格、TypeScript类型规范和命名约定。文档化开发流程和工作流,包括Git分支策略、代码审查流程和版本发布管理。说明开发工具的使用方法,包括IDE配置、调试技巧和性能分析工具。提供新功能开发的指导原则,包括模块设计、接口定义和测试要求。包含常见开发问题的解决方案和故障排查方法。","order":10,"progress_status":"completed","dependent_files":"frontend/tsconfig.json,frontend/tailwind.config.ts,frontend/.eslintrc.json,backend/requirements.txt,CONTRIBUTING.md","gmt_create":"2026-04-22T18:56:47.084469+08:00","gmt_modified":"2026-04-22T19:02:27.521973+08:00","raw_data":"WikiEncrypted:F3QgleoEfoy16cQggYe9C/ucQxiwh0okVA48AmODs5RKNS8ZgNngqmnHSFcG/1UaJxx+3dUn+EkMTpYUsOg40rZn+i8FphtVARmS5F9VbvHFcBiSlQaH+JQdr5mv45kEraKM1ZQU+BmiivHhLeOOFfZaf4wfF+UeGEwIZ5K3xgdlrIWG5cbIQke0aAuN4SwF5zFLobVRGY/vFRQZvRJuhJF3kH8cBQLGOghlzoYuvIMN0Syv8UZ5Bwec8p+VF36mvhOS8eFAksKHI6byEdpfq9UTiqxgtTq5L6Rj+37HpAP6khb++vSf1A0VT9NXevaHHhhVOWEkLrGTB/CHEKIg4VNJ5WWcgiIXY17BHQyXjIl4uJr0Fu2kI3bicD88UqLxf8vvCmO6N7KSm9OpUdXgYZoLSYFkU03TCUpLmPpUmOuHim5SyBdvTcd1cnYN48DrbjjDTnkYFYcHEsyGmIESGqrS8AMGyLqSnm5D5gNd5fcrj2+Nf4lrHZbGPN/GpOP8DVbWL3BxZsuvhmlXDJYDI4GYZU00w/cqWFfsh//f8AWxQksGMwuNq1z/Za4kUhyjJoFRB+vVjaYxJnmtnzHgcUsroK6pk+mnzl2sFRRlQRuMqDEWNhr31C+GITHwnZA+ISXYuGrJRXiyzbB/ICIMnQLKjTSGTJOv0611R7UfXQ9XSW5XcnEevf/xaKUaXyI/FSOb6NOMw9DEXCzTFiCSCqdXV24hyN/2eLMEWGD2d7hFS4CBCFDcKoDoV3UWasymEMLzPGi+O8yZ6SFUM2OFa+CFcm+CAV0glSLkFZMwoXi2vpmFERQ0wQbxo7EHKlZA2r9j8A33xsPpGi2bODYiAqMxjvXIIOSDkeokKHKcG+VglGTbPiYbKMmWb3UJ8LUxQsSFs9woUopss+huIV87sUJ7wqGYZGRktkElcXzByJv0D1LNiX7Zfkeq/DTmo9gY1fGwt8Lmr/ZToeLVUWrBHfDTpNtE8Vk+2HsAY/Q6mMhPpkWFCLXMhVexKYRhZ78g5WrTyG220Cs1UqKJ9EXOzd03j14pvt8XxTGNYB4512t168u5QR3RTob2+A8xvVhOvZ5bq9fUVTu2BMD6+hxnKeBlnxDPMCDB6TQppLI2ylmxd0STP3NVTtVXA7ZqRslddheuyl+fGiDx3YaKfbF9pmFBIcUVgxFDSi77e7ZkIQnP7jQeoaQtgVtuU/xJX5vPDaFWBTY5qcyicSVwBGJWIzfDA1ylBWdzcfOmtz21yOw="},{"id":"b2bb2e3c-5661-497c-97dd-15bf6ee8b7d4","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"扩展与定制","description":"extension-customization","prompt":"创建GEO项目的扩展与定制文档。详细说明系统的扩展机制,包括新增API接口、前端页面扩展和数据模型扩展的方法。文档化配置定制选项,包括环境变量配置、功能开关和性能调优参数。说明第三方集成的扩展方法,包括新AI平台接入、新数据库支持和新认证方式的集成流程。提供插件系统的使用指南和扩展开发的最佳实践。包含系统定制化的案例研究和实施建议。","order":11,"progress_status":"completed","dependent_files":"backend/app/workers/platforms/base.py,backend/app/models/,frontend/components/,backend/app/api/","gmt_create":"2026-04-22T18:56:47.085228+08:00","gmt_modified":"2026-04-22T19:03:46.033162+08:00","raw_data":"WikiEncrypted:9tFZPEMPWdLFkkE2XbXYMuMB8FuOYQ0fj+/JXYKsHGVuXRejugUICkyxL86Ju3MeDs2yZuuby22yn9F5xTbpPvuSC/FoLxV09I2ZpvfpcQFUCVmZyQN67BPwYA36FWIbbHI5AJYWayiJUo+io2WnBda4ESUkmi1Ifq6VpdD6Et8Rw2b8dgDDMxi9XaN9V0JJWAK4CsV0ZYGNAK947J00M9KveVb322Q2MkjA5aw0eylKLH0fzY4qNXNGDmfCOuncuvGiH/5DvAXCDuDLYzVd1AHr/9UgPCvmp017Oo8KijVdD66YD+QM7Rh+24GqNvD5xFbOQGd7ey9Mzs33E6JIrsbUhBvbLJOVlRo0EdScVPZsU4HO6zqDAg0ozD1a5kGX/WxHBvhKBJATucW9vC2XG5g21SjjE6NdebD3z3ZjN+cu8jYHMi1BwWhOmvNRvN8HIGjzntLglg7fNrii0iJXTxpZ8Mbpmu6Tk88oMecsGleuA2xhK1yEWycuQ2dZn5e179BE3mGaSOT0Fvp5yXgL4htvqVTAu4QZ+JFJVk8e4NkI2KaoEgS8vmkD+tvnSal6LUgMzXvRJVj5c7y5LEqxY7QwEbvunmtmHaw9xxVNmfj54tFWSDCSUVCBp+otUfdEq+e6UMOQ64QITUXyYlP+zGW5J0ZWBf9GlU5j70IekNuO3mLp/yU922OD64PhCEL3HOTmAtV1L1vunWchdtvyht6rkYuys0Al1WSGeCmVT23fmJlSuKe++K/qQ/TheLhgA4euY3OnlacLzbia76+B2p4tJY+XvzzDZ5c3M6YdKmW0+iKB+TRJgVz1Q8GpFIzUk/ShBam/TanUxOcC0ci89hjchiYImL1tQeigX4Rgfcn8mXYH956z7eyiSYtqk7uIsQYfiW7tqvcyN0jJpGUVHd9IzvK2NAC+ICHilErsg5hRJ66KFaugTnjd2WoIkU8rktkywDasvD24NZM417z8B8l8uHq9XNcg+2bdPA9hqx5ujEUiGXyWkBNvwCZXg9aL2q+4y5NMoZh6ak8uEjC1kc1S61q2kFyKyIjV7QbCMaHhjnrBnkSg/kV2cGrBPeOqWdtANBw5wZ/OJ38FLWJ5nPOzreTmQ/R6J6YNCfmRPpfpVj0tH0o+0rffNxxXa2TnUbu4OSSLdCljK6CsfuYmBOStwIQtEs8T8/+PsrbQKY9NM1KHqhg5Nk72UNylvGupV/Zccfjg3z78M9uUqtXSOA=="},{"id":"8e3d62e8-ddbe-4820-bc41-7da9e91ea598","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"故障排除与FAQ","description":"troubleshooting-faq","prompt":"创建GEO项目的故障排除与常见问题解答文档。详细列举和解决常见的认证问题、数据库连接问题、API调用问题和性能问题。提供系统监控和诊断的方法,包括健康检查、错误日志分析和性能指标监控。说明调试技巧和工具使用,包括开发环境调试、生产环境问题排查和性能分析方法。包含常见错误代码的含义和解决方案,以及预防性维护的最佳实践。提供紧急情况下的故障恢复流程和应急处理方案。","order":12,"progress_status":"completed","dependent_files":"backend/app/main.py,backend/app/config.py,frontend/lib/api.ts,docker-compose.yml","gmt_create":"2026-04-22T18:56:47.085508+08:00","gmt_modified":"2026-04-22T19:04:06.13183+08:00","raw_data":"WikiEncrypted:CaKOW8OSSWs4aEYk06Hu0tuZET5JxbVXxA/4vFailoRiq8l6SCRMdp2PbHfZPheZXyPTAgE3AT8Ce8j9xPOCdJxNKAOstuOjZEIRRi9OOYwoluR4MUnob24X+IyLHwTgsJGf9uPY1LVFTI9RTQt8aG1cBHibaRNEJ8+DVkXJW/EDypOxFpOxDxlaCscMdiPRLwhr9V4vJqWV9j2rk1LjP2/M8SJJQ8hbaDoeLevKdECB+aEw9JrL9ycGW0dhxKxQLf/ril9zNHrvahlSXbVZSZ39dpBl57NdFcFaOYt+paf3O6hv3bCTAKGLTRnYa/IuAZ6Xa7YnhqaGQ684LKCBz6VySRNYVUCEvZotYHf1IQdWOj0zFzR7LEIEikIlZyKak++yTNYfbQyBEzye3g6YCmlh6VaVmtGIRzjOYpu0K1UftNcRa1mWnIMrB/Pqe9DpNTg+Yxo3ZHrPyv3ZAmrd8MqA+T+P+/NerCmFD3dRf94NLPP8KU7in+VSfEjVmdE0N+3OfntU+uvNL/kkOhyWNRUKYKFxbqdwCTKqv3Nchs73XyhfNRP7gPkR/stgnowCRnKWS4ncUEJDZTWVFID52kE8nH7Kq/yuB5bJxFX9AeUUaNSBgvMc5YNkq3vULFEqC1/N0bpU4r9+0/U/BcMktmNrRr1HnOIPl3QzwjIsT3j2lp4UXS3Qm4BZFJsh8MVLfgOIMbO1jbuGCXHl88T/rZOCNwf4q93vWo2HnOxDkbdICWgZ9dinRr85RkFQjNWlXdAk0XG8lQPotwJLvS+ZASOv1aWyL42jk/mTJcOU53PFT5rf3+WpVmhNWQGguZ0l04eubO4Va39c6h/+DNsl0OEAiozIMohEgz77qsD47Kf0KZMLnqL3o30TYXYamX/CP8+epBqSWgmzX/ETrrRl/1/JPkSDc//0VC8UmWBJAsdUdzyQh9v9iQddyIZ4MV1k4MGTJ6GwrUqVTszShZJVG80wDMLQvJGqJ0T5rH1P7kBaJ5nBnth822MBy7B1IhN6nDqtLhOJcqT8Qt6x8ODGFx1QMNBdS4lHq5IcRy+nZKiPP3V7uMuuFQsNNodTRhmNnuAliJACWS89hPiGroan2VkusxIu2DW8wdQ/3Z31mIn4cE3O3+Aq6lO+BEMAuYh5WkOEmGsF3cjX/zPPiuBSP7Vd2a9o6gPA7MBfneMWyqodMp6PKh7EG26gaKUaIGq6VkQobSAVfbLxmfIMW6cZ5STH4U1/A7gV2PeuWzwwEJz7axGVQIJ6A9geQevZvD/cnX6BQBuOMkdAOB6PApCpQA=="}],"wiki_items":[{"catalog_id":"1bb55e12-24c4-42cb-8f59-ddc303460d45","title":"项目概述","description":"project-overview","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"34e572eb-6fc8-4de7-8061-63783ef8be24","gmt_create":"2026-04-22T18:58:50.323135+08:00","gmt_modified":"2026-04-22T18:58:50.329272+08:00"},{"catalog_id":"e3ae8925-4862-4280-b85a-0b376841b15e","title":"快速开始","description":"getting-started","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"7f028ad1-e989-43ea-b945-c79c33e6f0e3","gmt_create":"2026-04-22T18:58:54.232866+08:00","gmt_modified":"2026-04-22T18:58:54.238013+08:00"},{"catalog_id":"7c2ac186-ec90-400a-ac1d-b3b7ca2f01d5","title":"后端系统架构","description":"backend-architecture","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"c7eb113b-b503-4d83-be7f-9ccf3350c9d9","gmt_create":"2026-04-22T18:59:20.999631+08:00","gmt_modified":"2026-04-22T18:59:21.004772+08:00"},{"catalog_id":"d907a8eb-795c-4060-9cdd-50d31cf39be3","title":"前端系统架构","description":"frontend-architecture","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"3af3bb9e-4d99-4dc3-b1ed-2686db385c26","gmt_create":"2026-04-22T18:59:54.989699+08:00","gmt_modified":"2026-04-22T18:59:54.994419+08:00"},{"catalog_id":"243a1a45-d31f-405e-a3b9-694d2fe617d5","title":"数据库设计","description":"database-design","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"ffbb970e-56e0-40ad-bd88-1a1e55482d96","gmt_create":"2026-04-22T19:00:15.762815+08:00","gmt_modified":"2026-04-22T19:00:15.768379+08:00"},{"catalog_id":"7416db23-312f-478d-8883-2fc3c41ac2e8","title":"任务调度系统","description":"task-scheduling-system","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"deb919cc-9541-4ed7-a581-ae2876ea67c2","gmt_create":"2026-04-22T19:01:21.979952+08:00","gmt_modified":"2026-04-22T19:01:21.986003+08:00"},{"catalog_id":"7e66719b-4a8a-484d-889d-10fdc76788bb","title":"AI平台集成","description":"ai-platform-integration","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"cfc48cde-e1c3-47f6-afe2-a6d7a8f4ab69","gmt_create":"2026-04-22T19:01:25.981164+08:00","gmt_modified":"2026-04-22T19:01:25.986389+08:00"},{"catalog_id":"dd12cc44-0c95-45d5-a726-e7b8bcf2531b","title":"API接口文档","description":"api-documentation","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"49915365-e38e-43b5-beac-6aa7d5a74cbb","gmt_create":"2026-04-22T19:01:28.269494+08:00","gmt_modified":"2026-04-22T19:01:28.273884+08:00"},{"catalog_id":"803bfef4-3985-477e-a61c-915a246d0061","title":"部署与运维","description":"deployment-operations","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"98909ecc-fceb-4ab7-a26f-741838eb2e50","gmt_create":"2026-04-22T19:02:23.847103+08:00","gmt_modified":"2026-04-22T19:02:23.852058+08:00"},{"catalog_id":"2148d7d6-25c4-497f-8894-c4eb7a0d2bd7","title":"开发指南","description":"development-guide","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"643984fc-5a57-498b-8f25-68cc318d9d82","gmt_create":"2026-04-22T19:02:27.517117+08:00","gmt_modified":"2026-04-22T19:02:27.522128+08:00"},{"catalog_id":"fc6f24c3-594e-4153-854f-19250834eeb1","title":"测试策略","description":"testing-strategy","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"b0c36b2b-bb03-4624-933d-c1f6a320b7ca","gmt_create":"2026-04-22T19:02:30.709013+08:00","gmt_modified":"2026-04-22T19:02:30.714418+08:00"},{"catalog_id":"7c3e3c0b-5d1f-4d64-99be-668041c6cd9d","title":"核心框架配置","description":"core-framework","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"f94da0b4-8a07-4de1-b0b3-d3b32a12c3c1","gmt_create":"2026-04-22T19:03:21.689103+08:00","gmt_modified":"2026-04-22T19:03:21.693211+08:00"},{"catalog_id":"b2bb2e3c-5661-497c-97dd-15bf6ee8b7d4","title":"扩展与定制","description":"extension-customization","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"c5849940-e223-4222-be17-aa0a6cb36bc8","gmt_create":"2026-04-22T19:03:46.027426+08:00","gmt_modified":"2026-04-22T19:03:46.033304+08:00"},{"catalog_id":"8e3d62e8-ddbe-4820-bc41-7da9e91ea598","title":"故障排除与FAQ","description":"troubleshooting-faq","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"85306af2-0edf-42b8-b7ad-c03e769cb1e8","gmt_create":"2026-04-22T19:04:06.127415+08:00","gmt_modified":"2026-04-22T19:04:06.131953+08:00"},{"catalog_id":"bcaa04da-04e3-427f-ba01-847ad657e78a","title":"Next.js应用配置","description":"nextjs-app-config","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"c112a3f4-b0a1-4c4a-a325-e3ede8c90be6","gmt_create":"2026-04-22T19:04:47.356556+08:00","gmt_modified":"2026-04-22T19:04:47.361504+08:00"},{"catalog_id":"9e3d703f-f424-47f4-84df-b99873b93e5a","title":"项目介绍","description":"project-introduction","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"2793782c-91c8-4052-b9db-39513426c736","gmt_create":"2026-04-22T19:05:00.372036+08:00","gmt_modified":"2026-04-22T19:05:00.376145+08:00"},{"catalog_id":"a06436ee-1678-4a51-bbf8-b0d0ac3456b9","title":"数据库架构","description":"database-architecture","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"d5c75004-b01b-416f-850a-4791c5489a32","gmt_create":"2026-04-22T19:05:13.619093+08:00","gmt_modified":"2026-04-22T19:05:13.624074+08:00"},{"catalog_id":"7fea5a24-e6de-4003-bc70-9dae6d8fdb25","title":"适配器架构设计","description":"adapter-architecture","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"37003fc0-1cf5-4264-996b-40807001875f","gmt_create":"2026-04-22T19:06:14.01196+08:00","gmt_modified":"2026-04-22T19:06:14.016291+08:00"},{"catalog_id":"b027f234-4ac5-4d6d-9b38-afc8054325f5","title":"单元测试","description":"unit-testing","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"8150ddbb-7aa9-48d0-9953-2ef55e4bcfd5","gmt_create":"2026-04-22T19:06:55.084551+08:00","gmt_modified":"2026-04-22T19:06:55.090188+08:00"},{"catalog_id":"940e5918-1689-4001-a284-44f2de75b8ee","title":"认证接口","description":"authentication-api","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"7c7564ee-7fe2-4555-8ff0-4ec1b757997a","gmt_create":"2026-04-22T19:07:12.352034+08:00","gmt_modified":"2026-04-22T19:07:12.356599+08:00"},{"catalog_id":"78288302-33bd-44f7-8b29-24f516c8b6bb","title":"调度器设计","description":"scheduler-design","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"a1cc822d-5382-431c-8c49-cf398fb5eb3c","gmt_create":"2026-04-22T19:07:22.166733+08:00","gmt_modified":"2026-04-22T19:07:22.171749+08:00"},{"catalog_id":"6406f42a-e10b-4a2b-84a5-0a21c1c759ea","title":"代码规范","description":"code-standards","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"13c568d2-dfa7-4d1b-81c0-dfef247cbb67","gmt_create":"2026-04-22T19:07:51.271116+08:00","gmt_modified":"2026-04-22T19:07:51.274241+08:00"},{"catalog_id":"a91fff3d-ec5e-43df-8176-22f0084109ef","title":"功能扩展","description":"feature-extension","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"d2c7157d-157f-4990-8f76-11f3ea5435f7","gmt_create":"2026-04-22T19:08:25.13644+08:00","gmt_modified":"2026-04-22T19:08:25.14088+08:00"},{"catalog_id":"e8ec6ac6-ad1a-4332-a7be-727b47d71233","title":"Docker容器化部署","description":"docker-containerization","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"1ed0b482-3d6a-4bfd-af40-47a5d1f3e802","gmt_create":"2026-04-22T19:08:34.079807+08:00","gmt_modified":"2026-04-22T19:08:34.084736+08:00"},{"catalog_id":"c19260e2-5163-43d4-b35a-b48aae995f4a","title":"认证系统","description":"authentication-system","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"df661133-efbf-43fe-97c3-f581c81f47a7","gmt_create":"2026-04-22T19:09:25.676813+08:00","gmt_modified":"2026-04-22T19:09:25.682345+08:00"},{"catalog_id":"9b71fe02-5927-4a19-8db8-66eb129ecd9a","title":"认证系统前端实现","description":"auth-system-frontend","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"2b32ec11-d228-42c0-9232-103ba7e44f71","gmt_create":"2026-04-22T19:09:48.743111+08:00","gmt_modified":"2026-04-22T19:09:48.747746+08:00"},{"catalog_id":"b80dc237-1a6a-401f-9f4d-14190edebcdd","title":"技术栈","description":"technology-stack","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"e23bd86e-b4ac-40eb-b1c1-38d929fd5419","gmt_create":"2026-04-22T19:09:54.21211+08:00","gmt_modified":"2026-04-22T19:09:54.216205+08:00"},{"catalog_id":"7e5c3b8e-5aa3-448d-ae52-d5a96a413b0b","title":"表结构设计","description":"table-schema-design","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"c3fa653f-8dd7-41fe-8c2e-8b60adbf70f4","gmt_create":"2026-04-22T19:10:23.573187+08:00","gmt_modified":"2026-04-22T19:10:23.576944+08:00"},{"catalog_id":"cc7a1f1b-c70e-4c61-bfbc-6dc408a12ff2","title":"查询执行流程","description":"query-execution-flow","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"2fc79486-ec65-4533-860a-89c8877c2ea0","gmt_create":"2026-04-22T19:11:29.34722+08:00","gmt_modified":"2026-04-22T19:11:29.353951+08:00"},{"catalog_id":"fec685a0-c9bb-4048-baf4-40b56b2aa29c","title":"集成测试","description":"integration-testing","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"8c17b44f-1586-459b-a83d-c9b961cd2142","gmt_create":"2026-04-22T19:12:37.811472+08:00","gmt_modified":"2026-04-22T19:12:37.820956+08:00"},{"catalog_id":"9fe32b83-3697-4939-8b10-524f5ed3e65e","title":"Kimi平台集成","description":"kimi-platform-integration","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"2126339b-b0f5-4152-924b-cbe028cd0c39","gmt_create":"2026-04-22T19:12:57.009145+08:00","gmt_modified":"2026-04-22T19:12:57.01442+08:00"},{"catalog_id":"b10c2334-a850-471a-9851-a1c698e3a485","title":"查询管理接口","description":"queries-api","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"74d3018f-4e23-4687-8bf4-7fb403f479f7","gmt_create":"2026-04-22T19:13:13.111099+08:00","gmt_modified":"2026-04-22T19:13:13.115929+08:00"},{"catalog_id":"0c1d3542-92cf-4796-8dba-82caf2f7b361","title":"开发流程","description":"development-workflow","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"222a9371-45c1-4e0d-b1da-e8ada1c501c2","gmt_create":"2026-04-22T19:13:23.079488+08:00","gmt_modified":"2026-04-22T19:13:23.082539+08:00"},{"catalog_id":"109a8fb1-6619-4bc7-8481-e28cc2127d24","title":"生产环境部署","description":"production-deployment","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"26f80935-eac2-44ee-bcdb-d6a79c537750","gmt_create":"2026-04-22T19:13:58.693673+08:00","gmt_modified":"2026-04-22T19:13:58.69756+08:00"},{"catalog_id":"4d5ac6d7-8812-414b-b8df-68574cc36d7d","title":"配置定制","description":"configuration-customization","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"e1e0bd0e-01b3-4543-9781-d9beb32b9a57","gmt_create":"2026-04-22T19:14:17.837895+08:00","gmt_modified":"2026-04-22T19:14:17.84356+08:00"},{"catalog_id":"159f2ccf-71b7-4d1b-a4c4-c15b23a4126e","title":"API接口设计","description":"api-design","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"11936bb5-374f-40f4-bb53-b75264fc4b9d","gmt_create":"2026-04-22T19:15:14.073829+08:00","gmt_modified":"2026-04-22T19:15:14.079089+08:00"},{"catalog_id":"9cc59a5a-f597-4707-b994-b6c49514d553","title":"页面组件设计","description":"page-components","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"ba0390b3-c249-4e92-a3c8-6820343aeda9","gmt_create":"2026-04-22T19:15:16.958138+08:00","gmt_modified":"2026-04-22T19:15:16.963368+08:00"},{"catalog_id":"f70f5d9b-d7c7-4dc6-b36a-5f4508e6acaa","title":"系统架构","description":"system-architecture","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"70b3948e-f456-42d2-b7ad-a0097ad5ee5f","gmt_create":"2026-04-22T19:15:17.448445+08:00","gmt_modified":"2026-04-22T19:15:17.452857+08:00"},{"catalog_id":"816a2805-76c9-4f32-a3cf-96428208081e","title":"数据模型","description":"data-models","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"262a3941-fdc6-46b0-b767-be40aa9d5761","gmt_create":"2026-04-22T19:16:35.087998+08:00","gmt_modified":"2026-04-22T19:16:35.092985+08:00"},{"catalog_id":"64cbb894-755f-47b5-854e-c26c7821e9b2","title":"文心平台集成","description":"wenxin-platform-integration","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"fab60eaf-9652-4cb5-9f9e-0525caa62d63","gmt_create":"2026-04-22T19:16:36.921828+08:00","gmt_modified":"2026-04-22T19:16:36.925945+08:00"},{"catalog_id":"412f8cb5-54c1-4f32-8966-fa0e5e75bbca","title":"性能优化","description":"performance-optimization","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"d8e2cef5-37e6-44e7-8a7b-9bd365b82a72","gmt_create":"2026-04-22T19:16:37.806188+08:00","gmt_modified":"2026-04-22T19:16:37.81795+08:00"},{"catalog_id":"2f7fa0ab-cd3d-4f45-a1c1-389d5a0c2561","title":"开发工具","description":"development-tools","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"979be617-a83d-4db9-b73e-38581348f8c1","gmt_create":"2026-04-22T19:17:43.348035+08:00","gmt_modified":"2026-04-22T19:17:43.3539+08:00"},{"catalog_id":"40ac97e8-7ef0-4198-82d7-d2e332be9d34","title":"测试最佳实践","description":"test-best-practices","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"05e59a75-d52f-42e1-a924-f6a32f06f2fe","gmt_create":"2026-04-22T19:17:48.811042+08:00","gmt_modified":"2026-04-22T19:17:48.815559+08:00"},{"catalog_id":"41a414d2-e13a-497c-8a03-212624dbf5fe","title":"引用数据接口","description":"citations-api","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"737f8d6c-bd8f-4c11-b142-71a400423323","gmt_create":"2026-04-22T19:18:18.048509+08:00","gmt_modified":"2026-04-22T19:18:18.055466+08:00"},{"catalog_id":"2713d5c6-c6b0-4a38-83f6-56940c2bf695","title":"监控与日志管理","description":"monitoring-logging","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"eb1288f7-5ea1-413a-8f54-4f870306d14d","gmt_create":"2026-04-22T19:18:41.294736+08:00","gmt_modified":"2026-04-22T19:18:41.773932+08:00"},{"catalog_id":"c8a468af-2982-4d9c-82c0-313b5d2ee89c","title":"第三方集成","description":"third-party-integration","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"e9a02c86-236b-49cb-bbed-9462ee123c04","gmt_create":"2026-04-22T19:19:17.34688+08:00","gmt_modified":"2026-04-22T19:19:17.362503+08:00"},{"catalog_id":"9eee7fab-6cd9-4ef3-9415-2f8137f1d199","title":"数据模型设计","description":"data-models","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"23ad3419-5473-4e2c-ac87-d9715090279d","gmt_create":"2026-04-22T19:19:19.260533+08:00","gmt_modified":"2026-04-22T19:19:19.282116+08:00"},{"catalog_id":"f210509a-2381-46fe-8c22-0ed768e6ca70","title":"数据库迁移","description":"database-migration","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"7c0201f7-9e02-4cba-9ee1-81c3477f049a","gmt_create":"2026-04-22T19:20:15.023549+08:00","gmt_modified":"2026-04-22T19:20:15.06665+08:00"},{"catalog_id":"d9e45b2a-6443-4a9b-8ed3-4c3c04773772","title":"UI组件库","description":"ui-component-library","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"760610d4-dff6-4c6d-831f-7c9078db86a6","gmt_create":"2026-04-22T19:20:16.20858+08:00","gmt_modified":"2026-04-22T19:20:16.229612+08:00"},{"catalog_id":"178b681f-d013-44b7-aef8-dee7ca8c22a9","title":"核心功能","description":"core-features","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"1c4316a1-f1d7-46da-bcfc-ea2fa0c56110","gmt_create":"2026-04-22T19:20:37.644038+08:00","gmt_modified":"2026-04-22T19:20:37.649918+08:00"},{"catalog_id":"ac0658da-7670-4e41-9e57-02d9d0d50680","title":"报告导出接口","description":"reports-api","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"ae62f4a8-8840-4c8e-9a42-3373370299ff","gmt_create":"2026-04-22T19:21:24.241821+08:00","gmt_modified":"2026-04-22T19:21:24.246484+08:00"},{"catalog_id":"aad61788-1dc9-4682-b743-47188d7aecb6","title":"引用检测算法","description":"citation-detection-algorithm","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"00359e45-209d-4be3-8795-50dea52bdba1","gmt_create":"2026-04-22T19:21:33.106655+08:00","gmt_modified":"2026-04-22T19:21:33.125906+08:00"},{"catalog_id":"7cb1d921-44ed-4e22-8bf9-baba7ff8b7c7","title":"新功能开发","description":"feature-development","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"422c46b3-e69c-4023-a878-411a48ae182f","gmt_create":"2026-04-22T19:21:46.910999+08:00","gmt_modified":"2026-04-22T19:21:46.916175+08:00"},{"catalog_id":"c45e66b9-1ca0-41da-a796-6b98f394faa1","title":"运维最佳实践","description":"maintenance-operations","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"aa6db9b6-71e1-4497-a0e2-f3ff25358d3a","gmt_create":"2026-04-22T19:22:39.442855+08:00","gmt_modified":"2026-04-22T19:22:39.447035+08:00"},{"catalog_id":"bfca5ffe-8905-4ac2-a0a3-8e4dc43533b1","title":"数据可视化","description":"data-visualization","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"a40267bc-925a-4364-a01f-b96d3df60aea","gmt_create":"2026-04-22T19:23:03.398982+08:00","gmt_modified":"2026-04-22T19:23:03.405853+08:00"},{"catalog_id":"05214c1a-d804-4f3c-9048-20ba4de3be0f","title":"引用检测引擎","description":"citation-engine","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"19e75845-5147-4aeb-90be-16f3aa270465","gmt_create":"2026-04-22T19:23:12.049419+08:00","gmt_modified":"2026-04-22T19:23:12.055756+08:00"},{"catalog_id":"b32b024e-2d06-45c8-94c2-a07fd25ab9b3","title":"快速开始","description":"quick-start","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"7e679d62-c415-4d6a-a7e5-d41d00e0ed69","gmt_create":"2026-04-22T19:23:49.794133+08:00","gmt_modified":"2026-04-22T19:23:49.797304+08:00"},{"catalog_id":"850fc702-3eb6-45b4-acd2-ae87fec1f4f1","title":"工作器系统","description":"worker-system","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"83fe6837-7874-4467-8114-103062f15f58","gmt_create":"2026-04-22T19:24:30.524061+08:00","gmt_modified":"2026-04-22T19:24:30.529283+08:00"},{"catalog_id":"fda0598d-7e92-4b8b-be52-92ae63c3cd98","title":"API客户端","description":"api-client","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"7bd2c461-81b0-48e8-8cb3-b4b14305806d","gmt_create":"2026-04-22T19:24:50.482622+08:00","gmt_modified":"2026-04-22T19:24:50.486476+08:00"},{"catalog_id":"15d6170d-716c-4d2a-833d-81211a59027c","title":"平台适配器扩展指南","description":"platform-adapter-extension","extend":"{}","progress_status":"completed","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","id":"2632a6f9-774e-4a91-94da-a984bdb20758","gmt_create":"2026-04-22T19:25:18.714623+08:00","gmt_modified":"2026-04-22T19:25:18.720105+08:00"}],"wiki_overview":{"content":"\u003cblog\u003e\n\n# GEO 平台项目全面分析\n\n## 1. 项目介绍\n\n### 目标与愿景\nGEO 平台是一个基于人工智能技术的学术查询与引用管理系统,旨在为用户提供智能化的品牌引用检测和数据分析服务。该项目通过集成多个AI平台,实现对网络内容中特定品牌引用的自动检测和统计分析。\n\n### 核心目标\n- **智能引用检测**:自动识别网络内容中对特定品牌的引用情况\n- **多平台集成**:支持多个AI平台的数据获取和处理\n- **实时监控**:提供定时查询和实时更新功能\n- **数据可视化**:通过图表展示引用趋势和统计数据\n\n### 目标用户\n- 学术研究人员\n- 市场分析师\n- 品牌监测机构\n- 内容创作者\n\n## 2. 技术架构\n\n### 整体架构设计\n\n```mermaid\ngraph TB\n subgraph \"前端层\"\n FE[Next.js 前端]\n UI[React 组件]\n Auth[认证系统]\n end\n \n subgraph \"后端层\"\n API[FastAPI API]\n WS[工作器]\n SCH[调度器]\n end\n \n subgraph \"数据层\"\n DB[(PostgreSQL)]\n RD[Redis 缓存]\n end\n \n subgraph \"AI平台\"\n WX[Wenxin 平台]\n KM[Kimi 平台]\n end\n \n FE --\u003e API\n UI --\u003e FE\n Auth --\u003e FE\n API --\u003e DB\n API --\u003e RD\n WS --\u003e WX\n WS --\u003e KM\n SCH --\u003e WS\n API --\u003e WS\n```\n\n### 核心设计模式\n- **分层架构**:清晰的前后端分离和业务逻辑分层\n- **异步编程**:基于 asyncio 的高性能异步处理\n- **依赖注入**:通过 FastAPI 的依赖系统管理服务\n- **适配器模式**:统一不同AI平台的接口调用\n\n### 系统关系图\n\n```mermaid\nsequenceDiagram\n participant U as 用户\n participant F as 前端\n participant B as 后端API\n participant S as 调度器\n participant W as 引用引擎\n participant P as AI平台\n \n U-\u003e\u003eF: 发起查询请求\n F-\u003e\u003eB: HTTP请求\n B-\u003e\u003eS: 触发查询任务\n S-\u003e\u003eW: 执行查询\n W-\u003e\u003eP: 调用AI平台\n P--\u003e\u003eW: 返回结果\n W--\u003e\u003eB: 处理后的数据\n B--\u003e\u003eF: 响应结果\n F--\u003e\u003eU: 展示数据\n```\n\n## 3. 关键实现\n\n### 主要入口点\n\n**后端主入口**\n- `backend/app/main.py` - FastAPI 应用主入口,包含路由注册和中间件配置\n\n**前端主入口**\n- `frontend/app/layout.tsx` - Next.js 应用布局组件,包含全局样式和提供者配置\n\n### 核心模块\n\n**数据库配置**\n- `backend/app/database.py` - SQLAlchemy 异步数据库配置和会话管理\n\n**配置管理**\n- `backend/app/config.py` - Pydantic 设置类,管理环境变量和配置参数\n\n**工作器系统**\n- `backend/app/workers/scheduler.py` - APScheduler 定时任务调度器\n- `backend/app/workers/citation_engine.py` - 引用检测引擎核心\n\n**API 路由**\n- `backend/app/api/auth.py` - 用户认证相关接口\n- `backend/app/api/citations.py` - 引用数据相关接口\n- `backend/app/api/queries.py` - 查询任务相关接口\n\n### 配置方法\n\n**Docker 配置**\n- `docker-compose.yml` - 多容器部署配置,包含数据库、缓存和应用服务\n\n**依赖管理**\n- `backend/requirements.txt` - Python 后端依赖包\n- `frontend/package.json` - Node.js 前端依赖包\n\n### 外部依赖\n\n**后端关键技术栈**\n- FastAPI + Uvicorn:高性能异步Web框架\n- SQLAlchemy 2.0:异步ORM框架\n- APScheduler:任务调度框架\n- Redis:缓存和队列存储\n- Playwright:浏览器自动化\n\n**前端关键技术栈**\n- Next.js 14:React 框架\n- Radix UI:可访问性UI组件库\n- Recharts:数据可视化图表\n- Tailwind CSS:CSS框架\n\n### 集成点\n\n**AI平台适配**\n- `backend/app/workers/platforms/kimi.py` - Kimi AI平台适配器\n- `backend/app/workers/platforms/wenxin.py` - 百度文心平台适配器\n\n**认证集成**\n- `frontend/components/providers.tsx` - NextAuth.js 认证提供者\n- `frontend/lib/auth.ts` - 前端认证工具函数\n\n### 组件关系图\n\n```mermaid\ngraph LR\n subgraph \"认证模块\"\n AUTH_API[认证API]\n AUTH_FE[认证前端]\n NEXT_AUTH[NextAuth集成]\n end\n \n subgraph \"查询模块\"\n QUERY_API[查询API]\n QUERY_FE[查询前端]\n SCHEDULER[调度器]\n ENGINE[引用引擎]\n end\n \n subgraph \"数据模块\"\n MODELS[数据模型]\n DB[(数据库)]\n REDIS[(Redis)]\n end\n \n AUTH_API --\u003e MODELS\n QUERY_API --\u003e MODELS\n ENGINE --\u003e MODELS\n AUTH_FE --\u003e AUTH_API\n QUERY_FE --\u003e QUERY_API\n SCHEDULER --\u003e ENGINE\n ENGINE --\u003e DB\n ENGINE --\u003e REDIS\n AUTH_API --\u003e DB\n QUERY_API --\u003e DB\n```\n\n## 4. 核心功能\n\n### 功能概览\n\n**用户管理功能**\n- 用户注册和登录认证\n- JWT 令牌管理和会话维护\n- 用户权限控制\n\n**查询管理功能**\n- 自定义关键词查询\n- 多平台AI数据获取\n- 定时任务调度\n- 查询历史记录\n\n**引用检测功能**\n- 品牌引用自动检测\n- 置信度评分算法\n- 竞争品牌识别\n- 文本上下文提取\n\n**数据分析功能**\n- 引用趋势图表\n- 平台对比分析\n- 统计报表生成\n\n### 实现亮点\n\n**智能匹配算法**\n- `backend/app/workers/citation_engine.py` - 实现精确匹配、别名匹配和模糊匹配三种策略\n- 支持中文和英文混合文本处理\n- 提供置信度评分和位置信息\n\n**异步任务处理**\n- `backend/app/workers/scheduler.py` - 基于 APScheduler 的异步任务调度\n- 支持多平台并发查询\n- 完善的任务状态跟踪\n\n**响应式前端界面**\n- `frontend/app/(dashboard)/` - Next.js App Router 页面组织\n- `frontend/components/charts/` - 数据可视化组件\n- `frontend/components/ui/` - 可复用UI组件库\n\n### 功能架构图\n\n```mermaid\nstateDiagram-v2\n [*] --\u003e 用户认证\n 用户认证 --\u003e 查询管理: 登录成功\n 查询管理 --\u003e 任务调度: 创建查询\n 任务调度 --\u003e 引用检测: 定时触发\n 引用检测 --\u003e 数据处理: 获取AI结果\n 数据处理 --\u003e 结果展示: 处理完成\n 结果展示 --\u003e 查询管理: 新查询\n 结果展示 --\u003e [*]: 退出系统\n \n state 查询管理 {\n [*] --\u003e 创建查询\n 创建查询 --\u003e 编辑查询\n 编辑查询 --\u003e 删除查询\n 删除查询 --\u003e [*]\n }\n \n state 引用检测 {\n [*] --\u003e 平台查询\n 平台查询 --\u003e 品牌匹配\n 品牌匹配 --\u003e 竞争检测\n 竞争检测 --\u003e [*]\n }\n```\n\n## 5. 总结与建议\n\n### 项目架构特点\n\n**架构优势**\n- 清晰的分层设计,前后端职责明确\n- 异步编程模型提升了系统性能\n- 模块化设计便于功能扩展和维护\n- 完善的错误处理和日志记录机制\n\n**技术亮点**\n- 基于现代技术栈的全栈开发\n- 智能算法与工程实践的结合\n- 完整的开发到部署流程\n- 良好的代码组织和文档规范\n\n### 改进建议\n\n**性能优化**\n- 考虑引入连接池优化数据库连接\n- 实现缓存策略减少重复查询\n- 添加限流机制防止API滥用\n\n**功能扩展**\n- 增加更多AI平台支持\n- 实现批量查询功能\n- 添加导出数据功能\n- 增强搜索过滤条件\n\n**代码质量**\n- 完善单元测试覆盖率\n- 添加API文档自动生成\n- 实现更详细的错误处理\n- 优化前端组件复用性\n\n### 下一步发展\n\n**短期目标**\n- 完善用户认证和权限系统\n- 优化引用检测算法准确性\n- 增强前端用户体验\n\n**长期规划**\n- 支持多语言和国际化\n- 实现移动端应用\n- 添加机器学习模型优化\n- 构建开发者API平台\n\nSources:\n- [main.py](backend/app/main.py)\n- [config.py](backend/app/config.py)\n- [database.py](backend/app/database.py)\n- [layout.tsx](frontend/app/layout.tsx)\n- [providers.tsx](frontend/components/providers.tsx)\n- [scheduler.py](backend/app/workers/scheduler.py)\n- [citation_engine.py](backend/app/workers/citation_engine.py)\n- [auth.py](backend/app/api/auth.py)\n- [docker-compose.yml](docker-compose.yml)\n- [requirements.txt](backend/requirements.txt)\n- [package.json](frontend/package.json)\n\n\u003c/blog\u003e","gmt_create":"2026-04-22T18:54:45.079999+08:00","gmt_modified":"2026-04-22T18:54:45.079999+08:00","id":"1e2d007d-9249-436e-8822-c9b1a31580d1","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1"},"wiki_readme":{"content":"No readme file","gmt_create":"2026-04-22T18:53:55.057287+08:00","gmt_modified":"2026-04-22T18:53:55.057287+08:00","id":"354aa9f0-35fb-44a7-98d6-034981f130e6","repo_id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1"},"wiki_repo":{"id":"1095f823-f6f5-4e66-9aad-9f0b5b6b99d1","name":"GEO","progress_status":"completed","wiki_present_status":"COMPLETED","optimized_catalog":"\".\\n├── .npm-cache/\\n│ ├── _cacache/\\n│ ├── _logs/\\n│ └── _npx/\\n├── .pytest_cache/\\n├── backend/\\n│ ├── alembic/\\n│ │ ├── __pycache__/\\n│ │ ├── versions/\\n│ │ │ ├── __pycache__/\\n│ │ │ └── 488d0bd5ab01_initial_migration.py\\n│ │ ├── README\\n│ │ ├── env.py\\n│ │ └── script.py.mako\\n│ ├── app/\\n│ │ ├── __pycache__/\\n│ │ ├── api/\\n│ │ │ ├── __pycache__/\\n│ │ │ ├── __init__.py\\n│ │ │ ├── auth.py\\n│ │ │ ├── citations.py\\n│ │ │ ├── deps.py\\n│ │ │ ├── queries.py\\n│ │ │ └── reports.py\\n│ │ ├── models/\\n│ │ │ ├── __pycache__/\\n│ │ │ ├── __init__.py\\n│ │ │ ├── citation_record.py\\n│ │ │ ├── query.py\\n│ │ │ ├── query_task.py\\n│ │ │ ├── subscription.py\\n│ │ │ └── user.py\\n│ │ ├── schemas/\\n│ │ │ ├── __pycache__/\\n│ │ │ ├── auth.py\\n│ │ │ ├── citation.py\\n│ │ │ └── query.py\\n│ │ ├── services/\\n│ │ │ ├── __pycache__/\\n│ │ │ ├── auth.py\\n│ │ │ ├── citation.py\\n│ │ │ └── query.py\\n│ │ ├── utils/\\n│ │ ├── workers/\\n│ │ │ ├── __pycache__/\\n│ │ │ ├── platforms/\\n│ │ │ │ ├── __pycache__/\\n│ │ │ │ ├── base.py\\n│ │ │ │ ├── kimi.py\\n│ │ │ │ └── wenxin.py\\n│ │ │ ├── __init__.py\\n│ │ │ ├── citation_engine.py\\n│ │ │ └── scheduler.py\\n│ │ ├── config.py\\n│ │ ├── database.py\\n│ │ └── main.py\\n│ ├── venv/\\n│ ├── Dockerfile\\n│ ├── alembic.ini\\n│ └── requirements.txt\\n├── docs/\\n├── frontend/\\n│ ├── .next/\\n│ ├── app/\\n│ │ ├── (auth)/\\n│ │ │ ├── login/\\n│ │ │ │ └── page.tsx\\n│ │ │ ├── register/\\n│ │ │ │ └── page.tsx\\n│ │ │ └── layout.tsx\\n│ │ ├── (dashboard)/\\n│ │ │ ├── dashboard/\\n│ │ │ │ ├── citations/\\n│ │ │ │ │ └── page.tsx\\n│ │ │ │ ├── queries/\\n│ │ │ │ │ └── page.tsx\\n│ │ │ │ ├── reports/\\n│ │ │ │ │ └── page.tsx\\n│ │ │ │ ├── settings/\\n│ │ │ │ │ └── page.tsx\\n│ │ │ │ └── page.tsx\\n│ │ │ └── layout.tsx\\n│ │ ├── api/auth/[...nextauth]/\\n│ │ │ └── route.ts\\n│ │ ├── fonts/\\n│ │ ├── globals.css\\n│ │ ├── layout.tsx\\n│ │ └── page.tsx\\n│ ├── components/\\n│ │ ├── charts/\\n│ │ │ ├── platform-chart.tsx\\n│ │ │ └── trend-chart.tsx\\n│ │ ├── layout/\\n│ │ │ ├── header.tsx\\n│ │ │ └── sidebar.tsx\\n│ │ ├── ui/\\n│ │ │ ├── badge.tsx\\n│ │ │ ├── button.tsx\\n│ │ │ ├── card.tsx\\n│ │ │ ├── dialog.tsx\\n│ │ │ ├── dropdown-menu.tsx\\n│ │ │ ├── input.tsx\\n│ │ │ ├── label.tsx\\n│ │ │ ├── select.tsx\\n│ │ │ ├── table.tsx\\n│ │ │ └── tabs.tsx\\n│ │ └── providers.tsx\\n│ ├── lib/\\n│ │ ├── api.ts\\n│ │ ├── auth.ts\\n│ │ ├── platforms.ts\\n│ │ └── utils.ts\\n│ ├── node_modules/\\n│ ├── types/\\n│ │ └── next-auth.d.ts\\n│ ├── .eslintrc.json\\n│ ├── .gitignore\\n│ ├── Dockerfile\\n│ ├── README.md\\n│ ├── next-env.d.ts\\n│ ├── next.config.mjs\\n│ ├── package-lock.json\\n│ ├── package.json\\n│ ├── postcss.config.mjs\\n│ ├── tailwind.config.ts\\n│ ├── tsconfig.json\\n│ └── tsconfig.tsbuildinfo\\n├── tests/\\n│ ├── __pycache__/\\n│ ├── conftest.py\\n│ ├── test_auth.py\\n│ ├── test_citation_engine.py\\n│ ├── test_citations.py\\n│ └── test_queries.py\\n└── docker-compose.yml\\n\"","current_document_structure":"WikiEncrypted:e2xN3sUmMdAv5mCQlVMmdQnm1GRottmmAIOmJH1eq8PAmhZXawph3iaOinlYEG0yw1Ixi4/YcVqQZoNlfI/w4o97JifPQe21SYbB8BCrda/fb4eJtPG7JzWtfeP/dYddvEdnBwC8gNGE8lzz+SYhmG5bMlIALOfc4J5F5RKwS/00MjU2lajIHiPvVRDS/r+EHs+r5qeI47FQX9XgiXzMdMBELKd6cNHbgYr1MfBXhVo+iQr5NaT5toofLdaF5Hx5SbHxKz/U7SI4ivQLSS9s3jRxGJx7wrqO0jHYE6oW84KldUX5ES7z855neu3XdApslzcuJSX84JdYEVifkQWppAFX0bUMAixAfIfy8dWYKQxY+CPBwn6hycMNnwgXRv5YQLuKqbkEmzUxXOJ/9iTWe5H18h1nqi0as+OCtGM6XddiIClQC5FgR8yarH9qfJxsKmLyvRbS0rPeInm4b9xFG3AmfbRWUyQ5YrQkfdmyUkjSiibXozbzZjkggN0JqLFtmMCSFZiyFWmFwfLyHYHBsquCLbeyWRE9ef41JGILitTsRBZKzqsSRtj79wTNHkjkJ2aTwJmPgfBql+Wjc/fzho5vovWAjRVyYIKBLIt2J/xCrmOvrGuM6ieK+KVx7JUZoSWm4CJ7C1ODg6nub9PcGbTWsHF0iUP9VLF73areyHEzCRMjyMMziQuGwc2k9sn3SeYTpXMgNRI6PTtcSW8XaY+JEcqLDZggjOFt17HMR4o8hEGZd+024udJ1rXs0ZXy93CfUwJWkAIrHNmiqrFA4wpn3OOcS8VWs8yEHrJT+GP3Hb0BJa4HR0YIWaHw4iXW+rY66AFp0NSKY+sc74Xqg5qPSHSNimJ70DfC0DCfw3bo4ssDUusaZamHqNAD8Z6801Xb4Rd0H+3fZ6/genLT91EYChisPoXtXOTWvwTFXHvKNj4uxC0vgZ4xSx2iw2D6WH9RLlmxa6uWg8jnSv6xm4EPcv/ueGj+5AJWWhUOvboYdOZggTgBqYPtrMdcZPHjrtZE479WZ6S+A70n5jxb4hMvk7RDdDxWxamPBT78/HstdKRdWKvS5XCe1EzrlQL4qt5AuPdk58Uric2cPTmjBl+0/+Mh8ixkdIKT0yXWk5NI/8CiQAmBib+30nb/kN+8GWj1+QY/z4BmSpcITby3SG3M9WbLdxgnB6HP9O0ioxgAnWZ768o0uX8GtwWvXvpbiXsrak/koDF/zUkwo1a8Y7WtBNpFwc7sifQqe3+bWJFYExZFL+/zh9P8q+D69IRSg83s9zV35dl/NM4NI8Uy5ckHLdmORwtPocscQm4h/XQPL/kYCDQhHATi/tRanI2vi62JgYpaB65D6uaPQZeSVpTkyWnKWncMUOYdjlKVP11aVZHgEYeg8ylId1sUuP+lGy+QCZalNPFW8wsi1Mw20QetoexRhjS3DaQJtPGSFE3ksaQJIze3CN8ymTibznACUMn3B8lDSxC6x44aXE5EHb9qJEj8nxkOXFdAY2f9l1GmNgcSke94kEcNkSBS4ej0LzuhpxFPcLxjDrMuIStMmeHLRYQdQhSoghYer43lrgY+X5l9gCQH+SHN+UwkLLWbXGleJ2qkzF7HcFjQSLtH7EVAywl13XS9pf4/zn1x5reTnL24CvHQxFmW2blZY+xuCyGjF+5NVZG1UZseLyY/Q5KP4/6FRr6sDdyYCwluLwQdgppUZEH4yYHsxENKn+BXYt52JaxRL3qT777h4yC24UFwc4hzCaR24gn2uBgqy9xFsFaFGdau2wttQ6CT2FAZQkQzP3BSvJcXnjBHEai+eMtJ7/WyD2XoyveyVQBTUJm1U2t9g6L+WDm/HBTpbdn10SAT2ulqLJjmO93Lh7RTPJdc84/58z6x4V1R9WsfguwGHRe9TC78gui2MfdVTK4XqJYre+jS02B2Zbn6BBZdFpCaoQZ8i6tn/vTFHb+Z9txjLu7RvAfebZdYDgtahixsba2k4j+vohip6OC80IhbysYDJC8NA3HK5fEbX9E/XrYvrZoHTeXUr/vIIK3PJ1gXkckXrCYeCEfcxbUN96fkk0PubK7VdwbensIL11/ZREIbh3DegY13ynenPX3qU9Kcv+XgNDK+DDRwvaYrWcgz/3Do0NcFx450Jz+dQIU8VAnhyCPxwikLv3kc7gN1UzLXYIfUFP2JbNw6sGI+SypqesoJH6qvPVBEBUFY/I9S+1THafNBV9Ko45MBauhSq9cxmXqdQwFgURSCnp1YxzS1IAA6XdeykjA/KYLb9woGJw7pnp2ZZzGWd5U66tjnwHBb2xYPDTmFhH1tvyWtEz2t/HoLzIbx6p7PAFnCax1BKzbY8RoB6w9TB5V00O+TjEq9ygLlCGE0vObsbOpSjz85CJMA3QBSc1avzyZnAtzu/WIg5StOaUKAZxhlyVX26n+ts9bzd14V53GVvffTQMlwzGs0gEeITTGbJq0831OILj8LMrUSAuPp/fxZKtsu9MgzQGJ4cVHFwk8KwgARSlDejjp8oQ89mRvX568oLi1U0jzuIgwqDI7bO6fjR95n3AsgoJBwnqtxzsQHbi9jCI2fUodE7Gcw/8ZXKm1zRVLJZ/nvAx0HXEcYonH57r/6XPl2u3ZKAnGtuMUokasitmDYqBiIFRuKvwzmLrE146BFlOf6jhuKHJuJemLlHNCK6hHtzxe9UgKHcDZW6yue4cwUKTD/dbK1eoFtS/oVJNOFCQtVPNSdY1IbfJToaje9w7a7jOAZnfnmv2i8sm02tpmIMh1j7gt9VNrt02RzspVa0JGsnIxP/kKw65DrruZgrlS29QIrVjeEb/D+eWbwswUE0O2alGghQ4+2d3pdlFf4qr0dH28PLnYoYRLMwMu9FTGpQuW2IM2ACJYKgKFw4s8P5rex4Ye8YOP7JF14jvSu2LQj8sGbS4JebkKXzHkBJ4rx8wFDv6xKyxBpWpm8hdhmG7t6wP7TFvozewQFoqSDCdB7xz0nyPnqe3uG2hvApHLHOGmzNvTFmufGTgT5eNfFu79YNIiS6U3tDciycm6kqBMQFaZ4gH10gTfnQYE+y4j67xlQGfTg/jxoQIl5vK+h7sjTl3V/VJqhEdkf6P3n/6z55AM1m6iFknkmHqn+jfTWsvbKuSRgUi2rBHjmqvSU02+DAY220bMJZjJnFYw9ZWgyzFffnH4Qh/IWwbpvELRusxsq62OZ1BcmSldHownIMNVtwJLYY8UiorS6E/xIuZhdCWel1C006wsJuD7x6MWdhxYymNKS7Ilp8VPakzKZweMkNNl/QJrdYLdOlrPHfLogcBF+PDnes8XIT5KxJOSFg8BrttO6RwPC5Exft1HvMsdxaV1HTpyVCH3GaNoDiQAR8QKfBkQFtb9aVGDLUl8C6S3vgk8p18wJP7hBgmKhdDKb9rePoInhIqgBoHiANfK28s5xjXpSOrSP7U15PZVIQtGoC5cHpp6xEqx0NmHTb1B7ZAkvj4kRIIOC4cX1LfEckCBH8aa7+vd7i1MMHxiIxeEZjsTPzVepXI5BFpgOJ8xQXX+g6qQLD7ABn4W/1b/4i7aIKYIEo0KQ30pF5FkidzQuUFue+lprwdQJ+yMofDeUI0cB36Ny9rdVci5flDlpV2LEakzp68ByNxUE3371cVMpTNpNfWPiPxE951j941/EMEEvdPSRrraP/Uhm9Fr5HEi2zU+7wUfh24qYFcBRnaBMDLRF//4USkx27Kgx/hN7uPUdoYRU9/fPUQ9xmClaHxsBRJ2BxU2YC1r5kJAmkEBiknsoqxteDteVT9rE97uC8nCtdIqNTWLdKITriDo/3fi70NG2Hw8kM/UzyfdMkjUMlEiVimI2bfiOnBsMQiK1UYr6B9DJtOql8ukUUedT87NqAYK4qXp7N4i22xTGTRcC75JuKB8xvrkEEpHYpIfreNx9MeeMwJ/yWwvgR/HTLNh3qf/4iA/qUzUzWqHQvpHUuGgPfehMT3TPnIb1nfs2m515tV0KrJVCVmX3gjgNvkoaiHOUAwNwzC8ppXgwNX5/G7dDXgaTktfVlRAeH1bBJzH9iQQsOfZjIjQ4R1As6TyOYWp4Jb2qpuomfiAJpuMQUXkhfbSsiknHKP30Gt2pVd9gQ9vuV9BovLHsFita3iv+vd9rkR6kiRFwdJl+1RuBXLQkNZgfHb7CTHk1j9iOfXK2n9mZyPfOedDTTM4TFlwulTBx6pq9AkNeJ6vDN8n0BZjHNvWK1AGV8NqvKzq7oOqFZEh6e9sYm7hLhTX8x56OIXmj1l2iE/QXNS6pWQi4v8G22jJeKaowGMO6TQDkn0cWDJoNgEgcVOQhUSffxYtmCo5C/X08sU5gF5m2X1Seq/S9amjLfGCUNouswKtJ3MG6HPtZecA5iY8StLw7kxgoRnCzWtTKzki+NK+0PmhtPEDJdOl7MCYXmlYnAW9OAp0miQEcEYobj/KL8hm7RWwhkI6q/Lp83zMNPQFB3bfYtw/2i5GQ4wkjprFH6qWJNvMcHsZ0MKIGora//OIDk37ywYngNvMnuBPQMcQBZeQGy2cl4B7kIh8U10mkNOkf7Sc5pbTNv2p4YMh/Oq1Pr071K3krBhe+4FUs08+hX9QKcaX+nlPLpkeMgYfTATRVWDoEobqp+5YjPHKykOsizbwsbo/Mj2fT/qMwR3kRTuwoay4GhKuZfMPVUAs/CmdYoC51uWXZlTRVTlHg6bQ61Vp7kNrLeQ+NJagrCuu1lkqH+Y7YhUjPqrveFbwlaU0L0Gjtebb2bW+IKdf5BkGK2MKsyAk1UGilbkpKH+A6MShj7rMIWQxNvXsyHcCFj00T32+kIncuppjCxgHiz13ALJx3RWu8OwztlbE31xeWGQ2EKlFvNfY7VpoXOni6dtNLSy57pXQwcm4XKMMOi5+Y+o7rPhH/iFLCDdnAfNl9PkHtb4ROnpDp/wjwRtQRrBjyBHeU4SnMR5V9kWKSJnSV/Tue9bwAjduxk7MM7bW5GGOlGRp1VmWdlxv8B6M5bDkRQdnpel1W33evopsjXc11wmD5YtVmZpICwaMVeJRy0wYa4s1LxV1aRDuTToxNU5jLedlcvm9R6KcGbaJYGJacv6KBcANBkugYUVGNFat50d1/K+6Jl5eyh6/BAbs/mF1JHY+IbJUeG1Kw2Zk5OKnuXweE1ZiGFMGQ6fe93859LpzYTq3GhKW03atJUJvvfDfbXw39V5AilQ8V4j1t9N2fgY2qJp/nTWKh/7KG5NoDPwREhvdK5OH1taZ2NXpsYTyVeQBIFLNO0/LDNQKtfOsx46HzWH0EQyl9B6ahUltJVQWia4yMVHYGbAjBXzoDWdgHeKMcLMrZTz+D2rW0qEA8huNUs+oLDEayxhhM3f8pfnB8sl0CG/X+6WnxsZDlU/X2DIQ2W2kYycTlt6AaO/1pJWuy96nc63lpDaZWmBg48twagwUfD84mqf/BlTtjo43DMcrnXJoi93hjQxzBRDSsrMIFcF0tVRbjHQWW7yEaGCV8iUzl7VxvMKE/8fN1ewUwiJ8ntFGkpujHSHzGQSlS+FFwYNScTzWc3SwzktVAeEdtL/iV5zioUff8H5DaghfzSjwqi6k+HyTbyazE3IK5e7yhd7ulCdYcr/kfu6AwBQlWZyVeMz8fpUASPKoiR+JSYFV1xcHBRa3eqNGHeVxyJDSbNJ6aL9+E6U0x08Q5ZmZFQ2WD7ku5vkr04IsUXhbdsjz29j+CTGMEOi8m9pu1vY6jscb+yfnE5mJR3lb2+frqTK5kNX4axX/scOcnoJW285tEHJ5yFANFBhyZ+XQQ/gBrGGMoDRV5ZlnpeEEn7k/pG/Xwv4u3mZMK2lc1rU7Snuei7lZTPE5ngGD2AWyJhoCwewOAXUnYzJTjqbAvMS5IGrjN1pLiCPZ5zmQEKjxDxYzB5FV9uAnNX1xMiqY3w0cM7q3QXUQrmdZU66aaLAjhPIJKt0UfkXIDZYLZRcOvYmxRL1fhWhrs/j+MVyP/zyIGPjnxAMmitL+5T7Pb3iL/JkUElzl9wofxYlqbXluPL/Bj8BHEEAf6U+BJ8oH/mZfku02uLs7rZcq2+9sf6YsbqEKXnjqgUsYAgocSIwHs+mWkXq5W0Y76OmaLnPE6lF/jYzBCbZ/DIWbRilrbM+zC8egpEsNflog3CrKjcL/ZLBeDq5+16by+6Ma4vOdE+qKqzOKLNw2eMi6/XuJA6GjaKz0jz78+2proKGRDKBS+jP7x+58P1SvXx+Xt/HBBQM+ppzoNL4IL/J8WCL/hLSl7RZgQO4lbjhzejvzEdrYfO+IIWLlrNhN47JitvH4f1KuTeJnoUx1Vgw2LLUnXlZSdQQZyq+VNjM5b0ItTQD2KH9V+ZNZkbo4UK2Hm03AfgUcJwd07U7Ko5CzhzKlO02FFeLBolXdeoOCv7o81ZLRusXAnfuggO9oevucDpIouDHQ4T9FXynu+fyXDCaH6jobkJIrqvIeVAo3wusczl3vlu64EmR314b2BP2Oj2XRXpIyy7SHUmf5x91JZsDrFCS2mJ4EevdIr6sJZekUFl3Yj7zW2N0i6k36Y0eAmLOuucpCaPApIoL12acpO2hpmPJ8ZPpvN7+va7LrVSf4s2WZ4qm1Dy5l4NIaKtUnHLgoIuSEh1e7LJZZ8SNcoDUI5N8GVS4ui3s33BQx26RsSwFJkVm45WfZnrsWnoeEHGVKYVQDdPwkkdW4GHXkqy/4rP8dMMQ3lBe4yWZSCJzxQlmzUNAJlWgNL3AL2vlrJJPDKlbYFzdxMXGTsXzp1gQwhI8BxvzwsX06whT7emrdSQmoZYf4Uo/KO5lY3AeKKw/WV6rlWSxv4ZEOAyu5HSjOUVpQHJVNjsblEpqNUmyDSZIVZ7UnxOYQ6EuCtIH6BeTVFnRlQEIGo18pDtoIH0piFJOOdkVeoFuFgxSZNeyVUvCyVuSZ/liUN/PurYcuFeq/cjLHEIlTthNzJGbw+1FlKnSHp4gf5dT1aTbbIHa78D7//erzwFzVr5sNf5wVQn0vXVdV+92FwUsCXnkMAeUtH1sCJ5UDlF8zVp7oQg/N1TIIx1Gc0cPw+eP3WcF1rjO1KszrKBYZsXweUKd4GPGUqUWMI7Y684vpoKXOXdQ0wub1pOuS8HnJj11PR0JgdDdi2aN6oimjZ/oceDKJMIl3siF0rKlC+rJzDEALuGN4IHw3WQ0k2KwU95jHv7Uzd0IvbtvdQqulN7Yb2FJ08NxQ5DFut6+59oFfeK7YRJ1m1TeMlZaiYv5F5B3D0AXpcJdRO5LtE8EqWIRF+mN3KNrQXtwEmhVCPsmjZtHP42GrfZzGPlnklsoEudo07AcYmwROTm9VgfXIQqobdvU8B6gg5Ikv1/vG0XN7Uhqm7Y+eTKV4NVdBY5DLJjy8vMx1xhoxuzz8fg7WfjgptoYBN73C+b9s8Nv9QJ+HcyLD368XojUCiCCDryzemg6Os+KFpqDaTf//l7J8FztltDlQEAKrymaSV+M4x8/Y+zFAEKmtpc2VxR20qtkwFgifYVYyff9iiykCzqkjJXm8Q15fqqm0WLE8wa6AcQQY4GRu9dqwgRMyc7HyDEFeh7Rc99kqUJn4roBFJP6dCaziIY4HDBQXF/vHEbNmWl4GwwzUfzA7AAhSbPnVCho7UGbKYKUpUA4jVAdAW7itk/zfzPp03LmbdhArRUpP3hf7/nxj32nG2U0vPEYWS9eksPv8dHUn9RuPzDIUt+NPY5F7D9lY2ZqUEuOFUL6nlMTmpnfE+QAxqz9a/qoV4t3iVdTy5GtKeXdCfaI3h9XtKxuwu3OBM/uJ9XJxgyYGH8w8fEQnLT3jxRVTSp+eMTlrdqfGKEsWNFkJjPo1cyGkEhx1mbouqS0fJzJJFyy4WMCEQYQBI2oaXvR3Ba6mc6DLgvuZ4ASaoxTSSZVQCQ7nvZAzQe151REdoUHvXpfgnoxNMfLamnTKi1LT6UYtph/Uu+HCJoW0eUtLmR0/T26A4SbyLbvFviDnbr3Z/2jOePo6IT+Z2MzFv8FP6eJJS4sqiaoTC+I77qbeDwiBxvndig+f4JMRKPPN1vmQ+VWISYUQk2sgPwOinSPfZPm/OVf7+108tR29AE504ecqIInwiXCJC4ktJ4r/5GYvMeWagENxDBAurVEt4SbHGeAvPw+NG+A8bjIkP7Ezl7VPzfjD9fhxOKs09kZFu1YMK6Ci6YkI08CQJZVwD6PT1DYjoTrVZgWFKb2y8/Yzsb1DruBtAmLRKNOB7DfKxmRqIlz8fc2o1r/0Gfi/5U7MYxGvYuclLrcM9UAyl+tHo4ENjJo+cMUFUs4msgdQBAZEsO/ZbtbHQz5GFdZA3dhAbEMFaP5lk6dbjacTdtKh2RtGEVYn0Ka8u9YYChchgGxm9V5FWZbjLpYqzti0hmShoxLj9gIG4myPw7XhvMzJUQYr4bU7dEByyV4UYwWTGl7/ZdKkTKJU35xKXc/ENCdxTj6e9rGTW0JMOn7Ys5d6eoIxTXB1piELk0RbswxAGGx1LH2a/nU1XFxYlevMSOdtxkqlJszVIOZ2DFk5g3UZgJwYm1jXnPqdNyHcGJt07GpBzC/MWdygtb29CyLiqOshXfEj3uQCM+aTxbpyLhiFQSgSOEoz/LCQ6P+Q7C5uMOUelwyV0gxovVSm+qjvMJtYxYZbxObW2nQxK52F1eaHBqmXI56KThDp73n0GgBe16y8MbK+CqRk34GdDp8myK4pPTO2xM9hXR8j8n3B6dWXw0eZPfDFR7tQ0DZelgdJ+Aex2iRX/Ec8oYW0sEvvL/lcq0EDRXxeJoCCabdU87rJj7RvY1SVRu34LZM2fvRg+biPH9yLmCqlwpF87xV3u0TEQL514Y/BagB8dqi6H/4B/FEzx3QqkEOgJadZ948LwWORx0MTn1BR6MqRdqcXhg2AC6dOj6XTEYSJmgm2fkDlgDK4984z5ZaymVR4QCU/DOvRFZT+d/iC8xTmadcqP1CIF6R/fNHCLDzrOquk8jgVDDs/UyNdASxiz/X2aVtEuTzhjqcmLKW8sI7LpoUhTVnEBd1i7Z0+1pBX/RlQFrQwtOgnui0t9n5q3FO03JBHIseM185Xni2MhH5wFomD4b/+QGh/ITBQE9XPjFnpQtI/ZI3Xa6fbm7kqRpQk83VbISCbUYwiLpM1ArK8oTVvXUsSnoAccWUwer/RA5ZwAnnSkOcYKV4bhGlC1F3zKkGO0xUS9rZkJDr0M3N8MHqz46eS2+qgt+pwPBSl2exb4d9IAf0Q1+mhWjRPDbYMMZqcVAgMx1q6pr/zF2GIhi7j9FzRmpLc7ssJiB5Tn/i7ViE17l3yQT0QLdhcYVRavlrs6pilhGh2JG4W+Vcje2kpeFjmdB0t5AHKKTuSJu2OzPdFuGbDU0z5BcyB1jpYeygQDG8hvlFif8aW/7rQmCEKkZlTwuWqQvUbimCk6ADiQ5V1zUkoH3vLnC6Yn9v0QlAkqSbKk4ofQJ9725PN26DM9H4j9MsbQMl2PP/vpnL1yd0V2F84Tgc2rwr0nCUu4yr5KUto3cCkhJJLSpQTLOMcGZniKUbUdM8B9fYNdnBkoCOuyvKxGO24Rz+aPkj7Ve9AIM6+jg/IjMAhjwguXTishlBYZLBEzdp8ZHp9Pqt/Mg9MHUcJffR09qahElny6POFVO5DZbtBgp1dNZnGn4aeWzdluu79gE5y0N05hljg1G25W/YaADwCS4br+cdtg0FuzTy5pjxzhLPc+1CXJi+qXriedpxkFiEDQSYpe59ti2ZupALuERdbDb3E3j0xexbIpKGqWufcl8+6CLPP2Ny2mxA9nvrDnX/xkRgLZQB7ANncEHSm1OnLovGeF2qloFMh0XlrwXtjHjQcyZga3ILDwnOuQgokRiLZTc6X4ih+Snq9vN0ezQOTP5erJlIzll/lYSRz2MC9nsukZCjyzQgU3jyO14G1jf4a9Y+eFDRKrF2llcZ/tVyrJKeDueABE0RLV0EFOwGU54mZZYIysQn88t3QMTWgBijZvPpdbvjvkDCDwR36CWlrvvbLSISFDLRb9uKuzd+fTTN37i5aaVVT0rRiHYAiMHhhvRF4x9y8DRW7LFirOMmei8ZyXyQMP50/S/iBztAEdEUYp1+w/9Oscn7qzuQ8cAsmYzK84QLlBCPyMvfViFrsWnUCapC2OXO9I+X32baTpny60as9NBM+s2DmzrohZ99aKm5uf8oJsXjqmy4H2Jn7YAU1CiPFYIvGJMbmkVYO5WF4efWcN7CGyiyKbT+cw9oN1WLmquPNuxQBxNbvCjdMFiy+QrduVkBqCQ5kx1eY0IaOid8xGZHKGzaZYJxBHAx3gEzxIE+sCko5AdNcZImHHWWUsVgpkMwR/hhCmPbcQs6Mjd6MBU/KghIZKf2+X4nCrtJdi8J1ffwBFzcPVfxcGMw7soXuAB413A4XewaLHi36XFF3oxvnN9DKraUbEF0IrYrirtISpascB+TQFscCxYU3lX7vh5vAcND7mC6jHIWqwV5WBW7JgBFASDezjDzFPsI8YcyojVehtUEA02TP7E8kNj7N0nWiQJMQRsCPurfup7E2b5u4Xln3mGq+aBFykNycefh1+agjBP9EjIlfqmtg9SmDyfTqJUgHPpxLrUMiCuDJmRHlbRhCdCozuIaqlCw8ckfOpnkgGSgW46QjBwkRi+Bz/FAMNM2damO9n/x1bew2fTdY7vypQ7mHfWQHxuqCD6/R7Z+OfGYm81p42feVJXxe+UHZCLlLGM01u4MfuJ3MnPKhQgqHV3WQIlJU+ys0VgSTgDzS8VlPy+47HWFSt213J66J1NdFWZFuQGxum5b0kslya1byTvPB9huh0puMgT34ObqjCNJpX03Bz4ah2VUDOZPPXR5rvagUv8LQWGVwBL//4wClhfQzw/LMjKQBL1vPNxFUKv4kbALm+KyxP0ZwyiGXj+QRVO5zsCURbNCWrdZ4xljXp8oudc1o+onBRDyuVZzZ+qwAscPlfn3CwEQ/Oov5I+/5NV4fAUNSm62rm/NslM3lR6rxdj5KfeM1mM1MhUgswHduy2BdjDHlEPQjEP4+b/rdXOeL12WEmB35llhxX01Wxk2Kgl+v3PLZfuQc8+lhHBiOJPBrzc6N7Yt87bVGV39u534afi2voIwzZ+UW7xb3CZXJ8yItQRieo+upI0ZnF6yF+MLoxas98llawXH5RYdm0eO4GdZjPsvNOiBQnkP6dCeIZo9kVkhlIintzMP0Go+gV4uIC7W4F/V4nKGOzH3II2Do2CF6A7o3dndYLPbELSejXwGZ/85WIKThcqRm28XD7Iar/1tUT7oI3wWWdhgz9lqOOLe3GBcRdbPnga17j9qJLWuPhxU0IrMAAN2u1N8cvVNglH8ooglNDbEbOaayQvs6X7nbgLihRHJTPHyW3zNS0k2Lz9avGuFs/DjDmsKpGTEkAZ9ljTs0Nzh+x8fZsOOSMV4EGuxFrOGyXYfsBFEPJSWmC1VQWWJ0tgrndJeKGHuf4SXfOzIhYxQ8lpU4AlGZCUloEvO4gKdeQvHoAnjpqv1pWWhXwJw8xjOCpVg0USxU+MDzK8B6NQOq0aq1/+ZJq11TGZaqC0EeHbl6UDj+CH4JhHoi60sSDZdZqtheyqay2dVxqRdggzsXOsnXv4qXqQd/ft33jsr75byGDpr7Za9SeGqH8Y77rQYJXwYmfquIKZ6xlMAnbsVvTd7JdV7SoWl0OvYe0hwUBUJMAkraTJTpvAW4KbIgxUJBfQvo6OSwMi7CDiyv8n3o5v9YNFiNCdzbJMCJB+efchCYbNiFa2mrrpQILtfgDXnWMmE6NJjV8cs1+o1EmcZJZgReoxekKk7Q+Vu8sUF1z6xJ8Llv+prdXJlLvVRT9y+th+u7yDzAC0nNdfeAwha1bLpRqhremdwkhvQ9NOfr+XRuHIELVnKf98BDnl+0UUOMVe0IZlv/ICrh0VHzeOu4uxG+qwbTsAMUulWxrz0e84JC7LrwomzMaANfzK6o0VMPKZ6ABWPsqjHuwCPt2cq+ecSBHUOltZ1dq/YLzAHQ0F/cOJi+ZDaVblSZ2DrhyTdShVaD5OpW6e6MqyxOhea4Xze8ZZ8Gw9sdHrTDDcxAmcftiec1wWLaB/IP4cP0XmkUaVAaB5Knj9dKJvmuC+wwQAktgWc4+wL3volF7CQSivtrKLYf9P/aXNExQxVO1LBD20xVJKO+6xCr0gULaK2BRCwW4z2SXB6T1Bu+vmFRW61vQI+9Jm+/OK2SW//EBeu9oifr1HORaIEDp6Cs6IMT9Noe2dRZOBCvJConlkiMWL5+EaEMKU0vVYziumUJ5Q8NhZY+SDFyJcND26ZMB4ATZlJWBmpB/Iu21aGZPhMr+MAP4f9ZQx8tNYbjaZ+wCqgKYSPf4gt6p7rILhTiqZDJKNx1kWz/49uaLBJK7gU0mD0+8/NDysc9HfFOVmF6BBqF8ZWpTuTQZVnP/57jfp+cadK1YU+bz4czDkL5WMatWzMKOiiOu2Kbt75KhsCudAcE00q//gwPxNgCKqUJGPRydGfYXxGwmsXuspHDipX3cHvfZ515/E1AsDKqWrZKpptv2LN35qfVUYbvdK7yTY5jTWZ3GTdSNZ6fPZx3Z/lx4JsZKpdTMXorxx9KM5mkytfMN6G83Q8L+tx8uMV5TeWxLqYRXKr2ZXTezVVHjsgjCFYops77BnVUY5UdrcRj0RE/8PT2duYgj9iFaxaBBtVqKfpfY1OmajJXMtJQOavGtCzgZCGNAv2rl10e2VUh/tplRoLNTAIkyQWRPCs++z2RlMtRCSgjRnzzk/poDvFX0IFnUdmnRADUl76b+y5zKF20yzFVk5losfSFmkz1cCZzl7KzgUr2Tq3x78Dc17f/UjDPPWMEJgvDpAXlVtS1HAe/6KdpxF7zdK0GwJfAMVsJfM9cAoeZXdN5Yx9BPQpl/m+f0SbIZY/XxQAI5qHXjN4SK49cc0nOssFJ9QJjvE2ONpdUn4JcocQD29T2s4l8t/maTghLP2ROvkHkIpE9gjewll7OmxuIOx/O0zHJqRueCqMNHMUQUJ0AUgLp0RfkdstaMmVqDvH9Pgfhw7YHGz7Hovonuanv9d+8NvC0WmD2z9aMIQn8KilY0IhMOxCgi7gtz6CmNncfK/9iBM9DIWMPzbUUizjtAQKRSIEL+4UfAWBawVNNhQthdv3NXbrB9TmDFvPluVyE7lnp/956dQv1OLbioCVZC7F05w4uAgX9p2EkUefxEMIlxVSvM9Xv8lGw1hJheZC8mFhGJ96XGAh17uSSaU1NQwBLmHJ+3Oe+4XbGtrTC7XF+ko3pVj7k9sfVR4eC6DvJQRfQmram6020vCDrRaSg2zjt3HXNKMTGkT6uaKAQ8znQ9GNh2vfv01jFe+YV0F44hHM36oxePIa5qM8TXF06UeKAY6w/XOeRLz2p1WeOl1/OmqYVO7KOfxBY4X251v3NDFtJfRwxbhzALICP5IBajocqHcWfHBvq5D/1p6kPH2Kxj4Y75hVkS+KVk8JVzScDipWkzRfJosn9nKjGGOXlqctMhUDwGXq1xS92MLNZC4VXxPTdzH2yy4SMi8x8lZarmNEIFJVE24qxWm+6GQgbYqbrqPtN8rJpHym3rrppZifxr+ik0VCDzB7XvnVxk1L11RZ/sQBsfwlPR9MTb/Bgqf9KL6smNb2w6zkKVFM3aZVN2741EUBsgBUBuF/8pl0VuIDY6uVu6naLok9CbcZlG2BRW3SBlgBIsQeFrS40dUi98P+GL3S/vSaqpaQf7zErV0FBM/RJTSzraouxV3bBq09qpdY30oTarR3UmA8fbCbsUPnhMKXChMlzxHs4jU1O35IW9p3uc53W1tw4hHBqIGDUBXRu7S6HjqTm3lwJk3N8KEA674dYclP4gM9A2LvvBdoU05tQKIGyrUI8BfvFQEHDuwXJC0iT8jKCb79DYtkF+TSWW1DC23uEjh/tY6HvIKEERa54+9RuTMiW6U8Z+ovjnm27P7JOqDdywZcCseEDTL/4Sp8Rz/KFg/4QoVjTMLj4Z+UMnNowaYHXClO3jHQSmfq68+py3wbFyFG0ehG0Z5MIandbI6mjQJmiidN1v3y2/kW9D7n0ys4QTAnaaxngIu3ul+eia2mtau5WtbmyOzhVnPfszYvKQLHjcGssRcZq3xvL2oNfXc2fcXBvPZEhpDIGUhlnDUJwyo6OiHZ9Qq+Kjv62X3+ER+BBorEIcfxHXUFy/UeXE85XC7WoVKpQCXxbQYr7PhGPwKBikgX5sVJYToA4BpGkjmEBgm7FwxzsSkf6Ps6G3TzjFXpQYCBMSjGmZyQfSUaRK+lCgkWaz8bc3Yo2xShtsoPnaUzCgMh/GRHwei5z6MJTwkgSv5QQUyTzCCXarAzUH8qvstk2/DEH+f/wEGIo8lbz9jl/kSt9gpP+OVM7ZpwyiKLwQimJ5cvQRBytx8mLKfbBOAWrqudJOJ/NPCJsbRGiZeNuZCJ4ygnA5mmBXhWu9c+bMD+N4c3vUBHgldXQHAHMZnCGvEjextgPj8sZfkyePYGHz7KwOzO76zqdll8AVAQbmAf3cPxrah0q40pIVPf6VbBbXtlIuZB0k0OMrXTEdp2oUFA3z3A6LSSj0fwVX6Dt+o4qnVKKTYbhO7BunDtVQr+vHituAsISlCp95OvynY19pXLTYWv8VU+GPkMeM5cCaN818jtNd1go+cL0bfh4GgS7xGZnsccenkYaVKXl+0jcEJ6z4u2+iZcpP3gJugRzp81P181dO51x0MLnNsEtN/LotFOW4Im7v2Z0lrgqlzIy2lumsAQGTH0VkDJsJEtxFvKXz0Jk5GGSCJzvDdwTiagsNO3vWiUf2CP1z96n0HsQozHpxQ2vrnQI0vggtrAOQUSC6Guxw7o/S/Z0IR+byQHaodDlPnUMBWI9oOTb5waBu3BaBJubgDAvobi/5oIiDLryA0sdhr3pahfiOIBB1kpBGIx1uj9MUWq33aJ5vLibA+TnXCE6GnEiJAVDdYzSlOThG+TQi7NNd6iJ10U4Vf7y4G0/w/HbK43BJNaeLRN2rjdl9e60vypByMquBKaczTLHNHd/oSusRHV0RjHF+jJ/CsB289DMM2FaLd23VKoQkd7bG0t/43DlnK6Kx5ZZjTJxcM3XVawFwL5gXgtbWMWHk1OB8VbL3YMvz1Yb3C/fFXzM7+XlY+x0Cy8EmooX0H2vewsSHc/TzOOl2lh+Iv3Aa19d6K2tDIvik31Qqy9KBcDymF5AwkyGSHl+22GRrP8zA8nWkQ96n/RunjYp85GeOmElpZJJ/+Mg54tiolf705Rhsce62i5AK8siYc5lTjiVAI+/y0j6ARuuJA946Jf3JSVPWpeNX4sMFkP0EML8ToI+ugkrGm30iMz602vZt0AHvrhz+bbhkBZKeLTQ9FoJVE4hzwucOyCCW1vmBpkcBJv88u28KVc7UBFwnj3QN8iflBzGm9WMDzqn3Lia31Pi8y705u8jnRMnmZyx3V8DWZ03ExIUR1wezkXEBpZp3aMcfDALv39WQVOsIi7wo8yqxWujKT0sl1MzNR1rkiMJ+tLJKlDDSW5KxWYzzphn6zANoeN9AMsbh+m7Z/TUdcusygHjfayT4CryUmxfEdX8EfaETNkKBLJA6b0FqmW5S+NLe9LMw8kQX6FtgjjX3DmZs+rSPNwHvnERaCg6VR9zhZpfkkyC1+01PeXL5eBbPK1dAIdYaYXpxObB2keydfSqAvp8to2qknhaCj6fy3Svva6NtsxfE66x5jgzbD1zxnRgWy7UfuIrmg9rnVLt5sV5Uc/KvY+zGRqwfGwQcX07K0KdIm+FyuZXpDY9j/bJaRvoKcoU8mU4P76l1awAKHWVRomvbIRyvZmAfDaK7toRuoV2vGj2GTYsqqK2HKASHzl/eVbMXxtwpzmOShNs8cqpAeF2BR1HYuz/Lx4FbRBleQ4TOdIHCX4ZzmiEbjgDvhvYC/ck17G8UEHqEL+/Td9iG8D9OOB1JThpSVjvLvI5MCCl84E4EvBR409VtinziiEQlUk6YCnKl0uPRAwahcdCOlws13Q9ty/ElZ8+4ro8sQPFO+dhIHAPp+idv2lyLBl45pnkfqpIYJEiKCyl6Lsxe2kEhkL7GMPV5LmCHx+hkkujyzYOF4L8kTAQbdidZglx2Vm+HZhrdrtpb+pffeiCRdqroVbm6L9I1CqJ72URPt7Bu2gfW82xDCRnFoBM+C7mIFsMGyLSgtrF32oya9OR2PXdtlxmwG2B65E/028nc0yvFCDn9MiomTMNdyWo+sYu7zTIzDYa7Lp04AFZuYVOwmHE92uuSXEdvuiZrcPQUTPmQp5TfUiq9Nb2o3gKNDxb6QHhC67Tn1uofVXmsqr/CHlTGI6uR8uooBsD3XMymlrrJiopmbNZmicMjJBLGIlM1lgMfD3V//iHPaBxG6kRc1gKE62OazaLEED/Uikf5DQnQFB9qSxdEd5oTIuj++UUTRMuDKZzFTrA/htE066IDFPfl3pBj0eNmYAdcJb+QywXMMB5WFAJ5t3A8QVDsmLw7LExH3MHE+t0QPUDuMAN8tfE2QUxAkYQTnJdS9n7wEzmfwG6Vhe1taSnR/iY9fuussR4+2LNdoutk4S2FFCkYvRPpV/cPNPudiQHBXsvYaUm7OJ803neNWD1L2hryKEbTljH/c3Oz/Wue37eqMRH5x/6vtZAN2cW95zIkV1bBxKQ1hvYywAp488chKWo5bxXvzvQMOb8vZK4/L507eyKCd+w4/nR9g70qZWqGDK3XtW3dc6mpbHjcJ/FLHFhKA54g11Xs2YNIgy6omlKOAZaeMLJO9/WbWrPPNJIn56VqXLJ1GUJkRw8Ueje8L9fA5ajYtPxnqs6ZLwQw+xnKW8ySTP5JQ5UEhNefYPvxXbkMVOEzaH6zd7TVnYZ/PhG9I4nWvCPSW4kdBePkOFXeLKGydeiMZu8UKqrlN9bYL7qduhbhW+EnR9DQ38Owdf4nhPvXiEUqPJkyk806LKSmriM3f01h0iL8IYpJFJ9+8ZZ2jjZy+AnQ95aZ1LxUxDjZUlsrHXTFHvU4EE8gw7VnaLAA3Ta+FDyUGna55Hqexv/UAhQ6MxTZl+2baE9r3HEz76C0tOGEjiBsgc9uV+OyDFltDyut/13uTKsNmAp5uY8KZBP6QOzwwwOqsNS0ziz8yrmgJoNjgcf3udH9TJwwkk0oQKplFXhIHisJBrsLUQ4PvrLk4PgwYAYejLkq8hM1Dqj8zXxGy8c/ATG7QcR5UyR9FKnczLTLvS+U3z2vY4UZun/IpL/14PGaSbwhw/PFhh8y/rkLEarhU5q6IkxlsPbdewhGJfBKGn93+NF8i1WW9NbDjFLssFbMGfBaMnXvbZa04Tx3FFn4P4CqsbV6ytPJTojSr1C9kO7GkBynHa8F51S/NK7V3EzWudOcxIynx1YBbXmEkMinKmF9RoR9rnCM88TLsxSHvVaxUGc0nWwi/G+NOt5PVMX6VZcqIuN10CaSf0bjXGXqP8oPlAFGc6anOQeW3byq8HGnOHx0TaoZcIP4YCulhbyWiDCY594uMkA/0h71M7+fDX17M5ga0YgpdHv5LDySzr6CzkJBrKoQ2EKsg2A5Rp1mcu8b0eNch5qzxFoZXs51ltm8GMDScNJgp9CQiywhRMnV8yrbIEluSVWIlajfODf16VkiU/9ewLt+eIpMLCBOrhOMUeBfkg+M+a6tA0E3FSI72aTkWBI5VtU4yMTAkXaOQEvPGIo4kUzUzGezI9Qm1vUh+pQ9AnqjCTMi3bbMCwRGcdwOV3R8cui3tiq+oHGOh9LM+XOp42wRwHlx20B3SyUvG7zX7rDnC6y0zV85/58ZaHq+WaPVv1vuMJ2dlV3HTyaE+eRkzEqk/zQXGFAE6OQlVsTKSypE3XORa0YQv+wib1IoLhx48EN+iUiWwxORMeyr/RUoEaRmPmDiijqQhVlXZEflTrHR1Y32GDqRsyCZ0yfCt3+OIS59ZsDWzPaOw3MrXUFQgkyz74Jzq8YFRmTSM2HrWh1e/eF3MHcjwITwmtqqVAYv0LcUji8+3YSFRqKToA/BhEBOSnYQ/dSXlZ+VpG4Gp5mxyMSKr65rVE+qmUZ1MBxEcLpF0yraQpnJfbW1HpdNaisrt7y7oPhuQTBj90rgkw83CzNMaY7dw7EZP+ENj+9FUoN7FHMYa6Lh1Uzs/vZ1HvcSDjzejmkuC9IXK3A97XzWSATI/Qe5sbPWesjhpIQqjRJxjApzAaIfbUtQwlQ14L/uw7R5ey/eJnwXDtG6pIfm69aunw+1Z12v+BQ97b50moAp2ne9LoCG43KhHKWexenSWVSqw5FSf8p2GI4y7c+bh+3lZ53sKTGl4Y7anYj1Jb60qW8uNHmsfFPnLUFqX7NnWaT9lI4aKDyNm3IrczGM4H85DIdp9lZL2RfObBoNxlfHLNSaOGdEDIWgh1UsYAFbKpTe1T+HW1eCjYheEv9DVNY2z3kI3MEuDrewI4UCNucSr9Rre7kz1s/jE1kgH8dGwSNMx0ljm7ZIoXkM4KkLut6QtcaN9B7of8D2RH/LElRKJE/Sj2aoDdhBcxSkr23JL9G5h4I9U4r60IFdCV5WIJ6S0ddg3sEflZbqvUT1qSlRnKmj0DnTFhhVVppMz7+m1zsFYDx82bicOMTAofJvCwKO3Tj2eZKO5LrsZ0Ay7FzlJEWBcfjNSO5Fp4EB1wT2ABMiMa6pxH55tuq11XJ+KUcr11ll2sWciawazs5XJ4XY8KSkKZc2u1xTQVbJ+K3fITEkaJW2fgbkrsveqlWU8OkNTUakq2e2Dvk2Y40+dmVOt1YDP9DbQ7as5ZEFeXhmA0rPl6SzIm849qzbXhGkYgSrWUVVVHW/7c1LhxzrDyL9TFZ0UGsz7kcItqaP8xmKCdlrZQO9iO1nUqAJjHhJ1IRxrw45g2atYWTkxXryYqjceJ9ZA6hwRQp+qu6fh3uDwj8/WrniIz9N92dwgpmThtu+fpPvGk4b4cVD2NZ9Up3d6hMjhGBnGbHRthq/tDaREctcudvp8f8cHL7WRZulbfknyOoJd6QYt3GR01iSL1V1Y9AWJqq11EZRIInpjIEkKmTfMQIFqshiX61+JDfJI7139uVw3TXmMQh9meHPAXdhf74pkMOZEl+HYqFGTuVwkJ0a27q0o9BE0pN0SvTxpDmFRSd16DuWi8KVZBEMjgZiH1klwXlfj9mZDHXCsWs+erkFt+mI9TcatZEC1OFG01/pjJhQG2jydB6HxTVw5VxJmiZAkpjEHpkqPWXm1R8IwSK4qRE51bqjq30qiUBKantN7+fxfefshArTOdhc3WvfuL1NvOs4atBNHl0IPor++wfsWStCEG4ME/kbInBTp/zIaTObt6cSjGuO/BLV4BNzV9Tzn8zQFLoZu36x9qUuqlTw8VOaurCJv/ICaZqPcB/FS8N7+ZL3KLQZWuCJrT+KwVjUopIWOc2GXWS2iyxtKXTPFXDSgctSUW8W6Z+b0bQod3guUoRa1wSILMVOnTGUtPZ8ge0PdTFy1opTB4geUjj16oKsZUK8rFBPKz/9WZbZdG8FkMCbLHIY66X26BLeOyPHfjP39Fb/Q8sEGZFFXcapmVjqUGpnIfIvx1EjtCbNZs0KtO77o9/Aa+41qSZji9ikaJC/BzXCsBDNXmUX0ZEIQCCn8nNGJOB5zA0hPo70nIwsq7a84KzrjvX93kI0I+tg7rBzORAnOKalmxMzNDbSoAtuOQjYGpzCc7KsTaiofK2JTGyFOfrKQV20k3EXt0nhrdbkZoINkla58WygTrUbyPlM7z3JJT1pR4Bmec2w2ak5cjaFBJug07AbwshezkswIRi4G8+yyv5fLcy6ykV6a4YyuJj96AZHrE64ljMbiQbDYVQ0pa6KpmK6PwTtJ8jHl0fjSIFWLk5Tm6Ml2ZRO+KzB4wYukFkNktz+ThtpN+jcyqgAEirUVp7MQNJMKsxLOBy4VvxjHAuS2XhgDonfwuPXY2+FBwfY5pwCSVnKO2cWfpvNw5t2W+VmkAacCXLGNxEJCf+5x6KXAmeV/kRV8cArztg6peYfCraSACJAQ9k4sz8x2gIlbTJkj6iaBaphfnn5jtX501PKI9jGcdBubxgX3Il+DHveaKPu3gYUfFtxA1hagFUgffgNQDTlp57mcmC1WeCcnItDbae7Q+m24O93XwOj3oT/gd4rYK65ylRi4fKAi0xXMf3MRMJwk4DRZZarlVCdMT+LB7OCV1Gkx8NGgbp3n6Cl4egGOMZZ+Y1ORJpjBnTUYlk7YQQyZHx1nm9a11dUUtKp2ybl1Odvm8qs3KjW/n/4Vq9MdnS2OsCjuH/SqjZjBSBzkbVcG1ZbNsxH/0kSMJsomBRumTueaApvfwF81qdfr4DtkcHEBWoENYG11Ot+Ynfa0p6/04AZQuFmaAeSrqwlD+1nDzARk78rm31wch0NihwTrkuPRShVfpNkOYcvJrT3LHkxuIH7QUwNOqvXRLxZ/coTL31U6GTWLPAjv/PCl+MMfAj8VWylvcFIDH0wAuEb6qCnZ2//Wx3WMv/geg/lYX42+I1hhQkgdPctw/KeFoGIqKiZBTWcaHjABqUv0/AEGexNCQP2FFCyg8XZVTNFp7Qq3ti/7v1EdgfXrRJ8fvQWmVOcQhwsz0a+qI86//+z4RURm4g+RcmvgOdEOceGEWqwddi1RQRrzPGcxNWojkoowrUQJJHVFibNlBuPt0qyuqE8Ne7OAyYUrDKRA2VAVVWXibCcmB+WDeS/rprdpYVUAeWJ5boHFmdCE6oquPOJwEEgBev4HVMsJaPE/0HRmu5E/9Wkk3kY7+scbH2zga1gXEPUNEbRoECOp0SFaFUsSVHoBSRHDPUjCrdExZ1PatSO9V+x5umiOn7E6KLYNJ2ZTOHgFda95RvGLAXS6bZOC/SXxVJ4GL7mASclisUi6kyqEn8QfXgnFL7MgIjZKJTSV2rWcK3ll2xwRea0KBcj/3DwBmDHxnctxHDKWFE0yMPRi3y/hGBL84GBZwQulFrzsCNlW6boaa+KSD3694n8mQJBnM0iS5q7F0FS2xKLA07cecaM7cZpAhTnkwxWlrUbw/LZXqQ17Mhj/gbn39VEWvayT2GvPci9GrxSgzKcbSv49jcX1H6WcqVoqJ/RIKWB87iN/q9/eAVFDoo2KW84GsSdWxBDFIp7QZCQqXbn0vnoWhibwFEQVy82vVoWubT1qEeh482hCZRF+ojNCpdqB2CcIf2PNu0yGha07yhWKN1L6OGll4aulf6hFm8elgpv8YOS4U5/35YwxY7N+jVd26IcEzzzC6korxs0h/s4oOCNF347iaWqkWCyRpaOySkVKQa3uo/kXBs8BDeQaaldBsMmheea2fhdHwuofkgic1esN9ByOcmN39LNbl2rLsE//an+9VTfl9Vx+Qig4t2HQcU24SesSd4/aRzG8npXqJVS466843FISlNzM4Urs+thgg8Yk5LIwQmCtJQcKMv52GMZB5ioYQyMhr0sOkfkdCGFHF23fh4lFRPDsptPTxT8jUMwigP9LWJ8E9cwFWR+7y3clqXh6Vp0QU7lfFmvJBy/Gwg6rOxFUZZ+kq7t3JUoVyaUKZbdvp9TKmRRIbembW1M4+xhBB/0fANdaT7Zc661zYjKMyhM8LhqEzbwAcW91zNe6AJ04RSe28kQjdlWl8g6Bfta7yYh5nrQIa9rPQSSyBj2lM9bzOY0Go4to5pLI0ldZI94bw3uUmwCZdowdZ2oLilH3WXNFrc7N2DhcCOyMBNpbHzObxCJ34M9KKWokXYecZ1GyTJXRGChE/+1SLWDVDItukKuAxy2/07JxWCSBYMnPWGu1JuuUHveVyrVktodA9MypQrN8IpPaQa/dSBz/hCIAWAHeiTy2bOcnT1WufiWpUzn5vNGO7aRAodJObSHN08nZE12BuETI3qRKakxCcVUskcD4b+yu9uofo9l+1ZEmPJD74fejB/Edj4XffMX4N/S585fbCAZU5XkjQnp0WudL00fw4/+MTxoY+sFTiQktCu7/Ggi/tDEFTwDLArF3lW5BJGp7pFdz7cDSOdsRRylhGrTenUHy15UuNAD6UT5Dy+t8MAouVWv7Uo1VkyXdra57HMj5YXFIjV/Tqr1q1nDRicN2lm+C1AxVkdhy+gDw7CefSUAjK68CZKFO/GcpY45SSSRWHvs9f/Ojhcz1JYtQfnQWe9hY/Odlfn4yrvdQ/yn5cSU6wxFw53wrBYIQCSnICcDg/ejZC+QV2giz0rxVD3bSZuO4sa/cP62wFXdZIWeL3sCHB1Zmn2C8HTYtUfTh9BYOS3pJP+1cr10STKm5WH4wplvZHis3YTyqKyjBUWUQFVvBJXZ09MVlMG8Zw5Cdyjic05jEIuAk0Ktptj9aBRnMQxxpb8neBKCBYfWvOtA7Xt3tUCJEvmz9JUu9DcVdREVuj1qTN04WA9zanNHqU10JIRAgZ5rlTT2Q7QIPBHvEr/2OmfnEawR2Y8FP2BBafqPaUEZdui2HEJFQY1823aYRX4dyyo/XnzKhKqX2HBau/MwY+Ky4g41YtoObcDbKiiQAQhuQSRhrwvycxd3h1fcxJLOGxOUPpjoyg7MKeZiFQJ9ffRfsCE3AmWhiqe5Drh1FNJCGyI2io9fC7PdsXmTle/OU00x8qVTIPvHMg7YK1I1wcvK/BslyDYDPbmwD7AEeORFqXd7JmmfbpxPtktPLN0AIcFg17mNFLIvK8K1MySVhqDnMM0elTrQHEeshF22wLGvv/1ns1owCb3GKUHj5VpiAgkFgG5hB17tOAIjJjbkMby+k4E3K4dcYBMFejYeaEOVoatgq2HkT5UboWRf8E76IsgYe2jN2cPYjEay82PHoMQlxRppi6GA+DkXx1ip4sQZXY95xFnCexEjGAsrYz0UeYiEORnEqN91XWR8ca7NoYqLOaqJUmYBhxj2cS050HMVWxRVuKr4iF/C16HhJzWhVKeg0YqFK7AmabhwfJDONaI6LQ1Tp+NeSgZxtDBcGMSL6gOXxyMs4h/4kMvx5y24IVIMHf27pp9/hOc4gUCMWgf2NA1TWGLy92KZIduWQyi/qsVJgiDqIfUXGWjJH1VTannfwuM+xz/oC8PpzKbZcdeB98AgFYQWpZGGiKU/s2+S+r8916i9md4BcQsGZb65biM74c2vfyylrPCV7feRQbN1BBriCTIYr533PdB8FUnoTUfmnANJZL8BPnTBl1+aeSqeBoJJoemAFHLHpwzqqtiaFVP8ifOxyyp2UpFoPUV0JW4/5OrOJ0K3GdORAcMzOgHWMXfHN42FvxwrgeRJIBG5+idos4/rKJaUxVxUczziFAM4vTk4S8n2UUTB8O9HnIREM8VvhGLCfEHWeDrfLWeYs/iexilKsGlTpkrNqdh0l0gRWVsNTyzwd0/XE56tQy9Q8WJLMJmHetNjEwZtmBqicI4CNl3b9Ij+i102m7TZX9s8fcV6JHdkTky5hGTw/0/S9m6kfwghwm4WvVvQRF1Kv65bAauwmxTb6a/Xka2Q2TCMaSoNEJilopwovD/1LsdxU5fmuxZ3VNjohjC3YdX4qKZdt1qaergs73UpVAKpESVhnCueaYec8alsvX/UM6j1dyR3V2HfziJbccDH2Yc6fcPqWPhNCpNO9Sd+BtuZJJQzSE0JfuWNmeDaRRRvghI1cnQtCsVvf4x2ZxlB7kd+PWqScqZQdATq8YW3f9dKOYnZEINkGjfjV5lpqggYpQJv48Z92O6k6JPmlCCxIz6g17OegkAsSwVEE0aSYgzZahWYqK2dNpYBxDBensmRhjfF5q+dPOfWw8VZ5NL/GVjZn74Pbga/DMDWEsMFd6aIiRCiZWERFtdg2voUJejJvMH7qeV+TE1GlJyID+wZh/q0tPzJ1UU5IxayIhbDJEfvJ6kydHDxpVBfpEKGZTD+6f5zlk1F5M6JZl7kdQmt7jvFXbyzV9Xy8LR9VTaluVLpdhhl30ICAoq7XZwvgGW3Ww+CY1DpRafbA+j746C5uKfMAPk5gZW3VvzxiTAGhvWH4nCLxUI5ZdBNsy+g5azG/pvlMyEKORR7wZd2CAWK9BVx2SykR466I4Udj0u273+8ozre5F6ycDGtp4IJoTP5RCXM9R3pT9oUBVM6AcS9Xc4Me9Ky2Q5AabxLYTaAYN6mR/rEfaBTK8EVQjcY+nWDTIGfvCaXvFJ0pXV/IkSmTzpiriH242uPVo/KJTl/CrcplMqgrbaSxhZLGRrN7UPl82Kchke+YYd+LRjp/+X5wR5ATOBOokAzj+Yep121PBInBi4vqGdRuvF0hNsmuTcIqfdeYToSt7x8Xj4KMF13wv8gnvtdCh5qNIIGqN1eLeLaq9Qx35m8ASvEddH9U1ci2fWELuFEUPYe3zeZwLST5WkUxizPGsnYwAaMuBFeZah8jznuX/wlrlXzj+F5IZfL443i30Nf7+wzjIqimrURN/tdsZSB3kxWh3Xcu02LlD3DhtB6Yv7v7IY7snYeXGVq0D0E84kyONZSWR5b/5zaveRndIs/sICuXJKkZABN0z72XGdo6sotFfaIuiLV4U7YLSdl+V1L80onYCavOtbg+TNgNM6z+yAO3K0lybkPesoIwuJU0r78tIcrtMuOKpSoV4k3Tl1q15aE7O6FFx+HNARlm3pFT0D0DpQC0XwPz+2kZPIokowLV7ygUNsK/oFud0/yQmelATb1AN6k64wChhISgyTAxg+E76CPPpELgWnEOOg6yapyO1k5kW/8eQWIAjBsX643MWDNKrOsrEr6E8wmWvVc4ox9gsd7STptxf5hLWNA/TjrkOxo4E+OfzJAqGn2EOEZVKyuhmON4jE2d/W5yf2LjNlrMIuRH2brJ1usK0rP87omdHy+HD7FNsAi3wuX/eHy3eoLElfwpkamtCieTTX/qj4YZVokk5+4LvvRBWgmsmKgfgDKEafqzeblkPXYhSG1biG1RNCIty2w31aSFwK1ZovfK3HlvjI2E+tTxrbxtCDHAf/DXk9sOK4opvfIAPle/2i8DmjXdlvZ0oDuNFMjDk80eETTkU1Pl+wVnHWMB289x6a2Lm4DYAVHEfyNez2pUT3zaMQbM81H9OeSlkLDTg3Ht1jDUE6M+AX/9vG9pARptqoWhOl/MIWxfroxlMnEM+QHkMfaZIQBKKIW0Jo1hAod4AxazmXurP8uyPX2ptTlgLlyFOCFRKv0ZiDiG4NQV6mutFUvFW1sUko0fFMbOijIPQR/SCHWKXVErfSvACzMgnmlcLYbbNAqug1lsEeyGP/p2fHc2CVowmQ2ikbWdDf6d2K/PpFFCpthxPkFXPdKpAQNrQkMm2MdXI8c+n6TIyvLFFMQrSRPivFvXNMDyHWSfsXypCm5jBZ3AHv1c8uaReqIvhqF0H9AwEgL7+gTow/EIe3FS91omoH8Jcykebxo77mxZQsLvXZGcbZQPldnMjL2mF93n8nBh9AqufWIzb08yjPzIyrc6FvZCBMcBjn/DnsAHjK+otBmx0FuFFzXfipDRlHrXWqqzLOoc3U8wkdt9ioByR46U7gjDZ0okAiyT6pLXhhEd2AzA0isWEHuMA353xlFaD6S2TEWO8jKRb936GziwtTKWAoCqLBcTmk4ND1Rq0pfU314t+zuyvTRF55ZLrwNrYLM22+e6w+/s3SfV1vTO7tIp6IoBO7oynnjr+i+AmsPnE9Fi71mvZu1s84t2GNMYiogRs+qNH9ExEOVKEOazHs2crEi1ZDtPSrurbT0xdf3J7946FrdzanIAxyKWkLSjwJFSLxnhlsIckOwKUQJXgVvixTcVh7bXnTZajkw/8zG2vmzZ5L5Jtz64oNpjhDPXbRCCRquTJi02jQUE96pRRBzZk4pDKXIfGGBY+bmdl/u+Bh106Wbv//8lpqTz7cpvi6CD0Q0qPSo8YVeAawF/Cxf/YjzD+WwBb2NyWEnxfl04KHjiF/QWyTQq8rZYqFQdzNlChyeGf6//F9XZigbPxRZ7KsZ3QQOsn8HS6hJaJrjBWiDP601+rTk1oQ8mJ+g4zg7MCjBCyw8Nx3H9x7JcDedXFT1nnvtp65di/TkicONEr3KsayF+AcZGizdrgaSdvP7OVPgVXdMjdZ9KTn/klL+3qTtFsDWew0+T5S+tAVc2e8fkcXEzMIC1OTX73kr05ZjU3h/0Mb2estZCtd1wN+IKaEFJ4mDA/zdAukByKfZ/Sz2G6frdGWK9RIcs3IIWv5VbMjdWRGi0Ni3FJ/zcIRyQhiyPaVj3b44hbjF0pWMqfDV11/H9sZAg0u3znLuYVOYTrOYyWl8D+lZvJ/O6cAE1SS45SD1AvRsvJiJWAnygKqv6MWGOySRfjNJbGO+LnqMUbO5+ipqk7ygp+MNIQXeisCbxQaxR4CKbsSM10aXeFTI33nNMjIYA05f60Ni/IFEm18YKIz1oJrY0+yCcTDMAu9bmAIgl8njM1GOo2TY66ToM0/2RWJ4wYEW87V45Id2fSzMtLlAVES3giWCSaVaIJ4c0EtgiCUIJU0U3tiO99KfQIt1do3id86uaVzmDWYECuXHkBP3Vn9pwffQaQCERfDzQayOiJ8oQBfsXkYEv3NsiwvgNqyNeaizMUyvLdcdAjvuL14HTaGax25f5UnXF0GBtH4gqdVNoX1LvOY70AUiJcL4NN5ISX4FZAtkee8naZgSWyauPztiNPl7Z5pURHdjlDpL9GsXnCz83DwPaXly03k47JxPWc3iV6aOEXO96FicCeEGPno9Z7wx/HKUU970lCspk+TsZpr5f41OhLipmpCRk1Si9spf/X2sFrvWhSWm4MtfbENGpFmwpvRXVWmf+4rmWCtzq0EdJUPAzvYsnfcH9/tclvuu1LBTfEcJHILxcofn9lEarqGfkZihH4TH/x9gOP9ZEg/Kei3/cYkB69umViXUG5I4p00OX0PmAE2WX6D3PqSuCLWvh7u7Azqo0qUISYeMDcADKcI5+TxlL8JvNC/zdxZIZYnyNThsnQSPn+PdaZy0bJZIrpKaAB/ZgNZHW0ygrVOw+hbyWsanM0c1/kEWsJkN/HnglKme3MK2CnRKOpVB+SxL0a8stgcbYNZSLsPga0n9Oz7MUYKFzQ6tGaxazwG3xRGmGfjhH/QRjEx4CXxEFxUJZseZQ17Sd0xWBO0KfmjEnPD35/sZniS0FE7xQ/leAZliQ3q9xC+g7njTRFcYlvcDCNLhL88FgDHWMRkj52JGhXCkvqLjnwdCD+NHdlQyUeJxpH7uwCCUtzLILhk6Be47dzvwUfQAn2H4h8e8U+fUI7mAPfv/IJo8kRc4KN9UtEITboe2eOfMBn8JxF1vCcssg8KmmKVYJysbolypW1l1tyqzFWeSS2KMeEN1fYYmAHbI2V/XqEF4eLTqrhpNgsFkpcpu5D/Ht+hWhtQImrj/DNkEqzBsQV+rrpqhnu9Su+hHApD3ia5X1AumO3NADm3h2yspu2tgsKXgNZBINRX6zBd3zdxqCm3waki6FxbGl4Pu24qHcQMjYkuisgkCNXWqJvqnCFYbcKnTu0P26DWuHe3ZQytea20pH+yJdS6SzizvyF/X3qLG/59Mld6wt5W6LIk57daDlElqWmFpI19CLaEDpCMjaagE+zsOnqcWmsz5Rp4gPZZV0HQCUHodTyDACuQNDHonzz6s8AAv7F+TTSoSS1a66N6YMKF6hWFKeJX2MtXB94EtfgJwXNIuPWvG8/j3j2c1GNxc39K31CFY21Z6RNuznnAwGD8R7pT7brvuTvJXAVisyk+w5pTunc7QdCS7KxPAGG/ZvKLiA/qkGs1+4oAlyspFfkKdRom+4vA5QM6m01hMQOBR3VrkclK4/pqOXSy/OErcmt6uSil1qCiIKO914qD1jR4R8f0baPw0UTy8SogP0FJETqLFkuZQAODXFECHqcM/ftO2EUPN6Ui7k8vO/2J9lHL/dnRxYwKETWwCC5/UA3Yld8wx17ImYLDVU+nwNVtSx72B8n/GWG9MLomGglUzYWCKGdCRKW6UmGIXwc2qT8ssJziBkvh4Pio/7X6eLC97UxaZ9ulg16O/gTZlpvTJW2hPAyuX35u1hj3QAr9SKBXuPcrNNGxflo838EwROBSNcIl4LoB7T+zgtSU2nux4wd98kA8WYzoi+Q8Y16GlJf2AuP74mtkPgHdMDYA/Q+z+SvzE3WyPN0ztnTs7/hBm1ASpLeHNgeuKTib75Wd3v7QQmpg8z33kblqeiOwI4D8SEtpPzq8xaYJDPsq9EWfB3Pqeryi3uchzBVt6I0KtMTkU+MF56/a9QRkpsQcYJvMeFoPHZCs0AclOP+w3W8ywTQUubu/md6zUSChzLZNXxpycOK4Rj9dUakITX1QG/MsFucaCHva6SPudAAh9uRsdxqZvYFHC1VLLL32/OLfs7Ch6h2pS0GlBYMFfz4wp3pAmHWmkgnmKgJz7TIoY1h3nESMRnnyIlJG4Y/tpHNp19bwxfRIKWyzbrV4E22HbbTnK3m3m1SPuvFdIETYD08IHmtbRLLd+AVE/cZd1XC0RyoueBiuAF5cDOad/53M3jXMOpKyCszyTCwV+6hp3/PEI5Kx4yfN/4tz/NVZuOyzv27kircGyI2wzHIGZBOU8H2TEIv9fItaCblPtsAp4V0m53OkKyiBfug5eBTptwp0bHs74pr9HxaeC1pLmgKmpPv+mvYk9dtbvWPNTwOJsLWG/B+IZ1Uw+lH9hnl6V47ImL+puBI1aF3vsZN/Viri9YpS4ZJe4lbnhK1qJN2y6A+5dx+avLh1VSq64p3o246SWaNTNiFq2HqLZtkEB6AbaJTFMqUIYgZHDQOniZoHtaUYEfyvNXtS1ZJHpszAZTaiPenghSW6uhHLJo0aY9JDamr0EiNNHcSdJVt7BRweNxRqdfKnjqQMblAOcCZrKeyPbHDQ26rKny++TgQmsi7MBpjCxwcqLxCTtk/y+5zL/dRWetkVAIAqOBntDFud03PJpkHdJgij/uUxvLg6Wlw5yd6gb4U30T0UST2mVWFHTpenyzvKAcqPvmuVFYK+ml605U9quVZFqtSkJOvOyLA4cn0i1af8Cs3jjNgbGwy3jXNJh/73HUWUd66JFv6VV+GWhxoe1NGCXhmlq5Uhbz04wCfnqfmKQkHQTZJskcnLIzoeZuhQFqutnhMiy00vKVoB9hwo0OmzG3x+HJg+h5WBzMkVDukRqe91t7fybUEFcTzMYS1pt3GkNkzH2xWc50JJ8videj4JNx2oS41mgi0yJP192ygq8O3Y+iuUUDLp4Sx1LupeTp059OpLT2VsO/wPwj8fzTd72IdL/N+wTu8FN1xiBa7M3xCFgpgjMOnmQH8RT5iqAtrNbzi7itTzDvIA6avrKKzC9uPKzZ5HQxWzWb3fHyJ7tGTuBUBK4FfB9mnml09Fh+DIC0EVVA83+SF0EWjMlPDAbOY/RtTB4X1do5liwzlLe5kJ6o6UwHJTftmQ5kmM8cynBOKDyi+M27jpX3pStyrH+EvaLpzAMhPQjgFcLXCe2GiuuQ4s/xKuta6/IfmS53vITw9tW/UlOlTc9JEekFm0mpZUAI3FW4RkaO5brBNDr9O6+9s2uqJvrXJXdBHiKdiAHlBoLwhtjIS8iarPulSrsp7gxUw2OqrvrWDomu0NmcovTwOdr2+SjqGy05fZVMniNzblqolJZ7IvGBYbNhWzB4RMErTah+7FaTSILKrSlY3BV5ggdkMWNyzAC2Hf9n7MoFfmCRfXuiZsllLj/It5UNzEO3SJuA0CzqdWUWrhjTF95USy66NI5RQ75lWhSpwkLx41i/W2+E8M50+bKiQCKQw1FImr41wb+X+LWU2LASG3UBgos9YBiatEIGcZkqYSyeGrFpc5HCHL8/yNmjVG0FwGGaGfMD1vaUuTeimDw2mAVPc/UYGpWltmO8/9SEQ1t0n+DcwdhzZjFQX/vdmJwu/lguMyInj+KzsyB9kZhkmnffCygBFu32ZE0zjMtTkqSLhoQ5WBH2MsaM+4j30Yu/IfJPa0E4Wl39k/4OIJ0CxKQVmuw8QaTRX5okO6RVCyGd6XApdc0pKEEW47GNBpzZXfLXkJWg98iC0nBJ6qMfuI1eqsUxSNeVkxMPwASVdh32Q0FhcvLicbe85INdPqT1yP3pK1J3k/MbQIA7LJLPyRzEUEztD05Aif11svKSten3KgddPH41MmuaX2hmemj4NzAFA2JfFOauUp+0ZTbwCd19iKrnY5nUPeG3to6dsaFA51YpVlKuSyDrhDfV/9ybi11KZSqb6k2BtFyzFQhvotrMU6RX/Ohsf241OqoM3GHGjE1yzZtJvJyFqf3doRVAgOH3Dwf/6uVVQN+q/xNrL+J8acLeIjeNpRIotNmag11ue7MT9BkVdIT5KxFaROKt7Wl58pHHGz4H8zu0gzzHtPzm51xzSmjOzJC7SmAovjWFS4mqsqnZhDDRjioBwAnuRgZpYr5BFfk2pMcf9HhUyDmuiXhxU9SdZEOuA8zCaDCr9WWCrDVrCKzSgrKO77TJUoClTC3CRfcXGVfiPyc8r7uEwIuvXaqumfkGwAJrt+8iQZ2X1kOAN1Rq4y6snmlLsMJeuWWMkSPo81VQVTSsxf15SvfUDcz1Zda/KHp8RTLSQClYtwPKmzZXuBZ4yvnnYAfwMlDSuScamCDeQKWsm9z2sYMung7Ve12O8RyyHn/XfjdyosaH5trDvznVzBQwi/HbQf8ZjPBcDKJNXJAN9o/gTIrv/TyU73J61xYqSCAMKikYS62wLXVhCiTL8/Y5lR0MgtD/cHAamNh67xbUdIE14FrzjODpnzNzBKLiBwUOxaJLoDSydhKC5oUkx6Il/p0qPAAiqVsTYOk7Hb1N9SVFLX2g5oE7zb0Y506V/xDODgPeTyrieqz8zpIofojydpakJbw+QifOptG5AcTf0nYvFU32fxXbe3TmK4A3s2paPrU8aGFslW3Plh5BSY5G0MeFAWgxU0r6mvqFvOQQqXTruzTtrYnVjs+rw90K+Jx7db/Z/uuy8dvvnkAApri14UmMEhRi9dIZG8P8S85OqYBCcndHvsGVk6DQ5NKlQLGVP4UxQcwylD4suCWTmyqPcs1E1oaKyByxGaYZNAqaTMvHpCMd0NQvzCjrIr/S+dVlZwX/QGECXVMDYgMeI0l29npMGl0iSPt9EEqjvy2ne2Aa6FIr8Bwm8W0SxFjZa2iqrqFDdS00cYuWYZptJF37QH10z3PK00lDTYC/oXWr0ykWS8mAZJUaBcCBPPoC702aWCRdd6VlT4cimfrdbO1hv3xFwPS+R/vEUnN2Xv124VTmd6i2GSw1FGrLzqANv9QXt+/6lkzR05Q7NOozm9ofE66JzTJxVvH9DYXwmgYhxU170FUpJP/trjqNSeO0b4pLrmhJbh3ExcEx9+n1HiHCBX7lwKeaYcZn9Lcl8ti68e7Qc9HahX+FevzYPLzOsX8Dc+k7i4+iJWZZfKw9Ubr2qvWqzsBmm5wUwtEUQM6wIgJmm0mcd55hLMLw52zglAMKIdCWYJJEL4Ztxkyrzdp8uPsTc+verB8E9YWA4eiLV0hlIZpaRepBblP6Z/Q6Gqokkp0nvO4LFS0zKBjUqwGjYjlPRNQipU+FCOc7SuhA4YSOxIWji531vHqIBWTld5Mxonvm0uG3mjZr4u8KBmQCI3j57ujxNzQQZDIQ07g/+PmDR6J4iMqdtamFELjborH9C6pfrODvy32K5dLdLUjZUD92KorYePXbZ9YyMct8EVO9dIFzmwOHKfzOjWUQdJJg9K+4iE2V0Qudf01hBPMXSwyYdk7Nh77pOFP63MLolnj6MTZInZDuS1GiWnXZ7k2lA2+nsmRD3gWbE0yxEi9ksA6QoWt3SCXrEv6NnRA1+tQvgg0hEFLQdPcWdEJdNvBihjodfrC1d0eYg57Hna57YPCrzcGs+gNyuWcaxgK8JphCURGlcNqAm2Z7Y8x8vp2AIy0x+DZ0A8llzVJlB2th7eVaz8r7T//vK6mHbefp4XuTZfHaZnN6C19B4ahBw2+hnAh8epF7dgRmip8Zn9zmCEsDg4Er4qgp8fxrKJXLBRwfyNajFA/ibb+uya21IO9b7RdGyGGQ0Er1ld7nbShYo/qbogUOZzhvUKilZLmvjdMcuauEEIuxzTumSibAHlzw4tuDFSGV1a/1mLqJ8dusjnmVY6shRaPLZH1343XfSl1LBzKgaVCCgLrrb55R/GkudzQO5pMsGvZ7DXJVRGHooPQfR0rNrcrbWnUMcVO9sXceDTA8WRoazzTg4Nq00ifTkL5lf5XWJQV/mwc1+3zi5LEegNZf1B+aparwlx7wCxJMw8EJulaWriY6j5lYvVUdNFCffmMzeJbnTQB9KFLv6BCfHLhzglBB0zs7noaJq0ieO3+7nsVBOIxWcu5JtMJwPXt/016WKs8bbbLUMootB02uwFTsRbwe/dmihHTZwj2SUILEkaqeWyzp7Q/jTb9+9NF6/0VOS4/7vZaOtYNcgTIlJspLeAx1xdnV0BhlkEl5DRI4aNW4KxmryFF38BEbPZU3k/tNGD4saMQPeBQYXigXzbZoG9mn6NcAjvtneA0ZeV0lUlBXVlUYzL0b6zhVwg3dJ41N0tTGlrOIuN1EBfftrmI3uRe9XTqb7Ni0+bZZWZec3t4nd5b1aNdmwLGhVCd+nUDbhw6i9kQOSR8WHVRQu0bC9XKwP5vE6zoLb/g4EM9zVil18QQLlIs+Lc+rHslqmOQJ7trEE6LjzUoCcMTW6jOcNMDw5vIQsqSMKhm5Vc+x6Ca5VY9pUW7bbbZx8Lxjq/ICIp5KPrxIzg1wQu+F26L+pa9kkwCktEjtt6HrhkM3Ips/ahrCp2DmnHxSA+pKYtUejH7SjpWCgQgdFjg61cvyIY+Mf9854Pcn3yxJEE0kItYkbqhfCVJsLU7XbThEgPMBsgDbGETtLBNypRUSYmFCF9d5o67HPAH8IvSWE0JZ7jIVF4r4HfhWj5NTZ5IIaZL+3IAlYl8Y36a2Oo8S+8tdwMal9JBLQIWJqPuCseErfq2sgda4z79B8gnlrBYe3CkOE6Z2RFw6hkXQgiMvRvsdMSZ0cY5TLdbX47cfIofToE16BxJ/1UwheKaerjanENN/cJx1lMNo83iaj/xR+9RD0q4I/JpOrbCYZAgGU+b8m/HngcxuSDAY8iuu56g2XRa+9AxdjY/+poF9g2e2HHXKCRZdcjl8GlDyaQWYdaPY25yOGcA7xlFMh2et4d9yYb4MoDJAzyRh8UeE8nOTr/6hWmvVtG4uajVjq7ll2Mb86xKR0Bc4B3/p4pQwDQSfjC6z5vpXXcl2IuvSMidThyQyGFg7bOU5iUhgVPbxG6X5F4FuBfJ9SvaSQRo8KxZZ69Vadx7G92CThwxtUGiwpnPZ2plUqlBhOIOI++aUQQ+PNVgT8yz1xW485wHhlBQ6TlxkP3AiCnLirbdF6U0q4oyzR7sf5A1gctLBQ0NlB4Ne4Cb3R57uKyFIPv9xPCZnTUVr/B6JUo7uVaO1+aS7C9XtI+qZySNZPBe0Cp1MFQ4mZ0WoSk1clZPBpoNoznt0kuEELVmlEt7EQfcaFJqiigfR7n5w4zRPB3pTFip2eA9vilDALpNRL9h+S34MltWrlZHAgtS80DsfjABsw3uODEWl246pgTthsodKZmKhvGoe4oTSoUJJqmH5ZVYBsOsrDK+FtMyKsWta2dTIz2ZUn1dAnZrGqg8YsPIY941qva+HwtaJ1uZYlL8bIOflTxDi6SwFUtsRg82Y7Y/X1rcX5ECkkX1v2331wcyi6FBwBnF6Ti/EamOkdGfBxEneYnpBUNWObvba8jgtjCqM1INBJ11HNnIUMPUyTMPd87g7gBPKYskLWyYwOjly+11V1VmCX2p+r1JkQGWSUU0YQa+3F5+/L7Hmnai6IOK/KDfA9QdlYLTvWIHJw0ajwjCz9Uri8Gr3Djwmq9ntjrPptmtx5zK3qwa+xvehrb2C1KaxMHFUY5YMzR0ZHmJvyIGZPZ8v674T84fWDt190z/esSyVv5vKONWPlURKjc/CrD4urCKsRRsy2S691CXCA5BJDImUQj9zjVFg37YiyFarlq6jdbZkNFUR6zv63C3yYpUAWeo7YCk7cEXK4W5TbkHAC4szG918VTu2hpy/MVUFff+zPwxFGvf+ECaobBWhM/C0V7UHBVDZUaiPThYqGhV7u7HsMXtDl6JdP+6O8oyy6QWvSoVE6lpm8pd+EVjTW0bNUIrg6WweeZSqoSj2R6uaCs9Z/GzskM9uPXyiGatJCA5WiFjhgMHfM8pqKpHwdxcqJ8OLunVR7FlBV9NCCLML5KzNArsNGvGE6+CC8pjoFbC+aOzzI66GBHbMsSWVHcEsliFMj3fi91/RJ38DZwbPpqB8d90qqXXy5HLO5kx9AIVxVDmUMwPeUZeyo2+4yNMdOYmj64+S8EcUDMmXsQOV4eL7yW0E550nis9d+2dLWnx6/o+SWSK8qgypbWlP6gnlqrEPQFBcQOud80+BFM3zNz9qn88n9wPg0VhkZjvt0PKkKIDbs26yDm9hRC981tPia+5xlJFZVAIX35ATT88GVRp4k6fZH2nZDlhuvVx/YqcNJ4VcrcfuyHQlsEtyl3WU28B2KcoYX9adPlCxYYU92kQTcWHZ3RcsgnKAJen2UweIC/Dnx4zz4QJ2fH6bMJE32OaPOmACUNmaB7QuSRW5haGEXznuSc7ICSTtWp4VPmPctpIfCTqdKxwwFKiOteFULAdTzvs9ifotDPI0wxuKvKs7IFZz4MKpz37YN6/7euCqZXKhqh6JmIt+zgS9BH6WPi4urrGKFOQ3JGzrsEXaV1SMuHZDlCwiYsfrmFbgBK9vlSa7qf27dMwBKYpckpWYnqen/bQlmFbHFXFN6pX0GSoMm4+xEiEYEMmCZ/rfl95HANYsFB1ETDnHjD8LcIluPvsepyUjX4jKEMYnnkg5Fuqz/HOQe5CBOPDruATJIc5rHYmsrsIqvlLyqaiizOSlnVzYXT0Mn/4GktVszCet9oysaIxIcp/jQNAR5EU5Qyxh4PVrnX61BODzUdsO2juEPgTl7tjIdgS9tvrUWr/8V/zGyEQWa6731U0vJUsimSm+uWu/JjFswzLt5X3HO71vkklDmE2BK3HfPzI72GijXYo7cF8ykCQ21lGkpZzkf+VV8M4IFTUugEnmWa4WHoe+KPMoou9M+fTwrtb+htKtLjnK3fsbGnyHCoN72bP64O0/zRYxQdhNcgu4avth6LBiTMyEBDQxLTXttC3Dy4vUgW0jA0JlBO3YvqkysaZ6X7u7zohz8HEm3f5sTQNKNjgvu80DwSXHonhUPJh3HGOAke2v5nKXOQdMRz7LFTw++ijxWxhrN5RQ3oPz9DhOqQ3h9XJybZ+IxXkffjvCVlGh7C4Djfp31NPomcSNWJv3+/4AQfP4cDiRjHmux6M+Ef5INSWS46S5k1yDAw0euSb9O2ZwgzQzB1HisNipsL3HggTxpeN6oGw1Oye3Zg+W1cdankdM+iNcg8TKMhIT/okVC0n+11TJ1Qs8l5i4jIUxDFBe+vJYpy+GHm+ttezUeDDojnw+24eADMCTb46S+vLMZ4NRQqn9wP3E2ygps3TAO8LtVIlx3+/oHrjqnTH+bN85/aWww3qIAUEUzWsMXNjCWHOg9SRu6ZCvoblp+Hldpo8SW3DZSdqQa3MulIXrN/snSGrB3/1sZ6CEHYfibdN4/4s5LA/jCX+Mih34h7Lum5/V8Emvqo3apz5at1Ln82DPOnO2I8NtrmwtHzf9mf2lCX+nx5zXRGJ5iLMinneC6/SeoHvoh4vvFihvyV2uZ55Ju+rmRnRNTYz07P8g7NidGinwYATXFc8Q/LuYKz7u1sGY74uPzo6hREymNHKh4rtZHQqh7h6lNO00pNZCMwP2rGxnscX/Jk/anctPU+NOV02KkmuZ4T6qMARwiK+BywAJbGM3T7kI/OcLVyr1+BM6zQoxICPFG5QQpuBoHHE2JInv2GySZk4ZOWJykQCPb6gn4oqc5u1ANha4R2XOrXVJ7R46DO55gloppcz3c+ydaB9RBEola5fDMMZxxKptX6K8ctnF1+MHKkev28nekGs++6Xa57gj1mdGoTnXt4juUEvjLF+bA0U5F5+jKuwY9bXS7Ky3GcJTlKpGEJKL+PlenfmiBPiyffQ7buAMYGtrN6WqwBvx2m8myH9F4CXI8hkcULwjTe1Eq+dk1WUk6G/X9MYdhAAR9G0mSaQWaO0onFuCr60NvA8/scmij0ngvNBmnvMPOhjDn8LyexXzfUF8frDyGjne/R7njMjhAzDg1B2DEu3PE9ZhCAR00fpcxvGrBi0AXypyNJyJf0bHufD5I7tXCEJO/ySYBIPZinrk+/CdWzb2nJ+XAeXQHAvvrda++Wu6Y+V/fd5xDiFrUDxNZ6QLRK9J29suLHDOce2U/NpHUt35F87Z0NEiKBQ5VaXQVYBcAHcuQMNIHHfUNc9KA7NoO2Vdffg3P0uhuQQfeYAukurA10DWhwp0oaBD15zvclKifyTL1A/Qrnk2xVodIQ5KNpW/eSBTpdwrP3cq9su5spTYhqji+VXJnmjZrcsj7bJLfvbFdaQGoq6Copxo3rrzW7uvmtvIMRupn/KFTGZLKUeveTUCgPMPy32nOdGCJEMKSN0PjfAmUAD2AC9CukjLBOAObF0bXXe07YFI7Juc7XK0bb2lWE/nQGLO3U/u+KQGR80azmEGSwk3PeAAz/Xc93CJc+m6FhJzAGzLrI6YbgYZyaD3E3sH4NWjhScCwXQFQxFySq6UP4cokucux3GHz1jDUFcGX/l1TFVOh5X/CQAooEShekLr2E/R30hcjKiy2QEAJ1faKISCFhvjEVQ8rhE2UTHH3fD9H1qVfBtXqKSpxb0eb+4wPBD7wkJUce0JYcj4FyxpMbVjY6VeQilsBuR2p1qyJHoMTTYM0P+xGQlIN+RiRGn2SkZMTCr6l0Mf+fMLFbC0h4LOuYyFL9KduO27Lnogi6xp5CKOl2GS5gE42aBUoPECmLEgj5/GSq8UyI6/4veM8ga3FXniGT03ynz8SI+YBoKzAM+guuCKg2tRDvQBQ3aQaeZ8MpHnJ0Dmw+hpMtLMDyFhI8/pkjYoBYtf7bATluWehBr3dHzkuol7hcpPzxsSJkPS3imRjh8xIJTdIeaTzWw8O+AW8gW4kDvvUWS0yvKD4lsUuV7Y5P07DyDrJIwUO9Onf8hZxJkavq3oB1imjeil8qrE3U+g6jU6iFpwFe+B/MHzJkMA/QXZpp+YN4W7x06AFcdhxFlKvmQZ8lbe8DCV0m4Gu6m5YTcpzgrCkCTF3T1ilsZijTX8xnn3v/8glVh75zWY/8ll0osKbKClKXE7iKrl49ZGLLk90g6PpD14y9PYVddcM7urCEn4yEutyxbeRnw4NWnOJ/VJe1SaD9RwWZViMmrtEeXvN/GtX5HAfk1K7fNYPIxkSOrkG9+hSU54xqJ98FHz1lnOZln52FkY2u3sqBjMyi3KYXqaqWxcSmzapi2MY05RklrpNYNzw+Yx0YmbEvhDoLwxE5TTzY6QCuM0fIRx+L+c+lmg0PLjzxRw73PQIhTNXbUGUd9Q1wwgVAAMEkOMbWAe/rsefDYGiyWsr9G0qPT2bTERaubOlh4hQ6ZTTkd2D2jiyzvBNZKctBXvNFTqyot54RQ7TFLn1i9oxyolUhCyEwLLVWL9uW2gt7TAhWaYRlzv2jl3HHzRvmxZ91ap+DuhdrAnLj2PQMiw8GGguNGcDJK0ow3OEmtHQhmJcjY6JU2TCSHcZcYMfKdLn/T/KvEVQtR0tHF4sngVP15GjcJV7fNqMcBKF3ImPJJDefKNBFLPPh7eB1LjUfQDDQEWUrmd19Taqle2NH8B9Q/LyPxMxwaASHc5xAc5ZJDOX9raLFL//i2C4I0O4+/szxahpoBjuPN9iX71pgGzgjft2SEKtmQ1l909B9jgr1XlX2GBPcxQbqxXkjbXrMsmSW1UOkhHjv2ducDn/MaUYAPju/tHs8TqJiNIcRPJyAgRM1YvAQNSQlqv+5cEUgnzrISZQjOY+lwEXd0ndTzAdAzZdNQqMkYz0Uka5xa/3ukpOVE9DyYVhEfWswDru5xM87IXsAk24hJqz53+qL9dcF0fdq9GtN/gaHDqQ/zVJ14FayhAVHfjtErufr9treCof61XqvYMwkqquOwyzbd/SUtLStfNCT06wfO85SMkVA8+OWF6IkICrjUGbrmJLJsYm4JtjxYC7HjbJUUMSCZjOhrIwKXeenoCI68dRxwqrfTiVfFx2GsSM2piK6bBiAiaIqyTUOdaNN3Q/cN1Hjsrhyl5YRpYCFTg2EM56vTks3bTxe8N2ymgqZ4pFU2mnoZkSTf9sjhkDUM+5Omgld4A8aYoybTnpd2Ziyo2DesLOgJoy2lgsvFv0vE5i3apcDFUCu1Mme7k9uaoKvVfvJPn8TstmDzop8rLnd4hjGTWirG8D2axvb9BhdLSsmitI38D0oaFl731238og+NIsYfOSfc1vAsApOlVYqz2ldB/RK67fSTEiPmTHarO6sJ398AC8Ky62td3E6DD3CiPF81PggQ9QZcHkQuESRJKTw6b0fKjYpKhLJhy+8gAARS9krxDI97PaelTc9d83DRITDWQRVobk7i3oYQNuH50FtcjAHleitefpSY3q4F7ZyNoBUcqhsZBGyYsBLdqqEV/IOOaJ9rXmX1od31Ndhgi34No5PMrzuGKKRlHHrBT+lDazVc10/4a6VJdUNc9eMkHUxYfbzUE1rCgv9LzFL2hEGf8hEk0dkqYeaS8zv8jhVFtVfaVCU9JoVBTnR/e1DkLPBB5kQg2vKJMBwcJLgbf/OefJl1LbzxEjxrU0BJ3B7wXU9KH7EfdzkYeFr9sApLx77qbSvEWmoWQlJSOti0egba7fSv7Z/Usz1rmVXzlwLzI/Vh6+LBj8ivm+NgqQ+n+TWdXcPMHhqFBrJTLPdAQs+OZrnvLAdPTQEeTg0kYEHPThTH18GEieI7aOW3HDbTYBYfycYcFWpRfMGLNvgBPGANeSfPke6CJGGFTAjBy+Tm0K7L8t0OtBbwkk/nG6TLdKxVbt5LW8JeXDeySkV7t5CoVo6NxPUikTDGVqCUQRzzoj3PUvxNa06LVS9K3NtLaIrIUZyKxVjW+DAY8KGYfRXeBhw54n7OuZDE7UT88caNS8/leppZnJsvUWIIXVhagIoQn4fWZJF9XEf5BmXi17uOMMKBgDJ/stDiAciocZsGZoNSC1eAfarrolQFCHcCumtt/OG0M8sl1BRMMEC2IMweVPOd6B+LaAYjofTKekws14/kKEybxyoMjgwiRHr0wg0kx9l5u+hEp5dgvnycV0cBuJTakBjEmZWvCBzgVXwWe8weMP4YewM6DSB+78MnZwW2ChjP/1IEUd2ZWXO6sMhPnX2ggISlyXtQMxM2tb3WVUEYBxmixU8WSLXuG0CJAR9UH56NqjcL5c+O1Lpos2VSzdExi0uXSb8hydN5s5/SO5TpeUWz0hfDgJkXc/IqK9YHQOCOjhFq+O/h5z35jsR652Z9hIuBVk1WtIJK9HuMulzIPMzyC1bIsJcXbqdh2wjMpL7w//lUmN1BQB2QpMIRN60NbZa0OFJlO/HGrCKD82ruyqfoB37lOAWLE31wxNU9WhK7wzKbUFP18nhiLXicUN2UG6MnuzCHFsbrEhrgK7Q4fnHwBuefA7HNQeRRUtx3JMI05q4jxYVFe5isFVIA6rwcj/qPHPDwLjB12ZZiUuB5Cqymk3TAFaNZrn/kioRrwGo5x0eKU5SohK70ud5EP8xAz1OXym6K8Efhl8J1imiot3jseXvmt07FGJzBcWfhTNYvWqN3ieyyQI4vh2N/fZBeaudPYD7JBee/dkjTujQjQLGUEX9PfIXRTY64ztt8FDaqg+M6CbXJRRasbfyL7QhOh2zdVDcCsRR8qbfipIqLNHTdDjhwkshr0ScjZOcDMyKQytYhHnxXb9r++yrZ5QcISDcTwZ2jAUSVzquQGj+Kcy7A2uTNQPkfydx728LnCuyfNHgn2xWk9iUB4CY22R9nfRQy5Mbflfs2zlxVgSxDH8kmFAY2KpTh3YlzCQHM0lh67AJQ3VZ5IXOlEEADP633MuKIJhlHG0M4cD9+FS0mHtvXSWH4BpbIPsjx5M2oORf1u3C4yk3rR+fVgJoloxCE228E614ZBYtEepqyFw4idY0ZS865U6WKsQKONxnUh7wVOeOk0fM6Fiwi3e8qnymjz4jBiPwx6l1MCTzLkoT5gAVSvuEiAMxGz4RX6FdtPd/+8eQ6RLh1YDsxSPbb1VmaSufGjlAS4t2dCn75J2H1CjDnxGZcb+c4vUJXfTM1nL/ZI1OlStOgifmo4B6cYrx5MtiMbRes7xgyvKyQO36DFoQwFRzFnK3G4ixxZX8/wfgXUIkFomPyV6erFMBkFMFdVripZstY7iyLKwxCzfus77FBmnim72pYAR/wyJ33K9O4FHLg7TkwTSYqAhB5k5+ljIXz5cS40S47JUO9fuhsc8ektx2B7jZwRKIy/nl6gcEtSvww5klsrbghLLfTuGZdm4J4yAsws43eZnQEAGB3iaLoUJYC1rbuUfPXnFYMfRqUs9K2nCSdZkPCVoFN9a7XNCH+dg46f1JQ8UObrGplWxjHy3ev28mvvbAUxyOoLG6tXzUzXhlC9+VfMTQ6XarxAIbs07bZ36IYbVNIi/VdVuyp9KjQ+O2WjWs1/b4GsQo0UTbSHZQ+Tbrfy+iimcICveMOINavisZ08IickMY28lqGimnGknY27MJyyjv5t6kxNcMklsKFgr16L/weQe58IwnR0bOO6KV2nu8woaf1LUUe+OEVotjC8MS8quv0rrcOxCMon1mx8yFNWXg7tgdmd1fS//n7MRfaiLHeHPxcLEiXSTZdDMPmwaXFbhfrsPxacusM7k3uEJaajyI976br9x+3ljLNvYXDkZcD5F5d1281i6Y6BL0PtJvJ4lZRU8GfY3doagXAnMk51LNkyvbgy5MAhSEOqZYgvlhJeCjChBaqTZZXW1TBYKCQVBFNXBYvpI9wocsBKAMcD+JsLUS4FdAeeRJP5SQOfir4PSsb75fJMwCcc8iDU/EddSf0syo35I2bCd+3gaECmmnoJhcXVrsUv3XQMEe99/oZofhHWQhbf4zP9d95Rv6w4L89u5pouPfLR7nUQji8ZsBabMbjDcP3qLx4VuzFuOnvbWA0/EuVq5EZvgtQXhKy2HBmUTV2jxwqGcw+VwcdV10z6PSPH7cnuPaeLcY9gyyu/hhc9yQr3g3OL9ntrxkbJe6y+8VDc0oNPJOSKIKh94CLB0e9jAD3U+YAzzOPMpiWcWu6jJh1/RzgbScU5ujB7a6t7/l9QPoyrMAbdSGrBUOPf/LEx6kbnhx1WK3QJGwyCYmVFqEytz7PkgauppfGxMAI2XyjkpDj3RqSW9+V8YCPsnfa1xsvtqio4/DrEZhvJLfVfs6eILmlHU5AYVNobaEg2Zpxl7xVIxEBlz4CBzEW9yCE3Plnff67iSJ+sovb+7f3v6af+Vz3GbLJUb1D0RxaCY2U8eh3ork+gJoZ0kb0fUthQ110In3mrkblVssu7u6r09MZpOnAuzRgMiysDKgDnzAA7rpGpk+vc/Jk9k3Y8p3cUIC/KVGT8WSFBVsXs77FbtTb6XPB0fKn1oh3G5/y4p2IkDPZn3RpkD5PFa6WDOmURrzyUX3aN/+gWPKnyNyAk2cJI4eBPUKoZE28j5+fzJG8l4Nnw7oLfk+GGsVeqXa8/6McKGywDPGvodgS4WvSiFm1MCK8nk3JbkYirPGLoWykD7fftEDCOhuGhsXRch59QkAYGoEwa9YQLPYJch5taZNDzbZDet/gltY0HrMqZzcnj+WPHjRyHl7ZFJTGWLtaEG8xnx7Pi8ippi/h5bdVYDL3m+fR6Noad13yQUWwW34rch+0bPiNloXerkleYwNIdG8HY/iAQrLn9LtMVeappS0WOVr5O9yZyMUDv/BnR96+o8y/pNHvXzYydMP0LiRvK9Bok1HZvFEortwgF23+O2B9Tx5ExKO4fkhyT3NmFrQuNvwoujSyBvp9h6zFePXjhbEgtwDsNSMnEzoyuNGj+ncGnpu69MoZK+oKN4VlSrYU018S8E42s2pzkQYKTg74v3UjUy1OBbFA/kVCJ40Aaugh41zLLLmVu/M+O8DqEGgw7lK+163knsrqnaRTapIO8MLIUlOm1BmDxfoFCJQ8JLbYeTJgVYM+aIeWJn1x5Ysc3mSDhgWjlV3H2U78hqxnVpRJagwyF5HJxZ+lTJS7rK1+s/VcYXMq+ZhZelAkNzxUlEmO46Z2H+dP/ZP4iWXCxhaMswkjYfD2e7CMWjAHbQZ8QgliEJ5+PFOLzPeiUsNjoENoS4hEs5YkrqdVdwuSszEMPxhqeJQjAJofPYH9xZ2zEjGAzIVaB8DLbhfUO6I+4dWDW6zwBXxXfviUUFseR4DMl4NmsdPuL9FLpwJVhyr3m1TfBgnzodPZ63ejSP4nGC8Vm5Bngg7l/RNAWGTfFa+TL7NMYo5QBfZ2GlgQt76ubkli+g9kuUGjy+JuIph1spNLBlqaDWWh+8Mm5zhpoI97llG+qL1usCOaxrDJl5n1d6FwiQR5b5HnVbODqjBx5BOSvlWFR4dUbyoe7j7wtZtHTkAvd7IC7J7caE31eyOPS7zM6NjFF2QtApd/7ytsWx0xqDrLBZh7Ox15gLfNQ9vut0h2icNcDB4nUUfJdUFQGb0dHwgkY+CM+kRuXF5rAjwg6z9B/4erOObDHUd84H2E87Lh0TN06cFnc4fK2BA7KTenfGcxko4HJMBaWOd7x8nFj1/+HpvDIjStcEdhNy63v5l+kW/m/QULZvOcKR84g9Q5Go7CBWRnQC1OScqzWksI9JuQgrwiyL84HBVyen8y0gPRS7KE50Ovgt2NH2FGbgAeB1k9R0Gtp7f0r4EcMbeqsxLFR0YhZkTHFLVUi5d9KX52fWekVcEYWy1yIDcSHU8TjoeJPSICDmOXICt/jN4Q/qPx3QJAu09xK2vWUKeh0hq5EPOruBkjQpahRSqpQ8Gm95x+MxTHRJty7F/xvGB1AkvLikdty65Qr+VuVGbH07RBgCwbgNXL748KIjXaNMcmxM/JPslI6FuotkVGF+JvbmHpXFtn5/cbJKV8aOHDAeWlteOArJf4xnGUlz2gSMz/2u7cdDEWhfM4CEyq29c8tuvfu4iIzv+sk4x+bYerAEEK+7r/E4UEsvyw3zLQw6cdZ8jubMkJnrFoCLhVv4Sj1Tp5fe73w6B5x5luGBQ6b98eoY5lNaZMnvgyAZ6O3rxvtNAM4he7toOO+Ys9Lk8rRc0ffjvT4zjdUJwuKk+HZP2sM6BZkpL30X7gw3G3x7ZDH+MOjQa0MqBK8RTfBXuEGwhPcgTtRDdAh7SiMkbcTSRfiiiRhJfuQAHIC7bwP+e+OzQSF4UmUxV3bocGMOvTOZS6T7EzhzJvZeR3Yw3YIDt+fInsTuVJbzGdrO6XbKsyLo/jUWsrvCoMwrseky/PPDvKcpbR65WD/eqopNCDlTOVGBNizoLtI1QJIH+OgO8Q5RMG8FPdHr0ezf8AKvYQK/gp7dpvEGSvV/9qWPIbFKwWJZ4hnxtdlLVKvByUhJMlWFlY2dzXl6Mvg1BYgzWsWuUOp8MQFuZw1wuF0rxu8w6aTaRNHScoutz57ayLg2v7yfBGaXxpQ46HY8HS4x2R6gbyIhUQ7+mcqNSsHu0ydeyUauga6/7wjG4cYKSxOzUKnDSWWLISAvEwHOHaPbB/QXl6cTSwytBT1xw4pvdB9kp2SA4XDiX11ifla0jMLSCurJEKlC/2kcjLoftHEnHwOXQEiMZDvu5k/oHwJXWb0zr6Z99xMyniYZCvu89lNfFP4QIAGa4aNyt3iHXOomZfwM63+YJKnZ68MLuXW2qerNDHUJl75tcE9COaOjmzg3hQS0ABBwU9pSDE6LkjIb5MGle7qdGjOqmNpUZ8OIvkUfBwAeZ/J30KicWutXdQ6gGNiwt3HQC/Ymj0kz4OqreD8apfTurvHcXGvkaFNOMafwyhrgGN98+oK7lfMwYGPp8KrxS/ypPwhGS3jzQZkvSKz2dq+em8uO90iXblcz0psqHm4TduIZEz6LBnZ60T3ENPNqBXXL8Jps2bFgr+xgVvjOvaya5lVJZ1twzsbTTOWIiWTY03kDshWTZH+CmMdGJTibGuXmp5HDKcQqbIoAl++AXDh0hYy7A8Kr1KNVpYBQ1YbJnpsTeLFjb48f68aXwT/VDxZZig9shiuLlEccWEm70MTtgIYma/3QzkI12oKpu7FbLevoHuUlA9egmf1OAUG9Yrj4LFMWV+RcnycD9dUh8QifxXqwnqr5vNXYB5L1GXQ/KtgI2sL5lz88bONTBDwmFzZen/6R4leaRXPcrJ4ZnwGKYjjmu4GjGVFBlRkAzRftGsew0+E8DBmR10X+neyhZUukaN7ObOai6HkUFxLWDrnxeYFopLOTEiBMSDgSS8NmOio1B9OVHriiZZt8dgi8AadSF/a8x19u16Dsf3SfCGyZnrIO7RNAzfrL7SYW0O5hypJcsZkrRA/OgHsUdlzfM+pALHlRgnZCotvBgaf1KeXMLyesJyvKs6SnYKsZB/bFL2msAhqXx1lJmvW8aOsREFAGHQ4b0tQUYaL2Y3lncY8WQ7i8ZIq5RPW4RbqzZOMrrUAMBM9vSW3/dwJ6w3QmCHGxgj4PGzRGWI/KEM95lXEingaQNvmAFnPMQSCjEQKZ9BuXkKfUX9kB4Mod2yKT8Wn2qa+ec2hLzp9YZcSwCtTEXqoNKOmOXiUOfv96tVsBF6JUf/sHO2zEFIbDuObVWlSPU7bpnuWpfI7U59V+REZdTVobmwFfhjubnikocYBWjTJ9sqF+lkew1Je3kk1DR1R3i1aNXOntUFehpNkYkJ9W7QpDqyWG9JlGcBliyfbm44vRUMcsMzts5faowas4Q2FIGk21+Hv8wwypnSj8sIRkDB8nyHSINcyxsUGOLs3egHXnESAaxVMjCKLw/sk+kW7GzgcBB3lB4SYeN+ufsTJYldNW/zuQlscGO/3lLgFygESXT4bxEqknVGA4Uqh40dVZNQ5MxL1a6dag0LBSyh0SrnKukGcMGgGjB1ikeh5ENzb8FvItcJAV4DW8RuHn8Etn0LmtlaFtDj/os4EOOi4y9Xl9mG8mixsXvX5yKDQ2+BW9ZRK4ydhS/Y6dQ/taSl+r3p1xIAMB+ppUAN5CwM4W4AKBeHHEarnfRPpVgZ0LoEu4ToCNoI4fvas3/O934rNoahhz869drCdo2TFudRyu74vmIspO69ApCpPsRAInDTIQXw+GdXTY4fnDDmrHLwBfKOFKho5RwPF8oNIjUVjnygMDli6C+7NBzREnzQ3VvbF8KwMFJTKYA7++apfXHUHyz2OSf2CZCBsoMy8bFGYNjJnF8omSOIa0uz4YtQ0EVOYHfRQa/jfl9a5h6T9XHa4nuT7CvHU4aKmjyMilu/Vct/ZP7pzzL1tvn0rsoiPQPsHVK6rTH+TopPASmnOP31C6SmB+rDRi/IOR0soVunyuDIE7dp28AacQ4rxWLV8dI7RDeTTPRdwP93TgVfRkzfpAxwZdQlkysH1hADQJA54+vuK7RKVuNdZAArOgyUSpNlN70E5AQ4O+IxGeBBpkDFyuaouHfUB12yCBCk6RCg84p8JdNymKPPbRLk3rqWQrqQiKwqJH9UiOQkf1m9/XThOPqQYSoCvTh/CnLSaV3BpyMJK+DyV23/AUC6YGlEYUoZgKpfpVmRu6pchI2JNFZBzH3OLIYhQRk5tNgl32TM76+gCuDbp5WShv/Co6it/TicD14jRF6pxdiL4+C1cM7ld5F1hYyKfxClxQsyQjwepa0YaDkW2LYLJy8oNS+/29U7MhrYlRs+sDQIluolHQEmdp5pyA9p6l3P89zVaahy90GgxjUE9LP32H4GtLkYxCE5gD6J33Kgf9ZCLRN7SP8ea9WniGpQI7R22xFh4oUcYTJOiBY57M9ZNRZkREJtitwJqEUR2jq7oTmIfYKJp94Qc9082CnUNcEA8Am9uMvBcCz6znvc1y66t19pqx4HnjepnLcn6WLw/rm8v/Wp955sR6UbXqnZygKa2P7Rmt1nqJkfI2PQ3oEmbbvxC6uXjUOZESgU9ycJn1/c+sObQYaBCrgi1QobDXVemCIq9aqHCXcF3hEhXL/K4i++ZUy2uxR3VJgXmw9NJ0dCSuY9YSLMb7cvDXJclK9N5EFN+IBYT4sxETrFLQqay1vhxgZBOPAABy5dJiKu/en/LBswWmI3qTGvp83kTwJAYr48jP5dIfMIbG8IY19BJRCozCCwep/cB1a1iY79LwyztvC3usCtjRk7RPOFVpYo7fAHwrIupv7gZuSHwauA3ciu6q4KedQmpufZCOoFEFZXuXzJqs13tZ2ktsE43XQ2VTVDmPdtaLEah6OTn/sxqmHpbKTPcS6YDDvlix4r6JXsYQCKjNbAb1N0wzdFNiBHXowBoMAo4c9rRvxjyVuLkJqkh37A5DmW6fcOFKaB9yFysp0FrG1yfgsQuliafjKyjxYDmTt/QjYpMNi2UjBIs/meevATqQInBiTv19X7sy7G6e3Th9ppsGTtTVEX02Jsj9dHZRGKbNTvYFT4vqjvEYMneQXcpFn9vl3T+1jExBOc+GPgi1Ws1lMpxO+q/DHxqiOOV8RmSjcWPlonp5TmdGKYQ9x4zUyLGza75bw5z7+YFK9BBB3a4413P30m1RKWq2X5fBZ6+ENEQ1dPe63MsPGWxta+hr1kWb1pGWHDcIkRk4tPNQ6fv/nOn8X6q6sReGk7I11SdN0KVO4B1Y2ySquQSqNiImV1MaT6HksFw396lhjEeSYqOFwDL34aRgdMHpsuhK5RLBzFspFrt6aFKUkUCK2AxP6l18AbEv3xOt7s+0jvFuQZEkR48gfiWnOTUL3YqjegFaXWE87d0FtB5S7G91cIC6xhb7WCNkBYWkPOHQ3YmDpwpFZiIl/bGReLOs0lgcNsbqUsgnx9krucRi8RZ3zbWQTfcxHh8dtEsK7yVmjBupjScKzGILN/ZGsf260Zsn6LxeGevr/AK9MIt8lmgiButAnvPeOdknZOIuLEakCsEfXwz8F1gnmNHI1aLjMzKpy1r/cPJEhA+CbAiS0vuRj2fl+2mA7EG1ZbekCntPAocD59sOJp5S+3Yw9Jcsv1Xfiyquaxz+5xTZEQTqRatR1P1WWvI2vaKhMoTU/MvMIX/TMpmA0CZKN2H57+uRmr3yjfyjj2u+akgkMiSUuqyS3zuGQvDM6xBBKQqn1yY9kSUPSk8VrcSFVesd7hrifweqSZunrFJDIA6iW2VmjUAa0JR5AT6x43zfLq8a6/s94UD3VrU9D1IJBJ28qv1y10iwTaHhqTtKNvjm0uojlD0/yF1dFcSRXfLhYFk8a06G4VX7ji4FErlmDxT/RJZDGPCyi9Lj53/bCMp3EPCMYGN5o8nnRh/ma3BAYaaKc+1bzvPDzcpiOWfDlOC29ciYRAxbdo7i3qnDNEFPhOfgZKbiPYKijsuHx1UYakHUvNNV+yMmwCyXaN6MzokEWdB7XhQm+/pi2rPAcQv0xiFQzaZxl+Uy0VEYHznic9YVXsWGA+STcaB9Zs+tvxK2s5NldVxzy0S/Jj1uMpd25uPcqVHWzBRiPUoYBE42FLLVvO/idT3l60gWgKRC6GIHr4bCABXLs7cEP0zt6xqCo5gJZaFJJ+D4/RCxsvejnDQdMzD0yA/OyvlQ06I7m50R1MzVzlHwJpkubhqvi6cc9q9WJUNy7a9VaFZXHJhWusjP4tKnzRaub5H/7987pkv/gy2pvvOwD1hupJ5PiyFD8zdgxCopyVlRgfmu7cBlVOHnITZfIpxmyhf2qVt0WRGSVLSei8J3cM5NbXls5z25bvhcAtBcFUAEZliqo/l5T3GJRGBcHqA5sh69djaaMDj/cgckJT+YU3MFAWoiXtYcJVGfOnHVnC2aQYlFJxAlvRtG84qdZ4rolyW2Oo5Nyj4b5p/V9I6wigXYyU+eNFr8ufXZraPHMfk2HzVym/SOy+PRZzgiCtOEhIk79uPQAkksNdVBHLjXxK0rkroT0H+TFES+ubI75svKXAIsWwSQ8JNrf9B6qTdQy93hvRs8nU7xzfdsiRBYYZ3sCCq2m4JMwl3r4EpIJxnYfKKntSANt3WdKpyeei2w+f4Rc5LTmTJWXZ2VvPzsOjxtf/Nw3DCCeSkfIfp1gFhoqggw0KIDC6yoe4YCaP3SOBLNmrV5qt0Gayp0eiaCrrn6JmdAyi8GgmEA29XXzVXigENTs4pwO3LFAHyaPXGgRIr8mKXYnZNbcrOBFKAiCmvqBrTdfodEMpahezAC5LARpJqi7ZSKxG7sMhUqhHh+LqQ0QGdfTd1t0Wl6giqb5WJphOMDlvUirbS+aT74SfkQlyVe2lw+mXo/Uc5d64+TG6JFX8aBx2BbpBAzQz4f5/q0YTc6A0terRZ7Ap60ENUktqNHt8EAR5j+Bh3NUb8n66467jXymAPTOgNWyE91v8y9y1AbLKfSamHM6m3orUpQl7BZ4dZbVjd3h+Dg6R65o3djmTpzZ9gQn4g99IBVT9qacOWdPoWKenByCOS4h4qRBZrxnEDjegw5cB+6EYcpMCdcbGX3kUnl8F4jqgtyZdthxls3Yx4sZPDHrhej7/Khe2rQ8XXGMEGJHdlyzApOGhUYEy0AEi4PFIB1064gyvWLgh1m7uCFiZzLWh2nJopFGePqziK+XZSRUvS729U7293CF7WfZqR8aP6a64+2u7Ycv7gSDCK2xJrHaYYnD+qniSyt+wnBVRkDIq/NQNRQkzVFvup4pILQHrSBE8UjeGHIZBkiL2v50laGHykfc8F4+nzVHj+QtygI8OJXewirn2ld5rQYQ5dheCJBd+ZNe0I3x65PVxz7dj78MQE08XofumttXMHK049nz2m9OgocYpBqrLxy2Y+F71UIc3MrMkv2woGYxI8ku3mQl5Snafbq0jw3BJn9a92ewboLXzruw6WLtDMAOFw2K9p2IuV40Km0jnxebm7C8ErFkorGU4AFXmL9/CfhQGJVzXA/PNLhMVpF3K6WpSrVLsgjXGlDV/GFN8DtjuXH1NZbkBavRaacCS4aiYjfSMA/IMi4eWJSFHUhMg2dNTWEMXg0x2IjbkGuFux6RianQELUgzzLrk+cn8w84OdGxavyUZawkNHkRmQ/uXdDipOOZ7xZP/J1LWlfBQpYyRDx8NjRVPgC02HyeWon3FKEC/JhIZ1RsPmmJlOiYTOn5+8txyuASUzXEX8kjSqHP76Fvwhh3D3Kfa+JV1PsIcP2nPB3tQ7cPQx6eFs3lavufQnKF8HbIFqcoPyaXfADRbmBD0Mgj5fXyCLumkf9QGpqT4b5Nwd7OabCOd4H8tlhWlqI7Gvv47SeKv/9dzopQFqx77afWqzUGBIv+yWTahGFebmLfYVpFdxnHVZLo1+eoS9hVeI5JEWi7ophtnAhYDWcHnoWBfv1SL3hfo1sApxZsXau2ybzMal+wYlx6HZGbcE3dgvC28tKmCnxcn7PjsDlWVY5hefCmydmvR2zMH0luZdKKEHZ8kTqhrdF4urybdlA1jN4HZ2FRn79KzZvCLAfJVFtfMwVq7JSuZlPZRv7n22O8X5ADM0ldIuLeQGDHTRMvxUR8Kq+n11HUNLIpb60skkR/Z6F38JvLSLYEWdqOwx3DP3kfgjyKaVIQupDMDEaNHK3o+MEW1WiRGvY8+ehRIKqgOCr1hsYyzUBQkC6Cdr5YDpZJTiXofaRyHwMFO1PppAnD3dlfF0qattxVK8GQwLBv86QfW3UNkDCx4LlL7dx1RXkrHNcMERKdHQMG2FX3bU12R5OIyllnY2nQXsPl/VQUEyaSQOYf8NZYs8F5ie6T2k/pUHQvZdEQR8zbKHedkYCrCQXWmnIdatbl4Ztq6Jr4erhBG/qsdFbhisKfW19yoCkdUEz3xCFiWoFBlbRiblhmiEzYagneqgEpxxtWXx5G+7Vp6hI3YBNnuNq8DlnfdXMMdJzngNLEMN0szE1tsqfdDuYYbbSz3oLQdYl8Q8KOq1tOyCmIF6HibGCU997z5lXou098UGpvbghi0+1JyLJnJdOyh3rM+r8Z1brz6zhfSi7zYt5Ug4iFM6dGFeWKKDBMt33aoYYgveEbWe3XPRHB0plIzTHTFZ9w2/gj1GbC3LNgNlc3iYEJozCNV5VGSKsYkAHT47rMOlfM/I8HNLQyxIcuq18p1dkTWjjY0iGeJrZwOGKO9NYMwhID6knHxmYpJB1RuTOyF49Ih7TLoFOuxbORsu4V/BBMPNZ1Ye9dBCohaqCNeLxCeLk8MfK3EptM7PveSmlVr/q2UEQtSPDDky0TG3Ctn7lwmLlv5+4VDdDMFgCugD/yhCrAeMYS5otd5kbLY6R47Mmf63ywAYKeIle673j3UsuUvfII1eXW3FPKclG8MIpv3+hqgw7bkpIpPbzREAiGFoZLe6c6fMz+HErxzGkFiy+lMXHL9OEog6Zk8OcgIJwppdlCDKqhi62TMkQdc5acFWUlvvOQS5OLb07nX19aRavXJqaTmhf6U+x+NKNIn8CZ9Fi5sz5ANehR2P0TEjq7OY0kkNi1fjURRhDoqLwQXwAIzIYn6Neqj6IMrwBAN3rJE0NAZCTOeYupD5yGRBCUWQcMIZdkyBki09T7fT4IE4P6cUqJRgQa8hlCSIXYG0i2a5felQ0lU+DqoPkyui0l/nj+TB5XaApfjOgCzg47NSCWh4ghVTst4GitD+ymOR/ZxlYbMJzBPpoGh95GiR5jzUSW8KI8oAJ3Gmv9w/5iijofmDij8L968ip3SfrEhNk5G4UfspE5UTA50HAi7rfMHprqKthrO2PBc+M94drSqPjZbULFBmaUeLftZ6O9yOyqsmZg5RoqOCyrvwa1ccIfc+RiC5Mu9LqzB7QdRo9mUE1o96IvezsRwBEwCuv0w9DvK0txVbRIwzWA9ZoIRkT1rs4lh157O6e3i2X7PWkThScOKVT4yTMALSMuWmenm7okImKi6aYGS/XveQ5Vo0/sT17HFUxUNZ2xjHfwj9Kl77yefWaQ/vK55ze6+w6fEjmGaPQTxgdhWNHQGgo51R3lecLd8vsWroirsXOqhwepLH7fV7H1XreQptfrU2q7pyrNpdejb0InW9RJLJSIkVD3IuEJzbS/CvO3P2n0rao3FGa1Aju3TiP6KboZmS6jc5VupNWNekaZ9kbgDwrH3TQXt6bOoi3rmHynGeRmGQ6JlumjSNNp1uVDopLVGpzrPBhKKfTlpnx1O0x0IuI87n1GYLY+sdp/fmmKBT2IXLhRLaYLP8nimaPZzEQrCdZmYlQkxF/q+W5XMzF9AJJuRHzSmBzHjO2aXSVQAH5vFhiEVAHimWtk6u5IEldixNhajuPtNAFtRoHWjbtkaECw38h5D5wDLptOu5yRE5gxnMfsIWZX/L8+DbdrAKVZ+o90w0keGBAvppV8vsQHZeLrjVweNF+6OV0eMCopr3nGbWWQfQDFGJc/FMsAZh2+Lh0rWOj6dfhrQC1BFaqL9NcVd0BJTPFzg6eAhlOus/rgo1+FAFlux4rJKBZuQy1D++DmHh7fPWjUpy851vff3Kzrr/pI4a6hVJTq2V3tHlAAKKy81U5yRT69pB1Yb7P+3Q55svtTP52C2euwfzWHV68F1Y8O/ApPW1sB9Ck71+BpgqFnzd8A7SuY7xZiqhAD4Eh8A5TfYMYAxWmH8b2tkKPS5zy0VKKW3QiqMn1PK0di2o17lNk77OviPeRElR0ENQRVTyxZLz0TAcwCOIqvWCGulLNcD3pQy293TaMLVPNSz5cICyiObZgT50emXwhknddM629ImbJrD2fN4Nm/hdmKIpBOwnJL9HmrkzIPqC2E+9grjGkGp5lx/5hkXunPn2D+fdcXQnjXz/UqvdXUindwLdLukHV9pw3QtIzQ4nHG9P5Hy/5jUAbLoCKN9RIjhXUpbt3MuK9OpEes1ebqyfuzlXsICpq0Knl3EZzVKrdr6G85+qBtBp2717ayf8XO00IqSKqFbzL8UX6luBpYzgZ7tTGXzzUoU2QuNIeFPWY717k07UqYkAtifDxatcZ3+sMxbBAxAAWlrpjMOu4WD9Dteao5JjoILr6UDxqzyjVtMf9QkOI1yiJXfNywBTZOyd9qrnuu7gOsJ/O9WH7aVnXme5qoComYW3OlaAcV1Rpoye+ltqnbkkVllOE4lJJDqcb+Do9bwgkHytbL3z0Zir7nDy8wSA+O1VbJFBol0vrY3BwozITCU7XCJQiYJRSGBUCoTKy7x6DMZ5alVgT3KtPvOepLmqFdojoWPlPzj5cEwlCJNYGYIrkowqWmwnM+SRm7Uei86mMBfirhdsuZnh3JfLAipr2iSAHBvn5wyJJ1cdXVnwvk21hye83XT5UNehi4d96Ayd3aMWgC6+6wTXoVVcys5Vqio6jwpchX0bkJ6lFQ8MRXECS2jSgcYx1nC8hePxvs3zn8lnU4Kvc9o/eRNMHxbsgj4cS2o8qbFS0jHSnynbAxYqF+UN9T+AW1etclZzcJQjSAqypvvAkXtePLftaCDqryHGmhmufoyVyPhlbeuqd4lg0DQaEZZCBH9TN11HhCKDRSvDPlpGyW6vrnxDupz3NvZ3cm07vuyl4ah6cmWAjgSaRUd7kxygtuHzti1RQBBa5Cb15zeCKxQn+sfwP4L68PLrcX6jECFLVe2+OY/j/H9EEsPEzRR0/EHTwiNAHtAPcTBXWJrkett5AXmjNL3PzGvuqahNXU5YBFhHs9aq3LVj3eUMMcjXlMMa9CvKSEzgTIkeHTVRxqOcY8vmyjhDkvqjfuK//fnP4VeyXGaU4ZwRkrVuGAyuidRArr7wYaKXP15fk/vfOxf7/vpYKkhC608vwXG0Gd/tAocjzPwQqOw0wFANL0A0ahZJ6HeVN3OmliP4iGShud3/lGH6fSqbtRaS7PLy3mKAzHDZf+18plTHDg5qC0Ok6Rxc1drJ1RBLv58aldYAz3hv/Ry2yR1U7APdLgcm098L0ws8W4j4GZ9Ukiu9OvOXAOluQyXmH3hCzsd6qMPH9Sbp89N716+mccb2SyyXMWv917JEEh9Mfuggjo5I8bvyZiRItwI4uXd9VItYTP8NqWks2TiCgsIhEBgkqNtpDJ5jaesBC+5Vdu+AUMhkAlOP2s2q1r3RJ5UhyF4XurNoXcB+6h/p79p4ux97ox2C+FrlutYEnj+1EiTkqPlVqzaZQiNGSTjvG2So2OSTTLKDIS/gtw+5GBYu4/ZtH6CZ4N7DDo8rO5Yhbd+H9msoL/17Ww9JRJdZ2HsVFytyFAvTjlpO7+GMXPq6TDqdr96jyZQyoldNtspDCGKAJxz+brkHcTkFUAIxDYGpYhSw05Z8NTYUbq2+KPv9y/hbgLlxEWHMs1+gqCmfaZn2f6SWqkzrb79FwEEk+/mgkqxvH0NyeZJqo+tCnlHgSa8beFf6H20KX5oyGjeenIJLSdNdrYjKf8oSP4K9+cr5U8fumVKojEa/vxuuuboWNLL+4zlem5O1i6kLO++KJVr+eUZ2RBTe4HRD2XFmgazJIo8qx/w5kBpvWBaG3TFdGSFoktYgtBZihyQIQqZ18i9nSb/FwUrl9wwozEDhCj27ILAL4VMOBoki2SkVogMWvT9LTl/mqgY5H1HQmCqhiO987cxj/LEh6Tp0+xGHEnmDOV+S9u7PWIORdEALRUDhVSSixV9Rf2z+sNYkJYo+EGMfUTCt2GH5U+zvQ9xOsPDYyzMjgNbB3x2Biq1RckiFlGHdgDI//FYw3CllWZ+d2Gd6Q5GdX8nR0VQoY+YDgjkb+htdTFHDr/bOI8m/5VmOju5a4/PBGvHxfn6sbMYQVm5EQFSxPu0gidDwt4TJWb1plPWIRIiILugjLUfouKdSeDlfRFLY5GjHTrECra2V/7i2xCWQtpx0mFQ+UiXs+esNC+Mln9RaeERuGi66T5836jxU2fVYKr7hcjlNOD7l7ErlKxkvtJtsJn9hdCzbdr6oSGWO5B16vsD5EZ1ADThbz3EmpKgdcDD3iegzLyNAuUW8la8bjCVaPUw1sciOzWHArSG2IYkg+7wlbszZO2t4D+VJQLFdgrNpn426RHXPL1GqXmD3+xQ5KoMgt2vpviOY0ciGAP/yLj7G2Z27oWoYeg7H9KSSb7tdAbrRtWZmZtFjOyXD7sFsHkTYx1WM6tnEmEQy1gD1spYF/yhvccUghuT+yX4eswC+ej2JCuokyOFll13OlEWXX5JZhNaVdxJapwut2n+SnRHfIRZHX/qmb6MF4XmruZi5F8SSHdydDzGGsrz70me4vEHCDC+J9eoCxRaOoVLaOtWhpb5hqL8gd9DgxH8yqRe2ocYWu71m2m+fKkisjLixFmnkUHkFTDg9fbZALUzgyftgLT2IyGdZl4ZDdfWQnCN3Unke7IMAfJn53WEt2IaVYRtFJjerebcQnGBi/pMriNJzUnj0nYKWPge45Nt1jdFKNf4nDNY0BHrihGAjMgU+HQSa2e/b8r0Pa5TtO+dr5o2LZgieZk6wM1Tsjp2wRdgoULt6VL34xlgoqFYnx9aIRfhiaw34Q27DaZyK6patC3dbxyjksi3ET2adyWuo8Lsh3s3eI85FAp1tHCfA/3k2fKqDNW1wLhFYusIPCGJBLAifcte8tirVbtyMOQUnZY8VPzxUD4f60X8II+Ehlh0pzY8JMutCgCHEK/TjLeTrNrroFTKBgbSaTui84Fh33tgJXx/xLkf9Z6YYaNrOMU31rvYaSV3j8U1P6Qx9II+ZEEjnkYFrtQziFtWy7V1087cu361nYV5e8shQJkEZX7sRrcibP2dNDvq3AY0qKRchBVPmOYuzLir96dw2pw0THaqVdbft7FN2RGPRAE1MNvzB14wlebtzPJMpzEizg4EqzzbyuVP3RzarE6TtT0DsWgJKnEDGq3m1Iu+bFs8EPQwvM+uqYuN3iGeqGoftTHULgEd544gXbFYO5V76rzu5Re+MwPXEMRGZCC+H4xybiuFVVP1W0SLY9Gvh7DxCECDvsTdSOOvdYFqgoSb1J+iN8OaLf2RX7tfI3GROAImGuBmHL+9S7mHkwbFAYAW5Ew9H+A3qDv4kwXeyUPLsRsfikmBE0jdK581SmbWlypcBQOaSimViVZ/g18uzegaNcEPL2ZsQytFzWv4iAMZJaH8PpIkeNWKefGgWAs13n8DcFHTJaKC5JppITX7Tfo0rJDzZjME0NbWIZuMfWZ6aNX5EknQBBSi+5maRDueLcUnhrs+vE1YhlGAVh449ut9pr5wMt00Uo8MRdhufceD/XZt/D7zq1reN9q7lM3DLfl7SLaiXZI4QoYYeBsB0k//OuPGsOdEVGSRxYEmssc9e7PMzprV4jXbcCSAuwvb+1AEHNCHKT1sgkeqnP895PC2CxG/biwB6NV/Kk+nw1jTpbr8hmG5CpHdaL3EIGzfZnb60FA6SUDacUVoVvoBU899TMLSO4p2gcEeZOsytxfSRfieLOdxi9S6ZoVlpHEnsfEgHziTvn6Zis3nYMCS0DYQfjSDcQvTZwzvZhKlmz3rmHqY0UncppXXGEG0zMFUzkXmh9kPaYpKT/v3VghCHwu7RJWUR0eIin0/tFNb2paJyTK/vEkPixexLY70X1vlzTaGpPZPnB3TjnhXcpfZ0toNlghekynlgrEs/fNPsLi8LGu87cZSZ3FB5aFAMjnRLMauI48a+PXfgnbheMsRxLgrpQ0I95WP0NxKrJtfV6GbuGMWEiP8hqlVUUTm2jGq7NWpZhwcUWzH/yiSZPJOXCFiyjgmRXGGdEjneSGYSzT6zGpn3id0WQuTX6axIX7yCr69o9jflkeOPMafAAw2dUgVB3I1CHknBDbWrDXduZVI2j+9AT9d6Nm+55T1wHVP64makONKNfA2V9BfXW347gb3FoXVxRwlPDiU+ap1QanASGdZHZBli+Tg8MdqjPKCU87W8LPr6iz9BGdeyar57XL9EJE1+njYZQBFRjIOfzl0YM4Iynlw4L/JUAXyhq1rhr7AUFy8bk9trRXSHzL4DlY9lWndXcfIsru84jwv5ZtFnDyCtmfUEMrqvvLi7BkwU8iCx/M6irVq7/0xASimrulxIFsrKzg0Me1M5mj/vSp/lvgRp0pMFO8nFxYw1udAsF+gc6zo4Tis1BBsmiosdlGdYL6zZCyveNYQK1WxoKlVTV1l1FfuKUD8JT6ijpSF7bCEpRgMYpGVbmpjSIEmTR+9mUUbp7H0V6aJOLLNnmgK05fnwpRyIX5P4llwIxTcfLKhEfx8QEVOFFok7T9bFk2yReUijecxcIlVaZM/32wWHnaSjRC+yleVC+Dch6XqS8attSF2ZvYjhly5WBXQhzvfDC7TKUeWT9ZptZOy2RFs6Rcp5ohdPzv/3BJ+mxt7ow4buu+Kc4koBtFONYZaJifK7cO8mqxY6WQeJ4MkCqWQdRSTw9E4F1XvsqvM1VjbjoRKB2NWehpGbAF20dLqiVu+QnU0rvkWBva+PbgoMQ2l9C8sJi/v0kn4PkpxUrob4abhKhG4/UJ6t0Q7Jtz1o1ckdRfXrw7zaa41YYboPaWmDUwyBrQ5hpXwtd4J0ePorCzb1VlI6u3wnyKb3y9baXCRpeQnKIKnjHJMYCk3Q1ez/1YouMn4BD+7Q+mt6sZnR4x2n0LQWxCFjLwxTVbzWdhsU1vHWQL3IowVkgC6JpHfbYW07N7efNqmEnIigRXh1+RfNjhin5a2TDLfGZ9xnc8h3tazM0bGG/7+J6OY7noIvqnRNSiXc8KQiljwOJVcT03jCt4N5lcz0h+bAgnjfzuDBWIUtkd5H19tcA5MtzOqN06bEteWnlQZNgqGDjhKrnOBNM25YyJ2XReON+Sb1hHEgEzS/wsg6YS60nlfeAEskun4rInU4IcdA4PWvtaYvuJnsmH6r+pMgGRLAlUY+ok424C4PD+Eukebh6RMkf9IcNQCYNt2YRRVHT9k3Id6Pm7WZ5B9bYxQDXFs9Djk0VUhmetVhdY7ZhdosL1i/T9B+tBLoFAahyrj65mtTYOyUMRjpJtvK+fGERAXI4nlbzeuWnRh46XyOclnm6w8X0WqDNwu+wcKXda2DoPDtsWOcv8kfhXuzq/iUF2spGKweT43SPKtehDuQNmLVtXZ1U0fOEI2SjojqEu4EMZiyuOGJefdb3VOwWx5tPqc6gc+AywDPKuvmAk2LoFB2PAjhiEhfKEMHBwguPA064Bloe3be3d3Gk0uCzeyiOw6zQ5aoIQLrspfdVkH7Niqe1vK0MOVlZOSXg0hrf1RXD4gpgvjlGyysF+hBZbQuzcNSQKZEIXG2uuMA87A7f0FZyReyImjEgqQslohbFPK2K7FlQIzt0Udi2yuBivQNHvPmtCy842/UTLlS7g6x0s90BFKpEBgmkajhfkV/H411X+4D2bwc3fq7zXUQgff8W0f85JDwA/aP/eE9zZNUb/12JZLP2YQr62wqZQx80FoiUDgOs+bpYnULTZB0/7eMwuUzPU+g0m6POBzwZKWIDHKzsoj8/aGjd7W29Sw8jaFzTjG20cveazkGhsPrC72PuxchvQY7cvy3H/pZpUOTvVQdAzmJire8qFZTVaccpt8PAlTa40GAUEvD9bn1XnwvnIGtfx+LzUzIM9vbo8SiAB7C7DjI2OdhoSzaKjzs19Ndd3pkXURo75PAcl79T5dLY2OzSH2YavCBeI3hP4PmSuOogsFmsdd9cZ9R+fG7epdn4MHUB4gAw2mpt+pCsiYj19TtIqBTW2wjmCScllsXwpV4R8YNZBImmsi8EnK5f4wjUux6+SoCU3GjPRtmiAJGbEPhU1EfA7dEAonio/hKBklYpkCqAP1uFboWbP/8EpkZDUEZS4YwWBXr4SfwyIQ6ZdOVuUPbz1hh78uPpPFCOccTr5F8f8CNGqxg8TNcj1pRT0YnBAhXndhAvcdQjCGUI/TyBIxmG/ZTzyMRg49urrWFA+IRh0SMcdT9vBHcp1t1stn2uPiEcSGIWZC11eXv4F5j2RKrIY2Lsx6HW9EyjFVZP4emLh5SLucpnX6sF2rc14+xLOcQvs+8yBX23CPY7J4KrU98lguScIESHJ8SnXsDXmz3kbt+mxJvPEebe+Wuetb/6TQ7XWEhtyU5IsRA3dximmqXdqL3XElNtgZyCEw/EAbGiSP87xmS+sUsu0g0xw97HMs7YYumwehX9Ft0VnUm5Vzj6FbpFqJ7835IcGq7pfPF9jGVn8wPNOR6dqdaKOGoKa1Ej6XOeW04zek2P8dDNOuiUPpB4TOA8vhv0mGPzyOf7UFxKzT99PsZ7Bw8m6opWyPat267vTz8Pra5NXBH6fhKJRC2peIeN0xdlc3QoV6UazXS0mZHtksjyA/BGpm9ux1ilYiX5LLtO+oi8soZLHymeaSXAViqy+KCt8h/JaPNwDz3XRt3JeS5TXq+n9aByLxT8x1+VVVSKf7dMA/BkjxNij/CBiPtrhamK6MzN4qlX36XZQUTOD05JNNNSVUs9/4pUixWZrmqDOgnOSXYfKPQBQz6ubX1Jo105M2NjdN9JCSeTZddw/0ngIa/GK0M5D4Vtlam0xDcfsGerjV2HZI1fLtz//24c6/WZOuAhYOxR+s1f9GHuwqsrT97kxJUktddSEJhzlI/kMcz4Pp8OHlbUYuMkE5CipU43HHz2elLyKvr/2VwZXQq6T1v6x2S2GOF1ytYMVLX1FvEwayMLLq7l1VnotQOpYanBqKWhfyb4e3XcqosFldl2o6XX3Xyj7H7h2SH8PM+ra+nrRmuku3QoGTeMJfrJZiEClakYPwcR7uLWLQFdg+nYbNxpl9Lch4ro8uHqEPntyYn315Y+m7A0ygJAIWYdaB2NQnIEhwzFfhD3C5ztzqSAZKx6RDqEzTEWIGpoCCm4v0bkEnOUSM76KLmrtc7+nobBGeQbjJcnkl+32toge1GMYnqkSvDt9iXPB5z2jHtBInL++7BT3VS8WiueJyE8LUqRU2h3ldXDp0iNE+yAiiWjAacdbGy/HzaKNNyqL37OWoFQeIT0pCTg6ZAO8YHjM86a/r+Tgd2/x7+FWrI9LisskL+wdI5JVAaStQy86pC0I3nn1RUd79NaspE81Gh8Gvqe8o/IIbb3eJuAAPUHUFtJkhsTkMw8muMVc/E07izHZB+hNxpV9gFlnHUoqmGY8YVKywv3WpjAhiqsCv76Yb0X1/t6TCIoVvJx0A/RxEA7SUfFJ1QcfzpVpHbGHVed3oGlMAqAH4tK2xlRGVyf0oClkIUuRqVLdL/MVPojZHFVIVGlN5/h+y0D7vCKu6Fr9hX2MmtZpSJoc4e8W3+MCxA7l0D/WwhKWemnePHgxUxEW5zSUD8qJ3ztDeiYo4tfchcsEITOkCHdKxve8yaTVHcwgPUrOygWEv0HC3BDolpNjGR7WIyVMoZ72KoyijfhHnzOqbntmvXlh8UMUmsRPpr1xOUdSx7sA4ifA5ZG5vXqzWy0jLqu+ox70scTlUJaJd2/Z/5Z/q8lFAQue5i6mMVMxHeF0EUZJ4t399i32PHoxMQ9B/nI1iDvdIL9WjBu0sGVmfz3XfrKf/ln4NL9L9SmQzW51Z/9ZUjgEfvQugUEndO+ef+c1rbPmTA/z8vLvhZWANlToGw2N4aoNviCVmMVqtOg132Eb5/ivgvBnFhVBRQc1LFSRktGVdSz56xYcHW5+hqrxtNvCpp3YJsWZ6+AibuBWZQ/w2EyVw07WTxzIlXgAFDwHrFvaGgEUlwrJXRG+Wh+PRbqNsMq+X5Et8vyDeCuP/FP4Sb1DSJEaM8KDIpQatIkNFgSEiazdh8Zy5d8gfpIdeJO99K00CvxTn7t4MIBJ3V5fnNxi67nehyWok+cfyBP27taxbEaFwPsNSTESw+wU5yFVtMfJwupRHlYVNpLiQIXYrjrtRCwJt3/bENrnrf8WTrG3VydbjEesTGVy79wlOOABQCdVwnuDdVbWUZAzEq2148YB+ZvjeEWFXoK9Uwa77jzL5BG5Ut2Ps6anFyVZvSTRBWXVtS52zxXMRK+UbOZbKpCMqkcW1Zwci7EzzZ2mxu6sbNWW/FUz/LkINZi/AlNU1TOHsHZaNuJ1H18c81QdBviNg2CxF/a75UH/5KTiWVO+AITB9erDlNHGIGh9bGuENNpARVAcXpXfQKtZwk2t4HUt+s6e7XNjFv4HHQctdKni/7jatr8EVmSZU2jH//mU3A9RDi9q4pGWpWnx35WH/lnVGDPkz6ePpjU7VrKSA2ujP+5SaeoCxs7pjnzFFSGhmEmmWnTnMwbIyrBuKgXf+wV2r1TbtNMFl5ahm6S3RNOhFGVR9hiHGcKUcmf+aWEJPdaEAdx0EjBTNRbVHGplbseE3pxuC3T2abC2VR/r/YeWhtro/NRRMkqpUZ5KHP3hnc8B0VcztEru1uEHXIMjFn2BQi20kKu0VFzCXWgVhy6xJNjvZF0FPQ7P01QX3L6WRhAfw3Bi3wODBPgS2BzZQ9esJ21kJI+3hriQZCiQ7MFO73hkIr34K74+pFpXmX9F2G6m3w+vtkgl9AGE5zjzRkzWM1pi/vO9OwrdnzPOiT2XWk04WNik5JLPEIZlsPNuIzK4ps4RfgarAeqFxJ/ujJrO98LSu1IO2V909N6SyEIkzwokJPlBg6HVxPbsbBnEU8Ysk9ZbArHiPq4WipVQ2wZ32IGg8jhC+XY2/zUWZhkS3HIqhYpVlSB6cM/Tqjy524bhh0HpTiNDZy/PulBeQhT0WCnvfUGF196bnIacRobBhecQNIzn1YsxUCfqfxETZP52aUnJqdpTd77UAufRufqRc6mGbmhDcKiWlBR9kwISz9A20MAFkuTmwGKXlD/l5o/Xfk1dtY/nam/K9BajuDLn/MOz8kQXIJpeoxavw62cZZDU2l75oZBcvHawbQboHV+IQaJg6xYM57LGB0VwTILFZD1JgLobmpbCA2kWfXtQ1UvFYhPZp9KB5Z37I09lERILfxWKlitQNEC8QfRQSVzTvIP58cOHkQsEG78jVsEBXYXK1dKyeugO8/+9sp5IAP5A/FhdvFtMRPkvcYD78Ah8lTOH1W5GnIXetAAlmSxcN0D8P2LRFQRGXy7m64jSHk35bYmnhvCCsDac9si/Xentelndm637HXV70qW+RYOBjgj2glTltFA7JAMkqhT14jfOjbzR3pIjWeFOYj6ffgNPdM6F0yfKnPUfunkjdfQSC6epxyEQLNAjHxaCNkB/HFOhEO2xtqHD3rHf+JLu2fxy56z5g4zIaxmUqPC2EGLrc2uObsD4CLCvIL67CUONHU6gow/xsWIMgHr/JDpkcMeZ5qeOhqpt6NRy51z3qaeI5F4s3Fms5Xc9vHd0sQtvq8I0jvFMTkvSXMmQBtO757q91b0MCPz3MrjN+AZ6YjvmO+A1n8Zfj4H1Fq1hNZVcM6FlK1dtX6OcIvhP37Hq9JufU9yJRjP1HG30dbySdAg1VzcszOfu/52jeQS0vK0BPnUtBRolCGfF63eMpzoPrEeCviKXUQrw+5JKX4xf+s687cHGKE5L1lKtEpb2gwqXieLbYWHpzIKrO0FGK//xS4THbgxDQtw9+PcmqpsLBqvjvygxX7KlNWCh9zrMTKy5CXzEmr3HhoZOV6nQGzbpuzIyoHdL7JmBNH2M8k8ZkGgfp7epRSR9VTF1fPSzUB5yP1nRtil5+2Klsr/FpFOzsCmxVh73/kNYMyx8+92DYB/Ar7s4Ul92xltGU97LZFdBgmfXLiJlLZnm+Qqk1va8apGKPes1kAbDDQIQS4ozrSnJNghvdGlFYNI4Kck5PkcXcJG/t40lNeiRIzMW0seYuLTe1VfocyKaAzFvpAsdQ66u3cWXgb9tfy55fI3TwRQdeEVw+fuH9ljg/m3AhtuVL6X4IJgwTbnth64erdYa06kCrUU+HIUyjMlPZsCeRE6xM1MxNrtkg0W7vzlWfJIQh9KdpVRHp1io/bgvDWBTU/PujuGJyJeRG/mSVj8fCezRxF3VT7CQ3B4l0fO228eisA8zmo0Rtgwxpikfi7x0gKJUY8rlu5tjWsCS6KeN9a4DTX8XJ4BcCs9vHABgEyaYbiVY6zSGY6p9RO/3Xts3nv5sZgXwM43kAo0UffDsTUdb5nta1E8xwlNiNOar0kfVxdy/6rShFWmZsSURhjYGAbAis4M664dBlfPlHUTe3DZ/SXTFgm8z32CTPSvtjmlJvsc+K+ofPGpxQ78yfduXY68uevwZWLS5rx9wKBr2Uu5xZONgAeZqv7ODlLs0xf8p0Z4rLRdalV5X9lPAA9fUsLgV2xYMcypJVhcHwDdq1Ar2MbXv19zcnEd+80vnJh54jBfglxWO1gJc3QYwcsGAKMGNgtU1Pr9MH+jHu5BrrmU1p2H9UVyOso6bpvoUyJWXiKNdQduLBDM0LQUVxNxPFlGF/pYQ6EpN7dX4rErURTVYOPh+8wHEs9cPWs3RCJSA9vGGswB5dIMnvKojQOPltodcJ4xrQ9W7SrYjgbYoAcV94lY9foOt9knlThKPR8H0owTqVOpXTVE77wIPVCElbsLkYrQRLVUNGOltvva9lCAZOvz4N0Y6SHiF18cch+K+mjma4HdrvKthR0B/mu7qNRT+wxBcErKceMaeaymnpaKvdXeUPlMNBJVcN7uHLvXmbmSE0SYYBUab+olk9PEcOXcwXg/CJy4Ktve7AwYvdqyoNrs4SM/zYenHF30p0LPSAnX90Vd0CJ1UMHi7bjSgPVzeoBmETUGYUh+cPt2cjxlavT537zp21eepNn+S7q0E6ZkABHvMBYPBz3MmmhOdTp5kodFGI2FazjHc1Vea5QiKp2yiMmfLOKCVltubrkIUCMdMXi02Xwm8lca79LNPN2VApeCge4zpHNrMsFIPApNPKzorFGg3Dj56Df2lUsDWKM9XZh/imjiPCt9ZZeLR1qCMvi12hJher0EuJ0x0sgbY4tL8LqAK/I+2iITpAkfSktBOdNYNwkbhLrSPKyV2RxS4ytc3j5t5MRPl2HNbLnnh88a5rwAIOo9cKyApCVvv6ltdqJDTgacONd3AyaAkBo48mN0w3rpkjul8lPLM689ojbfAk/0C6TaeNT+T9ssI8/G1/oAhDaHCuK3y6zUmKrXPspfvQId/zgVTbgOYEp5OzaDtQOD4QarqC4Q9bMBmTNgI3W//6zkhcuL71JwBtNfZmobA6+PGuZmLloY07qrylsr8iJ/o+CknTyGoldvPxDx6wy7z53mStd+sUCUo/qKl5vH+cuBz63chYT9qffIkMjoOjl/tyvAhHeA7hn9BIbuS1/TF0BbFiHKSOZamcZL6GsmrWB+vc+QZc8iSIVXvg+D8u6YuIgDpbvqnat+CjZv+OErfLH46wqaSuMHOGYPEH1hC76C1hda6FaaHcd+YgiIQoRZaE9SH2Mre95d67rk7v8UgCIEQymiadFV8+7+7mM5svy+9zY4SOnb7SBwnFwy3F/Z/tnOdnQEAdGB9lJITLa8hrt94zVXuwsdef+2l7gS+p5M+sgP0kWBSorBuKXRk2pJ4LpOxhCEWpHqJhJ+L/BGtC7NaFm4jgxBPD7CSbn9nyM+9PPORQIA3c8s6gZoy/tZ6HNpuvtqVnEU8R/nyOjBPlCZKHyRLsOj94IN3y8LDa/2Ivkp4ent0yoH9Pxsz4y3ssgo7z0Yi5ijwYBuzMDoetx9cHzInpTcgyIVdPs6zoQ0Nyy1A1/x9qPEF9MAgyvHDN6t11bqw7PwY9vcF7meS+eYkCEf26E7SdsJhL+mlm7GyG8/s3f7wl6eiSRoSesjthXmEMVdkqymD4LLa9JgWTxrCGq86Ad94qzI5vRkC9i8gexJDSzhrgAdo9WK2JD/IKU/xKpN5eKx15BTVRtxnPhPpegrWixUl7uhd/K4z27Em+Tlk0hPMeyTv5V3hmr47w14Zp4XlRg8yzX5bgGhCGIYH9tYDxtedFi9yO2nbGLdZ64bwz7iv9mfMoydojHWyAYmBCJb6C5iMxT7EgzeK952TvFUTA1vk94KHvJmVNPglARmdYWJ8GN2y7k+VG+4Z++/Zb9bhZ/1wivylZMrnOFIKx/H3IRTxd47fQUwjaHNiWvOnsX9mMXPXuiOWYzoFVFCKdOf+K4VLvtNrREtWzquHEg17jceipmB2BRjeyiWKCG8KPkH3XcVB13ibWLeKqYc1fxGkzM/H6giF6rhTHuUCseW1itrhEUqEoLEQv/21OMXC+Fgvz1pdYoyEcZ07ty2mfA+MZaU3xCSgqKvUAbbr+3WWFQQ5+N7Eu16a5SCDjgzwRmDZnxC36tgjrdMMI4+TrFZpQ+ziBqv6zPjaVYWfaZog+p858DFr1aBFHU5X1I13QfdBfV6U/QsbG8MuhfppvcOfj6BTtFVJefH28iXcyK97xU0NYN4lLltdg1AWFjytcRnu5C+GUfzkUOVTdN3M+N/w9liVO+oJLclueTdLnPZupt6MyLO+3khWlV9O1NQoL7K9ozSqj1SfyTU+naVjuSCpql3REXbVJ9HsW+fcpc1+4P+3Wq8ivKt9LykybZyQwzs7t4ed3ddMQTmnnXh3zvXjlUJlyGR8tbSk9F3AKVEcebTGzzf4CE23yYrpq/1cJtBe8UVuDzQWxjj1z8K93Fgr1Kv+Jz/1w0YnF0bPXp7rRbqyDwLrdDzSqze01qY3jZWVnufrB6fkELrq/A8PoDZIeP7rdWUFCfxrVxX1hA5OLQXdzPz8Kx62xZPbKMYodsdVMaE3vlNw9Wk31G+FW1Jx1Cn6zgQvPh84taJiYEoFSpMCHaZpH+R/maWPND0b1dliquY9V2FepkKLSbkH21pq7KSXx9TFr1fgAfKoIeEwQgcCOjCxJBo68xYbeD9ez0rOunTEQTf9OHaxXMn/dD+WjjXKkzFohOm/lliaTe1P/J3vHMcpu7SLY+mhb3cpgurm9KseoA1rUhzTez2xr4OKktCbSrlKz289hxFKna0tftawg43tGblpf4P6EU6GOdCH7CrQA5bDZqq1ZpHh/fhjzHMINVrXEOjc24Hnd2F7vmGwfcrl1ouG01dVpB9kO/eCDs40HCrtkjmZsKED2z28QU/i2nQHAZYK+lyfXppQI98tA07PsFH40+64kN6/w8UVJW6fShNuhXIFIVEmylQAynKEu2+KhCAgAqKUr+37WRYNJSHzmC7U2ZEUUV+i+ZisAM33lLF3vqSXbaTv8KmR/XfbzRXwEmEGiJxhWFxzbkq+6sT+ga7tAFTGGiLUBVQ0K3cVH6VgbrFcik1MZSMfuy7MFhAj9976cuEfJaZWOgqR+8pFX9tQULGgGdMD0XmCi+oevWeUbpl9OKAoT8Q2y/gdyB5ZjVBjWs1zh+NBQZZ28jOs/z7xdd/vdYOJxEFxsAjvXnjyK8zdp2hYIPP8RpAhmrZjK0GYh0cEOoBSgRfeyk+TWKljqtEB95G4Focww6Y+HPNwnmDQeBc5/hDE4E+fxr5hsPcxr++4E+0THpISrIWtYfj3i0As4iSdUbL+oNspCwU+MCp2YUkLjiClF12xIhxPGGQxbPUEawtiWcvqH4NzTfEYTcRREkN3lRY5VlFNfu3GpLVKuyripBuLsSSVBFUcF7iEHBib4J/+S24c5Jpv116ZAhhIXWi9HoT3pGrPi2J8ML/qYjYegWryYKsnLTqPbMnOK/rio8atpqAV9s+MArpXrrh/h9n4p72Xr6czgcSBac5QK/izkqBqbirPko/GUI5mdhxFz49IN7VV3Hzlv5+IB0RIi67f06mYjNpjbFLBoPrU+X/ZHC2FO+CgEJne4tVCCcM+2kuSFupTjBZtEb8Aj9rxmVWP/Oft/02opeR6J9HB45hG3IT6Xbo8hovnnh90INVS6zLAKtwFbxBLyOuj5GKmcLg4Okid1hZQhgQxhrjJdrVODe8qWUsZtT3QV7356TYyx571u7xgHhLF8PcI9oBzSjt7nxVv13pSGOJxC6WUIYi5TRr+j98Fxidpfd/g+FnfVF6uwkIzGAFNqjMjknWRsN5WB3dl9WFciyZmN4SRuA/9u2Quvd0QZWtlpHoGloQGRX1RMxgvTSvcPVSyrhDA/vkTyl4WmlsxxLDUqDtYp+XISyRpz92sDiM2Poc6Cb7WuIQz4kFEQ2VpJMe7ALs8v0QSLJ++jpzlw/6ebttg9CMxTvGZagML2wgSgzEzGXE1+W+5P/B0XgRH6gtYDv9ONhmRorQghRQdRe3kFzfn8KtmWGoxK2971WmRXGbdnr/q+i5k8pOM8UI/B5UNFUJRRTm2JeFmjmdXCTi08mQl/JOgBN6gPxiCwGu1SC0DM14hY81/GZVp2wxBSCezKl/mPP8GV1Uo25KItEn2kF9ilTM2GsZPAPHkKKhOLpZ4+tS9ZePrSE3tfCh4aGOpt5uf3vVmrT4lA+JJ4V2TCww6xjPZboUpPzxTjV0f0buLO79g5k5gl8KTjEi+UWbU8dKF/zEHd0h2NiN3AH2KaiGY3tXb3LHkh198EONKAMp05ap9y27Ud3KPZzk0r8qKmCezvWSsz2nIFEmCihOKki+ccQA0X2fvBZhNFNJddwtb8v3ltsQ4J7cyMGJkew49YoCNbfcedpMDtTUcVTufYK8mBLFWuLI+K1wz9GK7HK+yHXh8YGf0MkeCxRaKIlaTPaOCd0pFvDF1GqLFAi9Hif6Bw4/BuXU1m2HOb6BFd8eq/QBkl4nSK1XCysnGM8zibPu5W/XowZD/KZ2K3D1dVWxqI0NuM5kmsOUvY5IGsuCU7TYthBdFo9QmboLaSNkDwvCtt5ycdjAVW/B1qMUIeoCL3hHL829W7UezHCV9zGK6T5VSNeNhChobnRp0dl2rBjOciVAw2UzcKOYl0jZemlHOp2W3PutvfJMPkaQO4j3xNNyuQffArV0JVFxNRiHGogpORXCnYqqhhvlFfrbkfkt2kXLMOJqPN0AF5HWqBUj8eOy1DnDV8Y7xXzT/BUaxdi09F61vF9K5YLyrn/0QOAV4UXQZG1lP2XIw4lHhylr3A+9e3nGRlWpFhLf0zaM1d53I5xDcV67si4v/MI7sGUfuveIq0mytKHgJ0JXh9g1LN/S1NaP/LVWVi0rPwbhGf9iF4mSiLKfRkdPahy//ckGBemLJb/p91rnj8kYU2H6XeYEpiiQSQgH4wCcNN0M9STkBc+oHIvcvbKKuk6L0N+QWFgXuoU2Gg6Ntivrch2vfzH9hGksz3pr87QMoYNm//2nG5kXEqVZmQ49gAFADU51oONxqHHH3FSNzhW/tiF9fatrIijmP0CSUO+x8JAmYqFQDHx32tU4J3wZ/wvti1GwhmA0S245ly/nWTzDVPlVBmTtyrq4aa/+IgYltlWGXJwspr6LNjnkCLNOY5ZfaYG7ys7xMD8iVwEk1Djio/FyQg3GdMpGwfUzjZ9s61fC3Soc5DwXL2hfxxJ7AAvFcT5cwPSDcQYSIjKwXJwlYjeMQq0yAISNMbsiGlszK+WK68q2g0hAMWHiVtdGrIic3tUTzpOkxBKKBV1aF0/jgDrT5oF/MlGYXs6BWXTsWhr0FOeNibgkRyHDgm0a+ric7yPOh0PIk+hUknsjaRKrUbf9Fo8OXdtEigmu1q1esq5hCXL+MYouurtYXYY5G5ohDJXc6dli/n9wlc+Ae4luums+xxfuOEoQHS3rWmW+uGpNJ2izOa12w9U3ikunC9QNxGNPxHtNmkd9o0quyesa2mwVxEyRkdvTI6aBsx+rnLS+TiTJQQFsYAsydiWS3zCBEA2x8B9v/c/th3V7e3iQxPNiAlcWwfgSqZO+ZYycKavgquXzsM/NPu9CLTSv+Lo7CwD7pE+sVYOSdUnTRquUYnYPXM0EaHy0ZfNTgfzvbjQo5BqC9SPtaemwcOHXyq98Q3pcSfNjbCA48P5FJhEdSfjs34mIZJ2T5s2PiWG5dLKCMwZxFhsmmJf33EKNIRMNFidqVNPh5aoiGbUnWWUZi+LLwy9d53CoDyw3Gr+G3Uh16m3XP4bA0V1R48WJ76M5mPfD+z+/2KIKZ3c9o7apjib6FEviPF+qOgNCgInmL2qRHIYkLswXKB3CEn0fO9ioMRy9xZcEjg3MnK0DnkAOYzKDV2irveUT8/rWB5ELFZqMDAs105n/dt93SYOwJ/foOx/Pt+1ogIo9Ib37jvAweez390k7xMClPMjrSeH8aSvDw854FZusty5tBpROPt6t/Ey9WwOH+WTcpnzZgv8WFFh1zNldk1fVRjhbIfx57JENEEmky/+GRL8ZC8ZXUPI9+HrsTo/0qJGtrLFNDLeOokjoehsY/H24x9P+5Mj0fKIdJfL2cB6YvuTubUbR4292cEiGKepjfxPlfPv3y3m/rRjIv4429Dry+iBdvCzLxWRmRds4TAOaDRtb6xeywjJlD/Al+IhWr6WdV0XV3fLH4YNSbZARHBha8RJ+OllGSGCYszEEpSN7RzcL8/bx0z3y6cnOspt7CKWaIgfMyEAA9IE7F2uYXl7Gf7S0ll/CSQT3Oj9mFuK6ZFRTR4igaafdW6Tw2sSoeEO3XAzNvK9G1joIMSsA/R1g/rZONJpap+D9rfITgXlEI3WDHIa4OvqdLYwvmxTK49Jh2cK2o4CkbrDXEA6TPrLQ0BjEx1Un/ev0TBScEFPmWg6seygVIewQqjuQ860uTmtloFF/Mf1KwCXOPqlzTQQlLlxmwWIWjB3VcLh1sDvJttLJEx/ziv20BKWm6jSzzmtKQaXiPZbrpoawLeHlkh/twtnyicSnSg7bDTMWAVhBO2NDG8dojikibTCJbGhvpqglqO4/MYG8jZ73TAJ57uh7bQ1y030Oxq7l0tYQkL5c/yoyHtNsZUtCm8Kl/+dkmj2u6u/hm8KQ2eD4lLjdjpiTrqXLpBcjcpANM9LPg3wiCzby+ZC7uX9Q5dJqv40xXQBRIcjJyiSy/dVEX1SAiVSypxms03PUDj/57GL8jQ/xivzKijJdfgmX6yNx8mdPiMvBrSoPTmK0gmXgjwcgP0XO+LliAanh/0Pz05gEE1k617wCuyU3JBi6vDhWWgWjgktrrCn3GrboERbF+O9LZFz9wCJ43Lub5mOB50JB3n+PpoFFHTVDRkFkquexXqyI6nPnVVE9F2rT2DauA/L9Cqxi/Zyy5B33EkaBI1Bp6vrNOxviNA09A/MxntWZ4uj/iuqGknOnbaP4IsJDZyTAnxXEK5WuvW1WnODZK7od5z+/e+KMDJmXfzY959ARqa7wOfYsYJ/9QMHd+KrSJjz2FwMlVUPeIVA9UpkZnERX8XOe3auJRTPLLwGoBMOy/CePZLMV6GE4WV4GV/650jxBxmm5Gqe9a8aUor5/yJ9XWgNGQGwubBjsbvgOEHFsVh6SJ/SyCr+1/14MuPcJl6a93NQVnB8kF+BAzAB0bxdAmH3MhIDeQ2qa9dHvgMeu796woj6D37ETNw0KsL2+jB2vjl1tjieZNCJuyE0VaHLw+zQgJo7QFcm1eE9GNedEwKzS1UKmUW2ichjjK+nOJ8ejQDbpe+WNz3NbZNLH/L9WWPqX9qG2QtCtZo4ZPZICCRsCSMzFEripy/23PkRlUBjOAmbZd6Os30sUcPVLsYQR9Qibo/Z+VU0VYOJcINPucJUAlbT/269+4meQU3MUNi1+lRiYrF7/rqw6PcWv7B0L15fGMlup7t4n66df4ErYPAo3BRF39OIBt5U4G3IukLE1s3h1tiIhUm8HUOxFo2zgWIRUoQUR67hei5/dUTSGIMIcB1K0nwVECyruuyFaBPVf3EA04mk9x2Y5kUnzQiKcxLbqjCDVHcelvRVhetD7kHBJJL+Au1MHBeq3KrFnQs1FQwWmEgK1gBAimHqhMkGH4PX7Z8voRq9c1mW+lHk1He771L5+at7awsbOGN/z2TLGd3DiWbJ5kwe67IPC276RUy6fEYhkIhCiPnJELg9eOpP/RtkZZjbPISwD0J0QS9SGKybSktgEIDJpp10AqyfQ0nHjrSBFZQxf4QtEr0Uzlpk3QpviyvNkMnJvOiefdcMMw5MQWn+vItCj8LW560NvD7i4FkpR+WwrOU6SRB2R7mTmjJrgw105J3+4Hx/z8Et/+V+ErH3l2NBuXkMk4mt/vj9ITW04ejvt3zTyh8/ZcQxs7VBeIFrzsqGRvaq2KFIcza1fm5P2Wl9K6Sanvhmb2bjmZf6mP7t+aLqi+yynL1WmyYsP3SoRiIjPn1SX5kTYNjb6FdUSlyZ8bM5rWPDZHNKMYI3dqn+Ymlo4xVdEXgOiWV+HBRCaNn+3ezJKD5G+q+/nrP/8NbaDDAxrE3oaYzsoH7bUyxlDxXWDm4vr08M8a7axhwyscqcq69FL3wiqTrXGXjqHIopkqtnEmUOEeQoUmHQlvZ6dC7568C3c7DR0mL+qTK2lD+7uqHRHG7dvJ0iBlZkdE7gmKcHVjX6vocSBGsGyYCZ1V7Z6qiwe5tOc39n780XIUSFZ+DVhsyLlupuoGB70w3peZHFFASK1O6HyeuZ9ZdfFPzcOx6Q1461DFVb8sECTiFBLrCw3mGT2dSnXF5k0R5YgDQ1oypj/W34lnwsill6K4gOejE48RvfjrWQy3b5+gdkdzMPgBd6Rxv4aLoncASuWMuClg7RurnvEmj92Ne8MIzl/7ggzEjJNRw965Pj/aUKKchgg0NwBJurLy1XZekiUIBzdg9PTDe1RPl+rFbzHvON3s7+5OMdXzMhkJM6QEvtyhByKI25AQ+72AiZfwuJqXADjPqfS3kmf5dWvwdc3cC7HLPb7Ls/9IP624i0KQbnSBFMDZRTOSnDQoGBSwZvx5L8KH5z2TCDeCgl10CtfAzNWoLaWY01B7bUmYECeT/xn/+tyM4j90ut6z6pG/lR9n7syoed1E8NGReyifcYo5wEsABxHY1HnY2xGdbpmCMmTsTk7hGQPABFIZI/DTpPxk2KQQMWkAitaBZmpIeLcpwzozHRUH6G8sPimfHAKfHLpyj5J9JwF+RpSd5leM64fzDOggAb6dWrBLtBT7Lsrnd4s8dENW4B+RWbS3i15F1Iryz9JSUmhiE40pNUyFrc92na5Pt7e7EZEl5qmUzIyM5uqPrhXnqtWMUlzdEny5k3cpDgc5U3jBnPK4zdu51AB8mCsCZw8Rcjc9XQ/D19j5sJZ1rMusX/hmlR25VeW1lFB0Gnq/7eNC9eHUUuPZ2rKYDuotHZrFSnY+FYLuCt5UIrrmWnBLlbVDGiXGfaSRFh2zkMoadsViwj+0Eed73rrqoKVdqNSnpcaFSalKm0f6qxLq1RezaL80MY5qfOjrsRNv2Zn4xof5eelmr1B4qHhMAQvnXqet5g9YjYtmu+EcJGguQKhbkANBQBT8c139qWNsNSjQpI4Oa2icIvTiOwUyQP1+MUanI15k/a2Hjr05RcNoDU08jqTX7BAmM7Hmm/vJEbK7LIrlurpLx0gXAHDsg3V6FT2GHgSdPqQUwOC25/d1fAZRcChZL5GbPYLFZjY1wrj0rUHcsrO8ADcTZPjLLbaqdJawl0V9HLPE9gxcDkkJ3URFUFt3y6VSPHhhoh8++C4s2Due4P8osu3YWE9vXwBlUCy4+bxdoSWCO+mzu6ohLzDFNwhGXbTSoZrB0FQxkh+9GzXdshACYPO5fimlATRJOR+IlnHWiCiPbhQMCLI7uG3qj2V22qm2nQ3ROfonDzIE5mmgvwkcwdWLbTmSsVRN1N5rN3ZPqGb69NDMW3afYRcX16wLkE1EKCJkyQJQ8jojJI63NRn4KzMoC0Gcglp4bSA5Kj3srUXUDAiwYqp016wbnAffT1F1nAbkWUxGycVf+LIiodfud9+sYpsGepRqtKGS80KJjoLENiqrp8Exsj8CsgkJSvIAPljeWvlUzYbON+E9J8A7tUJXVqC5fml4svcWbCyWQpG1VqundBsvdA8/k93CV3rklIpuCg30C+yrzlFtHSwKFobioSR8INdgSV8PtnDptGAsHr85C+j4gi953csXIHlXd9A1Zzw5+Qy5Fdnqkc7hdJQrqbNOwwoFYpBFfalht8c88hnGKvUbI0w5N14pUDonPKoc03Vo1LlH+tPJFD9B+cIEY9LesncnIY7/URV7U3qiJFKcwdddnsqzTI+qce00IbNzWRLZVTM1c+BoqT6NdikPiD4s5aBjG91luzQL8MfpZ3whhRw1qMHg8GEPcLDMJUoIV/xu5XEAjCqiEtNaDqnnNXP5OQpcH3ft70R/qNYoFQPc/v4AiuBmD02myA3hNnr4JUvg8usFVNfum1LouVTHStXfxN3DVXgZm2B/NtfxY4bi/+6iyTjzn6ovzW3KVTvjx/MleeiD4XexmPGk7Bh6gPl04BlFFC4AvSlOonxz5makyew6lZXDRJKmNU3TwZYubvEJKUYi1JoelUlXsOJTmn5tyxNQ4Z7PqkmhqOhGaf6DF+3H9NVNCUtj55PQj+qisSrVDsM+CtGiN5CJaA/+nAjCd82YMZNnpiZTGHO0JT0L//kABa5HVmBNtwc022NrOtx3nhQjpI+vhfQEAewsZrgBEsN02GVKgI4Fy1Zd9hbcP9w3cqxemh+vS6dhRmDALm3xH7Fx4YdUt2oBpNHcs2ak3izQh/aLAGhjebP6/AVctgDGfK6aZV5smhktAWwSjhGlDfGehUTIdBWzwolqw3EsvN8qwtjmR2YvjjuLcBMCNG8jxylT03/gNLUuv9oGihXn2V3UpCFNwFEX0IycxHVN46OTeLaw5qRQU/2VcsCBNK+vIOFTDNi3ruYqViYlKVEmKViLYlingY2AICXg4yaeXIPvsW+TvCzdiQCFprjctp+9krCTbinIG+kox1wTcL3D1EGc5ouv52MOJxhgGpZdjecf1AMmo/fIIjEElyVIZLArtQhEP8ic293EnLNn9Sq6kgIduWQ2xBHVX3jai5hiaV4qmAyjlHFBAIrtzFOR4oJ4WsDvtIg5DAjzcIb03+6Gop8nzmG4u6M5f3T/P0mN7CHOZq/sgHxaQw35sm9FrrGOQSnEAOiwe4OQ3Tr5n69Wdgjt3DkYXvSvYp9NWnzQ4fPC3PvLxw6wWMhISH1oHbeKOzmU5A95QNsWN2fDBZ4ef8UpfTdThM5F7cg4F1UX8MgGkCuRrxOnqdM+Gs+zAo+Vi4hoPTOXSn5u8YUlQWpF9gnVRy/QGbs+FnYA6egaXWaTdgRiMU/bzfg47TERt0VolvrIaEK8BfqpYxOAd/rkRCrsTzM4UHmMy+dRzbmJNBS7B28WOraLM+nbjfMRS7yU/F1HrA1U3tLLZEDVjvviqc0QNpitUsYyZj+uUbDCaDnBOIcmdK6zhGR9vanmR0A5Pftt0khZcUNNw6IRQDWalXv7OZoWBdjzqEv8dkQF8qeQmu9aEAw2wwjmn/+IKk5fKUrWMWXNitl0+arLTlsMmKsE8aD8vGea1HJLzO3yDqFDR+ZHZaD6ELaqyk0nz5P6G3woVxawLbzJGQDz/TGnA9ctYXD/VTFV6x3zp7oQccnlxgXVMTEZzP6Y9juf6cFYL86EcCcn20G2yL4DM+if7h/77d4yPv72XiMuC6flj6dBBIGo6d3ttapdVwULwydeusH3BjC3TxhU7J4EdHjj0BC021e/DNv9mRhQVfW5Nm6sCdyWZ7YGz7bBphCu99jIi+ivFI0KkQ5RpjDoaSS4diuGnc3rguuRX6c59BBBog7+RAip20YR5dxEUC3eyt59dRmq2wj8OMBBs0nL1rQKVof/8T0yvRwY2cK0lCvMysk2t0BWoZqK5bDrCEcb+qjsjvrNqJYN+Q0H3nF1jioVg4m3+T79oIIh+cBiN/Bi67VopnYZGs2rqFG96SJKjbsHeVnAQBY71XcKx1Cwp+k/7vAv2gWEHzgnRvbi+BcJy+ZaGid0/pK/6byhTto75cKDJMg9lNuMk99wSq5dgHbgZWpIOsDSyFe/yO1cpD669O0uzfrHFQJxJs6W2j0J2Eu9PJKFDKwBPBrl4+0m+PrJa8Fmc3zmzTrNgjlTSB/aZ9fh2QnWNHGbX6ARyJaOLACK2HQnRdPUAq0J5RSG4ao0kJE6Rq6pFAMMw8EzyDCWp532arjU4aGbppl3BXhJOWC/eS/id9+qcwkC44wKHCvh8RF4Ig0JhU62dbCPWidHirffta91hJXits/GMFYbLur7Hmu7efMWJSgbr8Tg9g+R+FkqZrNZSPWA5h6yxZlhCPM2rEyoG5xW7Tj772kJRMw90W7sOiNuGskeiKGxspXdvAPrAL13g+QbRUN0iQ1/C3SAQDMAWXals25H9inv3BDJ99kcGZpR0gDhcCd2177r2pj5crvrPw+pzV8nZuJ3jmkzVWwAtNkLqcAHLsxO6Uf09woCSdI/wQqIR5cc29D+QzbBJQbK5fh76rREBTqygFGBLsU48e46dPBhmsHDGDJMWozXbHZjZ7lNxdU5GTW7fgbvTGUzuEod+1hykCwViWP2KaZ8T1ruIgITTtWaylTVUExx7A3eh6MdVVhPrxfRxwPAwaYmWRDX8pGUnWlUYYIG9rfDcG4Cbb5TYHOSSXY+9pDPJLaBktNQfBMCFBsdO2HJDwDHVZyyDIX4dy8A3UrgtGfqYmEOboO2VCec4TwfGaJLk9ORgUAsMR8idUYgBSPBSt4vlpcri+cbITnorc0epQlWOPfFlNNn6uXoDnD5yhh722eQ3xHC3YOXzxDjLLDlKAsgX8NC+3B+ENNNIxqQc8QONiIZRQ6Wp5QcpzJhK3udBQez7x9tFvB8fkYKiEIm3jFz2mXeTP1absB/+t2IKw4NRX5TKUz8Rr34H8ioNbvsECusKvUH0XLf0vxrktn2LZRYfr13+U3s1cRyBmfDOFHynvP8Z14++FEaW/As8M4XgMZWA8v+z7nVKTe21MPk49s5YOMH2EzkbKVzmKLtM2E3JdzkBcReIZF1RDASh2kgYi7ShPzKMM0sqbqmLFxXDEZ/vuGsBK/sGwVVSsKqlO584gImPADA6cmU0H4dex7aB8z4lFqDmsLo6szLh+jyPDDfXx31Q4niRpQgX1hgDgqRI2v3N2yRTVGYbigTalwBq9xBNOM31p0Ro5vcPYCNVODSVqkZhsNa3Di89I0ZgU+9Q6cxEedns9uToQkSKhpYviDRbVzL99EYggTxbhmW3WMUSYWVKTFpsclX/OUEF2/w3+3IW0O1xHTqnTBV5TTbO9exfs/iJDNNML7Sh9wThVydVUAe+oV2t8vpwvc44E8tVt3VlxEznUOKJfUTmub4W7zzIg/V60j/fbqOF2f2a9XD/Hgg9BATohsGgXZ6WdqyIRkAlzmdRFboWoWLDSy6X/2BN87LSBpYkDnnaOB7IFBtO2luFjHlpp8ActPaRCIZ6scAUp76JZf5N3hgx4KHbyrkY05YTLBDpV9thdLEeIxmrO5rBPyUUY7VZb2wEHcUMGEoGO876Pr5awF5zuyiBR9qX4x6bckJkkLv3rWxeGuQ5/eKSORsGj5rvYHlaX7YLXtmUxNvSIJRFt90cE5NaWghGPgHGfIfEUCLZO5ieltCqDKQbjwsBtrmvg3P9W6z9hTFEofx2tkh/ovXQRzQWG3ly0DhwvHsYY10TscjyeRcW2jdI0kfzvhiqj8uS5HfMxTzJHP8N03W7tC3z6BdYJxFt0KIrKALS67wz5Mo3/+xtgA2l2OpqmgQWM3M/xnvcQX8c93YMKVmXzwRZHjFxL+g9cQNQ4xLrc4jflHxQlVilfP2BGc/Rr0Tsj2MLEcKUoSzVt+vXFzHH3ZxK/V7woGDqcBh7JIl6G9eNrRagedMKCjnQD1aVBhqZFq7fAnj9OUP3jntz3erYtymtdjvdnsa0nOCp09r/0fgsFCvqtM29B6xmdckpmWPkUsJTLA+LLrMDdlqkI+vEwVB8eAYkEzNxjdXEoBylzJ1esCKY2+6vv2zvRWfLw0ttkff2QIEH0RksQxDMQHYq/KKhRylh2hVOsFjD+Oj3QIuN/4+IkbUDLGTR5jXVtZxmEadZ+SnK4/bSNRV1hrwIhFbhHrpBX0nGkwSuGRMfuOwQeFW5z6fLsKwe3REJJpQej+2iaGSzv80o35Z8BLIqbtUAUXFTHf9Vfa7RAqZQQy+MA41v/nxlPwECBX5t6biUDReTxLzZ5kw1WtqkImGvQHGvfbEwE+t8UCOD634x6OnPORvdR0KlZLeWrZ6lGvzrAOz2sWIWf1n+Gi8m9dBtWYs/7RPgD4ldkDKn1s2UkDg6bTOJObulgZ6AF31epHiOYPr06XU0Lzhlb9Q0TfC/lw+cUyYmYTfj7Pzj8u0jRHCQ0Vn8JtqTxo2EhDUIvpEFzFV47qUGdoIbwHRYdnZLr1pggaQz/xtwm/rCDfYwz00ZtC/cjQNs3u7LFDnLbhiXpfGp3IVGZHxJ3JpibtlyGkJEywMjBWQRP9cGUJeSyX+k0Fqzb32YFDQ7djgs4rMBo9X8r62wV+AbHi9s45uUMT7Yt0MFxXfF8lEtkZINdZe728HbdEUd6Ixa0/bM86BNGvZWU3PuGQ1vH2p3xFn5qFhYfjlw9xQC5dLvt2YQMQ/NxGJgLCU6rwXdt179uSsmTx2aDtMQL99aoTqO9C7xmuDCIyvHAx9nszKpNpNGIAptHYBITuB5N0kYpKsxdHakCTYtWVTTzvGNQaJMkiY7WlOG+x/TzO2YSA5J+Unrb0ZNQ722/cK78i0L9gjr1Ix1U+a0xO1D0ZeIEaipQpvKMkw=","catalogue_think_content":"WikiEncrypted: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","recovery_checkpoint":"wiki_generation_completed","last_commit_id":"0000000000000000000000000000000000000000","last_commit_update":"2026-04-22T19:25:18.956233+08:00","gmt_create":"2026-04-22T17:58:32.591608+08:00","gmt_modified":"2026-04-22T19:25:18.956233+08:00","extend_info":"{\"language\":\"zh\",\"active\":true,\"branch\":\"Q0DeR-MaG1C-BrAnCh-Fo2-NoN-GiT-Or-No-C0MmIt\",\"shareStatus\":\"\",\"server_error_code\":\"\",\"cosy_version\":\"\"}"}} \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..c8e2af5 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,40 @@ +FROM python:3.11-slim + +WORKDIR /app + +# 安装系统依赖(Playwright需要) +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl \ + wget \ + gnupg \ + libglib2.0-0 \ + libnss3 \ + libnspr4 \ + libatk1.0-0 \ + libatk-bridge2.0-0 \ + libcups2 \ + libdrm2 \ + libxkbcommon0 \ + libxcomposite1 \ + libxdamage1 \ + libxrandr2 \ + libgbm1 \ + libpango-1.0-0 \ + libcairo2 \ + libasound2 \ + && rm -rf /var/lib/apt/lists/* + +# 复制并安装Python依赖 +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# 安装Playwright浏览器 +RUN playwright install chromium +RUN playwright install-deps chromium + +# 复制应用代码 +COPY . . + +EXPOSE 8000 + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/backend/alembic.ini b/backend/alembic.ini new file mode 100644 index 0000000..79339d6 --- /dev/null +++ b/backend/alembic.ini @@ -0,0 +1,149 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts. +# this is typically a path given in POSIX (e.g. forward slashes) +# format, relative to the token %(here)s which refers to the location of this +# ini file +script_location = %(here)s/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 +# Or organize into date-based subdirectories (requires recursive_version_locations = true) +# file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. for multiple paths, the path separator +# is defined by "path_separator" below. +prepend_sys_path = . + + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the tzdata library which can be 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 /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 "path_separator" +# below. +# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions + +# path_separator; This indicates what character is used to split lists of file +# paths, including version_locations and prepend_sys_path within configparser +# files such as alembic.ini. +# The default rendered in new alembic.ini files is "os", which uses os.pathsep +# to provide os-dependent path splitting. +# +# Note that in order to support legacy alembic.ini files, this default does NOT +# take place if path_separator is not present in alembic.ini. If this +# option is omitted entirely, fallback logic is as follows: +# +# 1. Parsing of the version_locations option falls back to using the legacy +# "version_path_separator" key, which if absent then falls back to the legacy +# behavior of splitting on spaces and/or commas. +# 2. Parsing of the prepend_sys_path option falls back to the legacy +# behavior of splitting on spaces, commas, or colons. +# +# Valid values for path_separator are: +# +# path_separator = : +# path_separator = ; +# path_separator = space +# path_separator = newline +# +# Use os.pathsep. Default configuration used for new projects. +path_separator = os + +# 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 + +# database URL. This is consumed by the user-maintained env.py script only. +# other means of configuring database URLs may be customized within the env.py +# file. +sqlalchemy.url = postgresql+asyncpg://postgres:postgres123@db:5432/geo_platform + + +[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 module runner, against the "ruff" module +# hooks = ruff +# ruff.type = module +# ruff.module = ruff +# ruff.options = check --fix REVISION_SCRIPT_FILENAME + +# Alternatively, use the exec runner to execute a binary found on your PATH +# hooks = ruff +# ruff.type = exec +# ruff.executable = ruff +# ruff.options = check --fix REVISION_SCRIPT_FILENAME + +# Logging configuration. This is also consumed by the user-maintained +# env.py script only. +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARNING +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARNING +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..461c9c0 --- /dev/null +++ b/backend/alembic/env.py @@ -0,0 +1,88 @@ +import asyncio +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 + +from app.config import settings +from app.database import Base +from app.models import * # noqa: F401, F403 + +# 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 + +# 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 = settings.DATABASE_URL + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def do_run_migrations(connection: Connection) -> None: + context.configure(connection=connection, target_metadata=target_metadata) + + with context.begin_transaction(): + context.run_migrations() + + +async def run_async_migrations() -> None: + """In this scenario we need to create an Engine + and associate a connection with the context. + + """ + 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..1101630 --- /dev/null +++ b/backend/alembic/script.py.mako @@ -0,0 +1,28 @@ +"""${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, Sequence[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: + """Upgrade schema.""" + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + """Downgrade schema.""" + ${downgrades if downgrades else "pass"} diff --git a/backend/alembic/versions/488d0bd5ab01_initial_migration.py b/backend/alembic/versions/488d0bd5ab01_initial_migration.py new file mode 100644 index 0000000..fc059db --- /dev/null +++ b/backend/alembic/versions/488d0bd5ab01_initial_migration.py @@ -0,0 +1,127 @@ +"""Initial migration + +Revision ID: 488d0bd5ab01 +Revises: +Create Date: 2026-04-22 18:06:46.629263 + +""" +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 = '488d0bd5ab01' +down_revision: Union[str, Sequence[str], None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # users table + op.create_table( + 'users', + sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('gen_random_uuid()'), nullable=False), + sa.Column('email', sa.String(255), nullable=False), + sa.Column('password_hash', sa.String(255), nullable=False), + sa.Column('name', sa.String(100), nullable=True), + sa.Column('plan', sa.String(20), server_default='free', nullable=False), + sa.Column('max_queries', sa.Integer(), server_default='5', nullable=False), + sa.Column('is_active', sa.Boolean(), server_default='true', nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('NOW()'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('NOW()'), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('email') + ) + + # queries table + op.create_table( + 'queries', + sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('gen_random_uuid()'), nullable=False), + sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('keyword', sa.String(200), nullable=False), + sa.Column('target_brand', sa.String(100), nullable=False), + sa.Column('brand_aliases', postgresql.JSONB(), server_default='[]', nullable=False), + sa.Column('platforms', postgresql.JSONB(), server_default='["wenxin", "kimi"]', nullable=False), + sa.Column('frequency', sa.String(20), server_default='weekly', nullable=False), + sa.Column('status', sa.String(20), server_default='active', nullable=False), + sa.Column('last_queried_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('next_query_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('NOW()'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('NOW()'), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE') + ) + op.create_index('idx_queries_user_id', 'queries', ['user_id']) + op.create_index('idx_queries_status', 'queries', ['status']) + op.create_index('idx_queries_next_query_at', 'queries', ['next_query_at']) + + # citation_records table + op.create_table( + 'citation_records', + sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('gen_random_uuid()'), nullable=False), + sa.Column('query_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('platform', sa.String(50), nullable=False), + sa.Column('cited', sa.Boolean(), server_default='false', nullable=False), + sa.Column('citation_position', sa.Integer(), nullable=True), + sa.Column('citation_text', sa.Text(), nullable=True), + sa.Column('competitor_brands', postgresql.JSONB(), server_default='[]', nullable=False), + sa.Column('raw_response', sa.Text(), nullable=True), + sa.Column('queried_at', sa.DateTime(timezone=True), server_default=sa.text('NOW()'), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.ForeignKeyConstraint(['query_id'], ['queries.id'], ondelete='CASCADE') + ) + op.create_index('idx_citation_records_query_id', 'citation_records', ['query_id']) + op.create_index('idx_citation_records_queried_at', 'citation_records', ['queried_at']) + op.create_index('idx_citation_records_platform', 'citation_records', ['platform']) + + # query_tasks table + op.create_table( + 'query_tasks', + sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('gen_random_uuid()'), nullable=False), + sa.Column('query_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('platform', sa.String(50), nullable=False), + sa.Column('status', sa.String(20), server_default='pending', nullable=False), + sa.Column('error_message', sa.Text(), nullable=True), + sa.Column('scheduled_at', sa.DateTime(timezone=True), server_default=sa.text('NOW()'), nullable=False), + sa.Column('started_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.ForeignKeyConstraint(['query_id'], ['queries.id'], ondelete='CASCADE') + ) + op.create_index('idx_query_tasks_status', 'query_tasks', ['status']) + + # subscriptions table + op.create_table( + 'subscriptions', + sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('gen_random_uuid()'), nullable=False), + sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('plan', sa.String(20), nullable=False), + sa.Column('status', sa.String(20), server_default='active', nullable=False), + sa.Column('start_date', sa.Date(), nullable=False), + sa.Column('end_date', sa.Date(), nullable=False), + sa.Column('amount', sa.Numeric(10, 2), nullable=True), + sa.Column('payment_method', sa.String(50), nullable=True), + sa.Column('payment_id', sa.String(255), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('NOW()'), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE') + ) + + +def downgrade() -> None: + """Downgrade schema.""" + op.drop_table('subscriptions') + op.drop_index('idx_query_tasks_status', table_name='query_tasks') + op.drop_table('query_tasks') + op.drop_index('idx_citation_records_platform', table_name='citation_records') + op.drop_index('idx_citation_records_queried_at', table_name='citation_records') + op.drop_index('idx_citation_records_query_id', table_name='citation_records') + op.drop_table('citation_records') + op.drop_index('idx_queries_next_query_at', table_name='queries') + op.drop_index('idx_queries_status', table_name='queries') + op.drop_index('idx_queries_user_id', table_name='queries') + op.drop_table('queries') + op.drop_table('users') diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py new file mode 100644 index 0000000..f260b90 --- /dev/null +++ b/backend/app/api/__init__.py @@ -0,0 +1,3 @@ +from app.api.auth import router as auth_router + +__all__ = ["auth_router"] diff --git a/backend/app/api/auth.py b/backend/app/api/auth.py new file mode 100644 index 0000000..9b5b6b1 --- /dev/null +++ b/backend/app/api/auth.py @@ -0,0 +1,42 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.ext.asyncio import AsyncSession + +from app.api.deps import get_current_user +from app.database import get_db +from app.models.user import User +from app.schemas.auth import TokenResponse, UserLogin, UserRegister, UserResponse +from app.services.auth import authenticate_user, create_access_token, register_user + +router = APIRouter() + + +@router.post("/register", response_model=UserResponse, status_code=status.HTTP_201_CREATED) +async def register(user_data: UserRegister, db: AsyncSession = Depends(get_db)): + try: + user = await register_user(db, user_data) + except ValueError as e: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e) if str(e) else "邮箱已被注册") + return user + + +@router.post("/login", response_model=TokenResponse) +async def login(user_data: UserLogin, db: AsyncSession = Depends(get_db)): + user = await authenticate_user(db, user_data.email, user_data.password) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="邮箱或密码错误", + headers={"WWW-Authenticate": "Bearer"}, + ) + + access_token = create_access_token(data={"sub": str(user.id)}) + return { + "access_token": access_token, + "token_type": "bearer", + "user": user, + } + + +@router.get("/me", response_model=UserResponse) +async def read_current_user(current_user: User = Depends(get_current_user)): + return current_user diff --git a/backend/app/api/citations.py b/backend/app/api/citations.py new file mode 100644 index 0000000..10e5eb0 --- /dev/null +++ b/backend/app/api/citations.py @@ -0,0 +1,77 @@ +import uuid +from datetime import datetime + +from fastapi import APIRouter, Depends, HTTPException, Query, status +from sqlalchemy.ext.asyncio import AsyncSession + +from app.api.deps import get_current_user +from app.database import get_db +from app.models.user import User +from app.schemas.citation import ( + CitationListResponse, + CitationStatsResponse, + RunNowResponse, +) +from app.services.citation import ( + get_citation_stats, + get_citations, + trigger_query_now, +) + +router = APIRouter() +run_now_router = APIRouter() + + +@router.get("/", response_model=CitationListResponse) +async def list_citations( + query_id: uuid.UUID | None = Query(None), + platform: str | None = Query(None), + start_date: datetime | None = Query(None), + end_date: datetime | None = Query(None), + skip: int = Query(0, ge=0), + limit: int = Query(20, ge=1, le=100), + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user), +): + items, total = await get_citations( + db, + current_user.id, + query_id=query_id, + platform=platform, + start_date=start_date, + end_date=end_date, + skip=skip, + limit=limit, + ) + return {"items": items, "total": total} + + +@router.get("/stats", response_model=CitationStatsResponse) +async def citation_stats( + query_id: uuid.UUID | None = Query(None), + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user), +): + stats = await get_citation_stats(db, current_user.id, query_id=query_id) + return stats + + +@run_now_router.post("/{query_id}/run-now", response_model=RunNowResponse, status_code=status.HTTP_202_ACCEPTED) +async def run_query_now( + query_id: uuid.UUID, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user), +): + try: + task = await trigger_query_now(db, current_user.id, query_id) + except ValueError as e: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=str(e), + ) + + return { + "task_id": task.id, + "status": task.status, + "message": "查询任务已加入队列", + } diff --git a/backend/app/api/deps.py b/backend/app/api/deps.py new file mode 100644 index 0000000..fd1336c --- /dev/null +++ b/backend/app/api/deps.py @@ -0,0 +1,42 @@ +import uuid + +from fastapi import Depends, HTTPException, status +from fastapi.security import OAuth2PasswordBearer +from jose import JWTError +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.database import get_db +from app.models.user import User +from app.services.auth import verify_token + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login") + + +async def get_current_user( + token: str = Depends(oauth2_scheme), + db: AsyncSession = Depends(get_db), +) -> User: + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + + try: + payload = verify_token(token) + user_id: str | None = payload.get("sub") + if user_id is None: + raise credentials_exception + user_uuid = uuid.UUID(user_id) + except (JWTError, ValueError): + raise credentials_exception + + stmt = select(User).where(User.id == user_uuid) + result = await db.execute(stmt) + user = result.scalar_one_or_none() + + if user is None: + raise credentials_exception + + return user diff --git a/backend/app/api/queries.py b/backend/app/api/queries.py new file mode 100644 index 0000000..35643e1 --- /dev/null +++ b/backend/app/api/queries.py @@ -0,0 +1,85 @@ +import uuid + +from fastapi import APIRouter, Depends, HTTPException, Query, status +from sqlalchemy.ext.asyncio import AsyncSession + +from app.api.deps import get_current_user +from app.database import get_db +from app.models.user import User +from app.schemas.query import QueryCreate, QueryListResponse, QueryResponse, QueryUpdate +from app.services.query import create_query, delete_query, get_queries, get_query, update_query + +router = APIRouter() + + +@router.get("/", response_model=QueryListResponse) +async def list_queries( + skip: int = Query(0, ge=0), + limit: int = Query(20, ge=1, le=100), + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user), +): + items, total = await get_queries(db, current_user.id, skip=skip, limit=limit) + return {"items": items, "total": total} + + +@router.post("/", response_model=QueryResponse, status_code=status.HTTP_201_CREATED) +async def create_new_query( + query_data: QueryCreate, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user), +): + try: + query = await create_query(db, current_user.id, query_data) + except PermissionError as e: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=str(e) if str(e) else "查询词数量已达上限", + ) + return query + + +@router.get("/{query_id}", response_model=QueryResponse) +async def retrieve_query( + query_id: uuid.UUID, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user), +): + query = await get_query(db, query_id, current_user.id) + if query is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="查询词不存在", + ) + return query + + +@router.put("/{query_id}", response_model=QueryResponse) +async def modify_query( + query_id: uuid.UUID, + update_data: QueryUpdate, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user), +): + query = await update_query(db, query_id, current_user.id, update_data) + if query is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="查询词不存在", + ) + return query + + +@router.delete("/{query_id}", status_code=status.HTTP_204_NO_CONTENT) +async def remove_query( + query_id: uuid.UUID, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user), +): + deleted = await delete_query(db, query_id, current_user.id) + if not deleted: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="查询词不存在", + ) + return None diff --git a/backend/app/api/reports.py b/backend/app/api/reports.py new file mode 100644 index 0000000..faaf5f1 --- /dev/null +++ b/backend/app/api/reports.py @@ -0,0 +1,46 @@ +import uuid +from datetime import datetime + +from fastapi import APIRouter, Depends, HTTPException, Query, status +from fastapi.responses import StreamingResponse +from sqlalchemy.ext.asyncio import AsyncSession + +from app.api.deps import get_current_user +from app.database import get_db +from app.models.user import User +from app.services.citation import export_citations_csv + +router = APIRouter() + + +@router.get("/export/csv") +async def export_report( + query_id: uuid.UUID = Query(...), + format: str = Query("csv"), + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user), +): + if format != "csv": + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Only CSV format is supported", + ) + + try: + csv_content = await export_citations_csv(db, current_user.id, query_id) + except ValueError as e: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=str(e), + ) + + date_str = datetime.now().strftime("%Y%m%d") + filename = f"geo-report-{date_str}.csv" + + return StreamingResponse( + iter([csv_content]), + media_type="text/csv", + headers={ + "Content-Disposition": f'attachment; filename="{filename}"', + }, + ) diff --git a/backend/app/config.py b/backend/app/config.py new file mode 100644 index 0000000..d8ffbd8 --- /dev/null +++ b/backend/app/config.py @@ -0,0 +1,22 @@ +from pathlib import Path +from pydantic_settings import BaseSettings, SettingsConfigDict + +_env_path = Path(__file__).resolve().parent.parent.parent / ".env" +if not _env_path.exists(): + _env_path = Path(".env") + + +class Settings(BaseSettings): + model_config = SettingsConfigDict(env_file=str(_env_path), extra="ignore") + + DATABASE_URL: str = "postgresql+asyncpg://postgres:postgres123@db:5432/geo_platform" + REDIS_URL: str = "redis://redis:6379/0" + JWT_SECRET: str = "your-secret-key-change-in-production" + JWT_EXPIRE_HOURS: int = 24 + PLAYWRIGHT_BROWSERS_PATH: str = "/ms-playwright" + ZHIPU_API_KEY: str = "" + TONGYI_API_KEY: str = "" + CORS_ORIGINS: str = "http://localhost:3000" + + +settings = Settings() diff --git a/backend/app/database.py b/backend/app/database.py new file mode 100644 index 0000000..bb4ba82 --- /dev/null +++ b/backend/app/database.py @@ -0,0 +1,28 @@ +from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession +from sqlalchemy.orm import declarative_base + +from app.config import settings + +engine = create_async_engine( + settings.DATABASE_URL, + echo=False, + future=True, +) + +AsyncSessionLocal = async_sessionmaker( + engine, + class_=AsyncSession, + expire_on_commit=False, + autoflush=False, + autocommit=False, +) + +Base = declarative_base() + + +async def get_db() -> AsyncSession: + async with AsyncSessionLocal() as session: + try: + yield session + finally: + await session.close() diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..ee471b0 --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,56 @@ +from contextlib import asynccontextmanager + +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from app.api.auth import router as auth_router +from app.api.citations import router as citations_router, run_now_router +from app.api.queries import router as queries_router +from app.api.reports import router as reports_router +from app.config import settings +from app.database import engine, Base +from app.workers.scheduler import query_scheduler + + +@asynccontextmanager +async def lifespan(app: FastAPI): + import app.models + + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + + query_scheduler.start() + + yield + + await query_scheduler.shutdown() + + +app = FastAPI( + title="GEO Platform API", + version="1.0.0", + lifespan=lifespan, +) + +_allow_origins = [origin.strip() for origin in settings.CORS_ORIGINS.split(",") if origin.strip()] +if not _allow_origins: + _allow_origins = ["http://localhost:3000"] + +app.add_middleware( + CORSMiddleware, + allow_origins=_allow_origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +app.include_router(auth_router, prefix="/api/v1/auth", tags=["认证"]) +app.include_router(queries_router, prefix="/api/v1/queries", tags=["查询词"]) +app.include_router(citations_router, prefix="/api/v1/citations", tags=["引用数据"]) +app.include_router(reports_router, prefix="/api/v1/reports", tags=["报告"]) +app.include_router(run_now_router, prefix="/api/v1/queries", tags=["查询词"]) + + +@app.get("/health") +async def health_check(): + return {"status": "ok"} diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py new file mode 100644 index 0000000..e27177b --- /dev/null +++ b/backend/app/models/__init__.py @@ -0,0 +1,13 @@ +from app.models.user import User +from app.models.query import Query +from app.models.citation_record import CitationRecord +from app.models.query_task import QueryTask +from app.models.subscription import Subscription + +__all__ = [ + "User", + "Query", + "CitationRecord", + "QueryTask", + "Subscription", +] diff --git a/backend/app/models/citation_record.py b/backend/app/models/citation_record.py new file mode 100644 index 0000000..bae59e3 --- /dev/null +++ b/backend/app/models/citation_record.py @@ -0,0 +1,41 @@ +import uuid +from datetime import datetime + +from sqlalchemy import String, Boolean, Integer, ForeignKey, Index, func, Text +from sqlalchemy import Uuid, JSON +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.database import Base + + +class CitationRecord(Base): + __tablename__ = "citation_records" + + id: Mapped[uuid.UUID] = mapped_column( + Uuid(as_uuid=True), + primary_key=True, + default=uuid.uuid4, + ) + query_id: Mapped[uuid.UUID] = mapped_column( + Uuid(as_uuid=True), + ForeignKey("queries.id", ondelete="CASCADE"), + nullable=False, + ) + platform: Mapped[str] = mapped_column(String(50), nullable=False) + cited: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) + citation_position: Mapped[int | None] = mapped_column(Integer, nullable=True) + citation_text: Mapped[str | None] = mapped_column(Text, nullable=True) + competitor_brands: Mapped[list] = mapped_column(JSON, default=list) + raw_response: Mapped[str | None] = mapped_column(Text, nullable=True) + queried_at: Mapped[datetime] = mapped_column( + server_default=func.now(), + nullable=False, + ) + + query: Mapped["Query"] = relationship("Query", back_populates="citation_records") + + __table_args__ = ( + Index("idx_citation_records_query_id", "query_id"), + Index("idx_citation_records_queried_at", "queried_at"), + Index("idx_citation_records_platform", "platform"), + ) diff --git a/backend/app/models/query.py b/backend/app/models/query.py new file mode 100644 index 0000000..2870e9a --- /dev/null +++ b/backend/app/models/query.py @@ -0,0 +1,54 @@ +import uuid +from datetime import datetime + +from sqlalchemy import String, ForeignKey, Index, func +from sqlalchemy import Uuid, JSON +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.database import Base + + +class Query(Base): + __tablename__ = "queries" + + id: Mapped[uuid.UUID] = mapped_column( + Uuid(as_uuid=True), + primary_key=True, + default=uuid.uuid4, + ) + user_id: Mapped[uuid.UUID] = mapped_column( + Uuid(as_uuid=True), + ForeignKey("users.id", ondelete="CASCADE"), + nullable=False, + ) + keyword: Mapped[str] = mapped_column(String(200), nullable=False) + target_brand: Mapped[str] = mapped_column(String(100), nullable=False) + brand_aliases: Mapped[list] = mapped_column(JSON, default=list) + platforms: Mapped[list] = mapped_column(JSON, nullable=False, default=lambda: ["wenxin", "kimi"]) + frequency: Mapped[str] = mapped_column(String(20), default="weekly") + status: Mapped[str] = mapped_column(String(20), default="active") + last_queried_at: Mapped[datetime | None] = mapped_column(nullable=True) + next_query_at: Mapped[datetime | None] = mapped_column(nullable=True) + created_at: Mapped[datetime] = mapped_column( + server_default=func.now(), + nullable=False, + ) + updated_at: Mapped[datetime] = mapped_column( + server_default=func.now(), + onupdate=func.now(), + nullable=False, + ) + + user: Mapped["User"] = relationship("User", back_populates="queries") + citation_records: Mapped[list["CitationRecord"]] = relationship( + "CitationRecord", back_populates="query", cascade="all, delete-orphan" + ) + query_tasks: Mapped[list["QueryTask"]] = relationship( + "QueryTask", back_populates="query", cascade="all, delete-orphan" + ) + + __table_args__ = ( + Index("idx_queries_user_id", "user_id"), + Index("idx_queries_status", "status"), + Index("idx_queries_next_query_at", "next_query_at"), + ) diff --git a/backend/app/models/query_task.py b/backend/app/models/query_task.py new file mode 100644 index 0000000..2fd2285 --- /dev/null +++ b/backend/app/models/query_task.py @@ -0,0 +1,38 @@ +import uuid +from datetime import datetime + +from sqlalchemy import String, ForeignKey, Index, func, Text +from sqlalchemy import Uuid +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.database import Base + + +class QueryTask(Base): + __tablename__ = "query_tasks" + + id: Mapped[uuid.UUID] = mapped_column( + Uuid(as_uuid=True), + primary_key=True, + default=uuid.uuid4, + ) + query_id: Mapped[uuid.UUID] = mapped_column( + Uuid(as_uuid=True), + ForeignKey("queries.id", ondelete="CASCADE"), + nullable=False, + ) + platform: Mapped[str] = mapped_column(String(50), nullable=False) + status: Mapped[str] = mapped_column(String(20), default="pending") + error_message: Mapped[str | None] = mapped_column(Text, nullable=True) + scheduled_at: Mapped[datetime] = mapped_column( + server_default=func.now(), + nullable=False, + ) + started_at: Mapped[datetime | None] = mapped_column(nullable=True) + completed_at: Mapped[datetime | None] = mapped_column(nullable=True) + + query: Mapped["Query"] = relationship("Query", back_populates="query_tasks") + + __table_args__ = ( + Index("idx_query_tasks_status", "status"), + ) diff --git a/backend/app/models/subscription.py b/backend/app/models/subscription.py new file mode 100644 index 0000000..83daa54 --- /dev/null +++ b/backend/app/models/subscription.py @@ -0,0 +1,36 @@ +import uuid +from datetime import datetime, date + +from sqlalchemy import String, ForeignKey, Numeric, func +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.database import Base + + +class Subscription(Base): + __tablename__ = "subscriptions" + + id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), + primary_key=True, + default=uuid.uuid4, + ) + user_id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), + ForeignKey("users.id", ondelete="CASCADE"), + nullable=False, + ) + plan: Mapped[str] = mapped_column(String(20), nullable=False) + status: Mapped[str] = mapped_column(String(20), default="active") + start_date: Mapped[date] = mapped_column(nullable=False) + end_date: Mapped[date] = mapped_column(nullable=False) + amount: Mapped[float | None] = mapped_column(Numeric(10, 2), nullable=True) + payment_method: Mapped[str | None] = mapped_column(String(50), nullable=True) + payment_id: Mapped[str | None] = mapped_column(String(255), nullable=True) + created_at: Mapped[datetime] = mapped_column( + server_default=func.now(), + nullable=False, + ) + + user: Mapped["User"] = relationship("User", back_populates="subscriptions") diff --git a/backend/app/models/user.py b/backend/app/models/user.py new file mode 100644 index 0000000..b5d19d7 --- /dev/null +++ b/backend/app/models/user.py @@ -0,0 +1,40 @@ +import uuid +from datetime import datetime, timezone + +from sqlalchemy import String, Boolean, Integer, func +from sqlalchemy import Uuid +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.database import Base + + +class User(Base): + __tablename__ = "users" + + id: Mapped[uuid.UUID] = mapped_column( + Uuid(as_uuid=True), + primary_key=True, + default=uuid.uuid4, + ) + email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False) + password_hash: Mapped[str] = mapped_column(String(255), nullable=False) + name: Mapped[str | None] = mapped_column(String(100), nullable=True) + plan: Mapped[str] = mapped_column(String(20), default="free") + max_queries: Mapped[int] = mapped_column(Integer, default=5) + is_active: Mapped[bool] = mapped_column(Boolean, default=True) + created_at: Mapped[datetime] = mapped_column( + server_default=func.now(), + nullable=False, + ) + updated_at: Mapped[datetime] = mapped_column( + server_default=func.now(), + onupdate=func.now(), + nullable=False, + ) + + queries: Mapped[list["Query"]] = relationship( + "Query", back_populates="user", cascade="all, delete-orphan" + ) + subscriptions: Mapped[list["Subscription"]] = relationship( + "Subscription", back_populates="user", cascade="all, delete-orphan" + ) diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/schemas/auth.py b/backend/app/schemas/auth.py new file mode 100644 index 0000000..b93346d --- /dev/null +++ b/backend/app/schemas/auth.py @@ -0,0 +1,33 @@ +import uuid +from datetime import datetime + +from pydantic import BaseModel, EmailStr, Field + + +class UserRegister(BaseModel): + email: EmailStr + password: str = Field(min_length=8) + name: str = Field(min_length=1, max_length=100) + + +class UserLogin(BaseModel): + email: EmailStr + password: str + + +class UserResponse(BaseModel): + id: uuid.UUID + email: str + name: str | None + plan: str + max_queries: int + is_active: bool + created_at: datetime + + model_config = {"from_attributes": True} + + +class TokenResponse(BaseModel): + access_token: str + token_type: str + user: UserResponse diff --git a/backend/app/schemas/citation.py b/backend/app/schemas/citation.py new file mode 100644 index 0000000..8891a5e --- /dev/null +++ b/backend/app/schemas/citation.py @@ -0,0 +1,49 @@ +import uuid +from datetime import datetime + +from pydantic import BaseModel + + +class CitationResponse(BaseModel): + id: uuid.UUID + query_id: uuid.UUID + platform: str + cited: bool + citation_position: int | None + citation_text: str | None + competitor_brands: list[str] + queried_at: datetime + + model_config = {"from_attributes": True} + + +class CitationListResponse(BaseModel): + items: list[CitationResponse] + total: int + + +class PlatformStats(BaseModel): + queries: int + citations: int + rate: float + avg_position: float | None + + +class TrendItem(BaseModel): + date: str + citations: int + + +class CitationStatsResponse(BaseModel): + total_queries: int + total_citations: int + citation_rate: float + avg_position: float | None + by_platform: dict[str, PlatformStats] + trend: list[TrendItem] + + +class RunNowResponse(BaseModel): + task_id: uuid.UUID + status: str + message: str diff --git a/backend/app/schemas/query.py b/backend/app/schemas/query.py new file mode 100644 index 0000000..11e503d --- /dev/null +++ b/backend/app/schemas/query.py @@ -0,0 +1,93 @@ +import uuid +from datetime import datetime + +from pydantic import BaseModel, Field, field_validator + +VALID_PLATFORMS = {"wenxin", "kimi", "tongyi", "baidu_ai", "yuanbao", "qingyan"} +VALID_FREQUENCIES = {"daily", "weekly"} +VALID_STATUSES = {"active", "paused", "disabled"} + + +class QueryCreate(BaseModel): + keyword: str = Field(min_length=1, max_length=200) + target_brand: str = Field(min_length=1, max_length=100) + brand_aliases: list[str] | None = None + platforms: list[str] + frequency: str = "weekly" + + @field_validator("platforms") + @classmethod + def validate_platforms(cls, v: list[str]) -> list[str]: + if not v: + raise ValueError("platforms cannot be empty") + invalid = set(v) - VALID_PLATFORMS + if invalid: + raise ValueError(f"invalid platforms: {', '.join(invalid)}") + return v + + @field_validator("frequency") + @classmethod + def validate_frequency(cls, v: str) -> str: + if v not in VALID_FREQUENCIES: + raise ValueError(f"frequency must be one of {VALID_FREQUENCIES}") + return v + + +class QueryUpdate(BaseModel): + keyword: str | None = Field(default=None, min_length=1, max_length=200) + target_brand: str | None = Field(default=None, min_length=1, max_length=100) + brand_aliases: list[str] | None = None + platforms: list[str] | None = None + frequency: str | None = None + status: str | None = None + + @field_validator("platforms") + @classmethod + def validate_platforms(cls, v: list[str] | None) -> list[str] | None: + if v is None: + return v + if not v: + raise ValueError("platforms cannot be empty") + invalid = set(v) - VALID_PLATFORMS + if invalid: + raise ValueError(f"invalid platforms: {', '.join(invalid)}") + return v + + @field_validator("frequency") + @classmethod + def validate_frequency(cls, v: str | None) -> str | None: + if v is None: + return v + if v not in VALID_FREQUENCIES: + raise ValueError(f"frequency must be one of {VALID_FREQUENCIES}") + return v + + @field_validator("status") + @classmethod + def validate_status(cls, v: str | None) -> str | None: + if v is None: + return v + if v not in VALID_STATUSES: + raise ValueError(f"status must be one of {VALID_STATUSES}") + return v + + +class QueryResponse(BaseModel): + id: uuid.UUID + keyword: str + target_brand: str + brand_aliases: list + platforms: list + frequency: str + status: str + last_queried_at: datetime | None + next_query_at: datetime | None + created_at: datetime + updated_at: datetime + + model_config = {"from_attributes": True} + + +class QueryListResponse(BaseModel): + items: list[QueryResponse] + total: int diff --git a/backend/app/services/__init__.py b/backend/app/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/services/auth.py b/backend/app/services/auth.py new file mode 100644 index 0000000..827c4b1 --- /dev/null +++ b/backend/app/services/auth.py @@ -0,0 +1,68 @@ +import uuid +from datetime import datetime, timedelta, timezone + +from jose import jwt, JWTError +from passlib.context import CryptContext +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.config import settings +from app.models.user import User +from app.schemas.auth import UserRegister + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + + +def hash_password(password: str) -> str: + return pwd_context.hash(password) + + +def verify_password(plain_password: str, hashed_password: str) -> bool: + return pwd_context.verify(plain_password, hashed_password) + + +def create_access_token(data: dict) -> str: + to_encode = data.copy() + expire = datetime.now(timezone.utc) + timedelta(hours=settings.JWT_EXPIRE_HOURS) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, settings.JWT_SECRET, algorithm="HS256") + return encoded_jwt + + +def verify_token(token: str) -> dict: + payload = jwt.decode(token, settings.JWT_SECRET, algorithms=["HS256"]) + return payload + + +async def register_user(db: AsyncSession, user_data: UserRegister) -> User: + stmt = select(User).where(User.email == user_data.email) + result = await db.execute(stmt) + existing_user = result.scalar_one_or_none() + if existing_user: + raise ValueError("邮箱已被注册") + + user = User( + email=user_data.email, + password_hash=hash_password(user_data.password), + name=user_data.name, + ) + db.add(user) + await db.commit() + await db.refresh(user) + return user + + +async def authenticate_user( + db: AsyncSession, email: str, password: str +) -> User | None: + stmt = select(User).where(User.email == email) + result = await db.execute(stmt) + user = result.scalar_one_or_none() + + if not user: + return None + + if not verify_password(password, user.password_hash): + return None + + return user diff --git a/backend/app/services/citation.py b/backend/app/services/citation.py new file mode 100644 index 0000000..42d8f26 --- /dev/null +++ b/backend/app/services/citation.py @@ -0,0 +1,277 @@ +import csv +import io +import uuid +from datetime import datetime, timedelta, timezone + +from sqlalchemy import func, select, and_, cast, Integer +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models.citation_record import CitationRecord +from app.models.query import Query +from app.models.query_task import QueryTask + + +async def _verify_query_ownership( + db: AsyncSession, + query_id: uuid.UUID, + user_id: uuid.UUID, +) -> Query | None: + stmt = select(Query).where(Query.id == query_id, Query.user_id == user_id) + result = await db.execute(stmt) + return result.scalar_one_or_none() + + +async def get_citations( + db: AsyncSession, + user_id: uuid.UUID, + query_id: uuid.UUID | None = None, + platform: str | None = None, + start_date: datetime | None = None, + end_date: datetime | None = None, + skip: int = 0, + limit: int = 20, +) -> tuple[list[CitationRecord], int]: + # Build base filter: citations belonging to the user's queries + conditions = [Query.user_id == user_id] + + if query_id is not None: + conditions.append(CitationRecord.query_id == query_id) + # Also verify query ownership explicitly when query_id is provided + query = await _verify_query_ownership(db, query_id, user_id) + if query is None: + return [], 0 + + if platform is not None: + conditions.append(CitationRecord.platform == platform) + + if start_date is not None: + conditions.append(CitationRecord.queried_at >= start_date) + + if end_date is not None: + conditions.append(CitationRecord.queried_at <= end_date) + + stmt = ( + select(CitationRecord) + .join(Query, CitationRecord.query_id == Query.id) + .where(and_(*conditions)) + .order_by(CitationRecord.queried_at.desc()) + .offset(skip) + .limit(limit) + ) + result = await db.execute(stmt) + items = result.scalars().all() + + count_stmt = ( + select(func.count()) + .select_from(CitationRecord) + .join(Query, CitationRecord.query_id == Query.id) + .where(and_(*conditions)) + ) + count_result = await db.execute(count_stmt) + total = count_result.scalar_one() + + return list(items), total + + +async def get_citation_stats( + db: AsyncSession, + user_id: uuid.UUID, + query_id: uuid.UUID | None = None, +) -> dict: + # Verify ownership if query_id provided + if query_id is not None: + query = await _verify_query_ownership(db, query_id, user_id) + if query is None: + return { + "total_queries": 0, + "total_citations": 0, + "citation_rate": 0.0, + "avg_position": None, + "by_platform": {}, + "trend": [], + } + + # Base filter + base_conditions = [Query.user_id == user_id] + if query_id is not None: + base_conditions.append(CitationRecord.query_id == query_id) + + base_where = and_(*base_conditions) + + # Total queries and citations + total_queries_stmt = ( + select(func.count()) + .select_from(CitationRecord) + .join(Query, CitationRecord.query_id == Query.id) + .where(base_where) + ) + total_queries_result = await db.execute(total_queries_stmt) + total_queries = total_queries_result.scalar_one() + + total_citations_stmt = ( + select(func.count()) + .select_from(CitationRecord) + .join(Query, CitationRecord.query_id == Query.id) + .where(base_where, CitationRecord.cited.is_(True)) + ) + total_citations_result = await db.execute(total_citations_stmt) + total_citations = total_citations_result.scalar_one() + + citation_rate = total_citations / total_queries if total_queries > 0 else 0.0 + + # Average position (only for cited records with a position) + avg_pos_stmt = ( + select(func.avg(CitationRecord.citation_position)) + .join(Query, CitationRecord.query_id == Query.id) + .where( + base_where, + CitationRecord.cited.is_(True), + CitationRecord.citation_position.isnot(None), + ) + ) + avg_pos_result = await db.execute(avg_pos_stmt) + avg_position = avg_pos_result.scalar_one() + avg_position = round(avg_position, 1) if avg_position is not None else None + + # By platform stats + platform_stmt = ( + select( + CitationRecord.platform, + func.count().label("queries"), + func.sum(cast(CitationRecord.cited, Integer)).label("citations"), + func.avg(CitationRecord.citation_position).label("avg_position"), + ) + .join(Query, CitationRecord.query_id == Query.id) + .where(base_where) + .group_by(CitationRecord.platform) + ) + platform_result = await db.execute(platform_stmt) + by_platform = {} + for row in platform_result.all(): + platform_name = row.platform + queries = row.queries + citations = row.citations or 0 + rate = citations / queries if queries > 0 else 0.0 + plat_avg_pos = row.avg_position + plat_avg_pos = round(plat_avg_pos, 1) if plat_avg_pos is not None else None + by_platform[platform_name] = { + "queries": queries, + "citations": citations, + "rate": round(rate, 2), + "avg_position": plat_avg_pos, + } + + # Trend: past 30 days grouped by week + # Use naive datetime to avoid mixing with naive datetimes from database + now = datetime.utcnow() + thirty_days_ago = now - timedelta(days=30) + + # Cross-database week grouping expression + dialect = db.bind.dialect.name if db.bind else "postgresql" + if dialect == "postgresql": + week_expr = func.date_trunc("week", CitationRecord.queried_at) + else: + # SQLite compatible week grouping (YYYY-WW format) + week_expr = func.strftime("%Y-%W", CitationRecord.queried_at) + + trend_stmt = ( + select( + week_expr.label("week_start"), + func.sum(cast(CitationRecord.cited, Integer)).label("citations"), + ) + .join(Query, CitationRecord.query_id == Query.id) + .where( + base_where, + CitationRecord.queried_at >= thirty_days_ago, + ) + .group_by(week_expr) + .order_by(week_expr) + ) + trend_result = await db.execute(trend_stmt) + trend = [] + for row in trend_result.all(): + week_start = row.week_start + if isinstance(week_start, datetime): + date_str = week_start.date().isoformat() + else: + date_str = str(week_start) + trend.append({ + "date": date_str, + "citations": int(row.citations or 0), + }) + + return { + "total_queries": total_queries, + "total_citations": total_citations, + "citation_rate": round(citation_rate, 2), + "avg_position": avg_position, + "by_platform": by_platform, + "trend": trend, + } + + +async def trigger_query_now( + db: AsyncSession, + user_id: uuid.UUID, + query_id: uuid.UUID, +) -> QueryTask: + query = await _verify_query_ownership(db, query_id, user_id) + if query is None: + raise ValueError("Query not found") + + if query.status != "active": + raise ValueError("Query is not active") + + platforms = query.platforms or [] + if not platforms: + raise ValueError("No platforms configured for this query") + + first_task = None + for platform in platforms: + task = QueryTask( + query_id=query_id, + platform=platform, + status="pending", + ) + db.add(task) + if first_task is None: + first_task = task + + await db.commit() + if first_task is not None: + await db.refresh(first_task) + return first_task + + +async def export_citations_csv( + db: AsyncSession, + user_id: uuid.UUID, + query_id: uuid.UUID, +) -> str: + query = await _verify_query_ownership(db, query_id, user_id) + if query is None: + raise ValueError("Query not found") + + stmt = ( + select(CitationRecord) + .where(CitationRecord.query_id == query_id) + .order_by(CitationRecord.queried_at.desc()) + ) + result = await db.execute(stmt) + records = result.scalars().all() + + output = io.StringIO() + writer = csv.writer(output) + writer.writerow(["日期", "平台", "是否引用", "引用位置", "引用文本", "竞争品牌"]) + + for record in records: + writer.writerow([ + record.queried_at.isoformat() if record.queried_at else "", + record.platform, + "是" if record.cited else "否", + record.citation_position if record.citation_position is not None else "", + record.citation_text or "", + ", ".join(record.competitor_brands) if record.competitor_brands else "", + ]) + + return output.getvalue() diff --git a/backend/app/services/query.py b/backend/app/services/query.py new file mode 100644 index 0000000..1df9293 --- /dev/null +++ b/backend/app/services/query.py @@ -0,0 +1,122 @@ +import uuid +from datetime import datetime, timedelta, timezone + +from sqlalchemy import delete, func, select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models.query import Query +from app.models.user import User +from app.schemas.query import QueryCreate, QueryUpdate + + +async def get_queries( + db: AsyncSession, + user_id: uuid.UUID, + skip: int = 0, + limit: int = 20, +) -> tuple[list[Query], int]: + stmt = ( + select(Query) + .where(Query.user_id == user_id) + .order_by(Query.created_at.desc()) + .offset(skip) + .limit(limit) + ) + result = await db.execute(stmt) + items = result.scalars().all() + + count_stmt = select(func.count()).select_from(Query).where(Query.user_id == user_id) + count_result = await db.execute(count_stmt) + total = count_result.scalar_one() + + return list(items), total + + +async def get_query( + db: AsyncSession, + query_id: uuid.UUID, + user_id: uuid.UUID, +) -> Query | None: + stmt = select(Query).where(Query.id == query_id, Query.user_id == user_id) + result = await db.execute(stmt) + return result.scalar_one_or_none() + + +async def create_query( + db: AsyncSession, + user_id: uuid.UUID, + query_data: QueryCreate, +) -> Query: + # Check user's current query count against max_queries limit + count_stmt = select(func.count()).select_from(Query).where(Query.user_id == user_id) + count_result = await db.execute(count_stmt) + current_count = count_result.scalar_one() + + user_stmt = select(User).where(User.id == user_id) + user_result = await db.execute(user_stmt) + user = user_result.scalar_one() + + if current_count >= user.max_queries: + raise PermissionError("Query limit exceeded") + + # Calculate next_query_at based on frequency (use naive datetime for DB compatibility) + now = datetime.utcnow() + if query_data.frequency == "daily": + next_query_at = now + timedelta(days=1) + else: # weekly + next_query_at = now + timedelta(days=7) + + query = Query( + user_id=user_id, + keyword=query_data.keyword, + target_brand=query_data.target_brand, + brand_aliases=query_data.brand_aliases or [], + platforms=query_data.platforms, + frequency=query_data.frequency, + next_query_at=next_query_at, + ) + db.add(query) + await db.commit() + await db.refresh(query) + return query + + +async def update_query( + db: AsyncSession, + query_id: uuid.UUID, + user_id: uuid.UUID, + update_data: QueryUpdate, +) -> Query | None: + stmt = select(Query).where(Query.id == query_id, Query.user_id == user_id) + result = await db.execute(stmt) + query = result.scalar_one_or_none() + if query is None: + return None + + update_dict = update_data.model_dump(exclude_unset=True) + + # Recalculate next_query_at if frequency is updated + if "frequency" in update_dict: + now = datetime.utcnow() + if update_dict["frequency"] == "daily": + query.next_query_at = now + timedelta(days=1) + else: # weekly + query.next_query_at = now + timedelta(days=7) + + for field, value in update_dict.items(): + setattr(query, field, value) + + await db.commit() + await db.refresh(query) + return query + + +async def delete_query( + db: AsyncSession, + query_id: uuid.UUID, + user_id: uuid.UUID, +) -> bool: + stmt = delete(Query).where(Query.id == query_id, Query.user_id == user_id) + result = await db.execute(stmt) + await db.commit() + return result.rowcount > 0 diff --git a/backend/app/utils/__init__.py b/backend/app/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/workers/__init__.py b/backend/app/workers/__init__.py new file mode 100644 index 0000000..417b18c --- /dev/null +++ b/backend/app/workers/__init__.py @@ -0,0 +1,12 @@ +from app.workers.citation_engine import CitationEngine +from app.workers.platforms.kimi import KimiAdapter +from app.workers.platforms.wenxin import WenxinAdapter +from app.workers.scheduler import QueryScheduler, query_scheduler + +__all__ = [ + "CitationEngine", + "KimiAdapter", + "WenxinAdapter", + "QueryScheduler", + "query_scheduler", +] diff --git a/backend/app/workers/citation_engine.py b/backend/app/workers/citation_engine.py new file mode 100644 index 0000000..eae68c9 --- /dev/null +++ b/backend/app/workers/citation_engine.py @@ -0,0 +1,308 @@ +import difflib +import logging +import re +import uuid +from datetime import datetime, timedelta + +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select + +from app.models.citation_record import CitationRecord +from app.models.query import Query +from app.models.query_task import QueryTask +from app.workers.platforms.kimi import KimiAdapter +from app.workers.platforms.wenxin import WenxinAdapter + +logger = logging.getLogger(__name__) + + +class BrandMatcher: + """品牌匹配器:检测文本中是否引用了目标品牌""" + + def __init__(self, target_brand: str, brand_aliases: list[str] | None = None): + self.target_brand = target_brand + self.brand_aliases = brand_aliases or [] + + def match(self, text: str) -> dict: + """ + 返回: { + "cited": bool, + "confidence": float, # 0.0-1.0 + "match_type": str, # "exact"/"alias"/"fuzzy"/None + "position": int|None, # 在文本段落中的位置(第几段提到,1-based) + "citation_text": str|None, # 被引用的上下文片段 + } + """ + if not text: + return { + "cited": False, + "confidence": 0.0, + "match_type": None, + "position": None, + "citation_text": None, + } + + # 1. 精确匹配 + if self.target_brand in text: + position, citation_text = self._extract_position_and_context(text, self.target_brand) + return { + "cited": True, + "confidence": 1.0, + "match_type": "exact", + "position": position, + "citation_text": citation_text, + } + + # 2. 别名匹配 + for alias in self.brand_aliases: + if alias in text: + position, citation_text = self._extract_position_and_context(text, alias) + return { + "cited": True, + "confidence": 0.9, + "match_type": "alias", + "position": position, + "citation_text": citation_text, + } + + # 3. 模糊匹配 + best_ratio = 0.0 + best_match = None + for word in self._extract_candidates(text): + ratio = difflib.SequenceMatcher(None, self.target_brand, word).ratio() + if ratio > best_ratio: + best_ratio = ratio + best_match = word + + for alias in self.brand_aliases: + for word in self._extract_candidates(text): + ratio = difflib.SequenceMatcher(None, alias, word).ratio() + if ratio > best_ratio: + best_ratio = ratio + best_match = word + + if best_ratio > 0.4 and best_match: + position, citation_text = self._extract_position_and_context(text, best_match) + return { + "cited": True, + "confidence": round(best_ratio, 2), + "match_type": "fuzzy", + "position": position, + "citation_text": citation_text, + } + + return { + "cited": False, + "confidence": 0.0, + "match_type": None, + "position": None, + "citation_text": None, + } + + def _extract_candidates(self, text: str) -> list[str]: + """从文本中提取候选词(按非文字字符分割)""" + # 匹配中文词组、英文单词等 + return [w for w in re.split(r'[^\w\u4e00-\u9fff]+', text) if len(w) >= 2] + + def _extract_position_and_context(self, text: str, keyword: str) -> tuple[int | None, str | None]: + """提取品牌首次出现的段落位置(1-based)和上下文片段""" + paragraphs = [p.strip() for p in text.split('\n') if p.strip()] + if not paragraphs: + paragraphs = [text] + + for idx, paragraph in enumerate(paragraphs, start=1): + if keyword in paragraph: + # 截取前200字符 + snippet = paragraph[:200] + return idx, snippet + + return None, None + + +class CompetitorDetector: + """竞争品牌检测器""" + + # 预定义一些常见行业品牌列表 + KNOWN_BRANDS = { + "保险": ["中国平安", "中国人寿", "太平洋保险", "新华保险", "泰康保险", "中国人保", "友邦保险"], + "金融": ["工商银行", "建设银行", "农业银行", "中国银行", "招商银行", "交通银行"], + "科技": ["华为", "腾讯", "阿里巴巴", "百度", "字节跳动", "小米", "京东"], + } + + def detect(self, text: str, target_brand: str) -> list[str]: + """检测文本中出现的其他品牌(排除 target_brand)""" + if not text: + return [] + + competitors = set() + for category, brands in self.KNOWN_BRANDS.items(): + for brand in brands: + if brand == target_brand: + continue + if brand in text: + competitors.add(brand) + + return sorted(list(competitors)) + + +class CitationEngine: + """引用检测引擎核心""" + + def __init__(self): + self.platforms = { + "wenxin": WenxinAdapter(), + "kimi": KimiAdapter(), + } + self.matcher = None + self.competitor_detector = CompetitorDetector() + + async def execute_query(self, query: Query, db: AsyncSession) -> list[CitationRecord]: + """ + 执行一个查询任务: + 1. 创建 BrandMatcher + 2. 遍历 query.platforms + 3. 对每个 platform 执行查询和检测 + 4. 更新 query.last_queried_at 和 query.next_query_at + """ + self.matcher = BrandMatcher( + target_brand=query.target_brand, + brand_aliases=query.brand_aliases or [], + ) + + records: list[CitationRecord] = [] + platforms = query.platforms or ["wenxin", "kimi"] + + for platform_name in platforms: + # 查找或创建 QueryTask + task = await self._get_or_create_task(db, query.id, platform_name) + + # 更新状态为 running + task.status = "running" + task.started_at = datetime.utcnow() + task.error_message = None + await db.commit() + + try: + result = await self.execute_single_platform( + keyword=query.keyword, + platform=platform_name, + target_brand=query.target_brand, + brand_aliases=query.brand_aliases or [], + ) + + # 创建 CitationRecord + record = CitationRecord( + query_id=query.id, + platform=platform_name, + cited=result["cited"], + citation_position=result.get("position"), + citation_text=result.get("citation_text"), + competitor_brands=result.get("competitor_brands", []), + raw_response=result.get("raw_response", ""), + ) + db.add(record) + records.append(record) + + # 更新 QueryTask 状态为 success + task.status = "success" + task.completed_at = datetime.utcnow() + await db.commit() + + except Exception as e: + logger.error(f"平台 {platform_name} 查询失败: {e}") + error_msg = str(e) + task.status = "failed" + task.error_message = error_msg + task.completed_at = datetime.utcnow() + + # 创建一条 cited=False 的记录作为占位 + record = CitationRecord( + query_id=query.id, + platform=platform_name, + cited=False, + raw_response=error_msg, + ) + db.add(record) + records.append(record) + await db.commit() + + # 更新 Query 时间字段 + query.last_queried_at = datetime.utcnow() + query.next_query_at = self._calculate_next_query_at(query.frequency) + await db.commit() + + return records + + async def execute_single_platform( + self, + keyword: str, + platform: str, + target_brand: str, + brand_aliases: list, + ) -> dict: + """执行单个平台的查询和检测""" + adapter = self.platforms.get(platform) + if not adapter: + raise ValueError(f"不支持的平台: {platform}") + + # 获取 AI 回复 + raw_response = await adapter.query(keyword) + + # 品牌匹配 + matcher = BrandMatcher(target_brand=target_brand, brand_aliases=brand_aliases) + match_result = matcher.match(raw_response) + + # 竞争品牌检测 + competitor_brands = self.competitor_detector.detect(raw_response, target_brand) + + return { + "cited": match_result["cited"], + "confidence": match_result["confidence"], + "match_type": match_result["match_type"], + "position": match_result["position"], + "citation_text": match_result["citation_text"], + "competitor_brands": competitor_brands, + "raw_response": raw_response, + } + + async def _get_or_create_task( + self, db: AsyncSession, query_id: uuid.UUID, platform: str + ) -> QueryTask: + """获取或创建 QueryTask""" + stmt = select(QueryTask).where( + QueryTask.query_id == query_id, + QueryTask.platform == platform, + ) + result = await db.execute(stmt) + task = result.scalar_one_or_none() + + if not task: + task = QueryTask( + query_id=query_id, + platform=platform, + status="pending", + ) + db.add(task) + await db.commit() + await db.refresh(task) + + return task + + def _calculate_next_query_at(self, frequency: str | None) -> datetime: + """根据频率计算下次查询时间""" + now = datetime.utcnow() + freq_map = { + "daily": timedelta(days=1), + "weekly": timedelta(days=7), + "monthly": timedelta(days=30), + } + delta = freq_map.get(frequency or "weekly", timedelta(days=7)) + return now + delta + + async def close(self): + """关闭所有平台适配器""" + for adapter in self.platforms.values(): + try: + await adapter.close() + except Exception as e: + logger.warning(f"关闭适配器 {adapter.platform_name} 时出错: {e}") diff --git a/backend/app/workers/platforms/__init__.py b/backend/app/workers/platforms/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/workers/platforms/base.py b/backend/app/workers/platforms/base.py new file mode 100644 index 0000000..1d97158 --- /dev/null +++ b/backend/app/workers/platforms/base.py @@ -0,0 +1,17 @@ +from abc import ABC, abstractmethod + + +class BasePlatformAdapter(ABC): + """AI平台查询适配器基类""" + + platform_name: str # 平台枚举值 + platform_url: str # 平台URL + + @abstractmethod + async def query(self, keyword: str) -> str: + """在AI平台查询关键词,返回原始响应文本""" + pass + + async def close(self): + """清理资源""" + pass diff --git a/backend/app/workers/platforms/kimi.py b/backend/app/workers/platforms/kimi.py new file mode 100644 index 0000000..bd19dc3 --- /dev/null +++ b/backend/app/workers/platforms/kimi.py @@ -0,0 +1,205 @@ +import asyncio +import logging + +from playwright.async_api import async_playwright, TimeoutError as PlaywrightTimeoutError + +from app.workers.platforms.base import BasePlatformAdapter + +logger = logging.getLogger(__name__) + + +class KimiAdapter(BasePlatformAdapter): + """Kimi 平台适配器""" + + platform_name = "kimi" + platform_url = "https://kimi.moonshot.cn" + + def __init__(self): + self._playwright = None + self._browser = None + + async def _ensure_browser(self): + """确保浏览器已启动""" + if self._browser is None: + self._playwright = await async_playwright().start() + try: + self._browser = await self._playwright.chromium.launch(headless=True) + except Exception as e: + logger.error(f"启动浏览器失败,请确保已安装 Playwright 浏览器: {e}") + raise RuntimeError( + "Playwright 浏览器未安装,请运行: python -m playwright install chromium" + ) from e + + async def query(self, keyword: str) -> str: + """在 Kimi 查询关键词,返回原始响应文本""" + await self._ensure_browser() + + last_error = None + for attempt in range(3): # 最多重试2次,共3次尝试 + try: + return await self._do_query(keyword) + except Exception as e: + last_error = e + logger.warning(f"Kimi 查询第 {attempt + 1} 次尝试失败: {e}") + if attempt < 2: + await asyncio.sleep(2 ** attempt) # 指数退避 + + logger.error(f"Kimi 查询最终失败: {last_error}") + raise last_error + + async def _do_query(self, keyword: str) -> str: + """单次查询实现""" + context = None + page = None + try: + context = await self._browser.new_context( + viewport={"width": 1920, "height": 1080}, + user_agent=( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" + ), + ) + page = await context.new_page() + + # 导航到 Kimi 页面,设置30秒超时 + await page.goto(self.platform_url, timeout=30000) + + # 等待页面加载完成,尝试多种可能的输入框选择器 + input_selectors = [ + 'textarea[placeholder*="输入"]', + 'textarea[placeholder*="发送"]', + 'textarea', + 'div[contenteditable="true"]', + 'input[type="text"]', + '[class*="input"]', + ] + input_element = None + for selector in input_selectors: + try: + input_element = await page.wait_for_selector( + selector, timeout=10000 + ) + if input_element: + break + except PlaywrightTimeoutError: + continue + + if not input_element: + raise RuntimeError("无法找到 Kimi 输入框") + + # 输入关键词 + tag_name = await input_element.evaluate("el => el.tagName") + if tag_name == "TEXTAREA" or tag_name == "INPUT": + await input_element.fill(keyword) + else: + await input_element.fill(keyword) + + # 提交查询(尝试回车或点击发送按钮) + try: + send_button = await page.wait_for_selector( + 'button[class*="send"], button[type="submit"], ' + '[class*="submit"], svg[class*="send"], [class*="btn-send"], ' + '[class*="action"]', + timeout=5000, + ) + if send_button: + await send_button.click() + else: + await input_element.press("Enter") + except PlaywrightTimeoutError: + await input_element.press("Enter") + + # 等待回复出现并稳定(检测文本停止变化) + response_text = await self._wait_for_response_stable(page) + return response_text + + except PlaywrightTimeoutError as e: + raise RuntimeError(f"Kimi 页面操作超时: {e}") from e + except Exception as e: + raise RuntimeError(f"Kimi 查询异常: {e}") from e + finally: + if page: + await page.close() + if context: + await context.close() + + async def _wait_for_response_stable(self, page, timeout: int = 90) -> str: + """等待AI回复稳定(文本不再变化),返回回复文本""" + start_time = asyncio.get_running_loop().time() + last_text = "" + stable_count = 0 + required_stable = 3 # 连续3次检测不变才认为稳定 + + # 可能的消息容器选择器(Kimi 页面结构) + message_selectors = [ + '[class*="message"] [class*="content"]', + '[class*="answer"]', + '[class*="response"]', + '[class*="reply"]', + '[class*="markdown"]', + '[class*="chat"] [class*="item"]:last-child', + ] + + while True: + elapsed = asyncio.get_running_loop().time() - start_time + if elapsed > timeout: + # 超时了,返回当前收集到的文本 + logger.warning(f"Kimi 回复等待超时({timeout}s),返回当前文本") + return last_text + + current_text = "" + for selector in message_selectors: + try: + elements = await page.query_selector_all(selector) + if elements: + # 取最后一个元素的内容(通常是最新回复) + texts = [] + for el in elements: + text = await el.inner_text() + if text and text.strip(): + texts.append(text.strip()) + if texts: + current_text = texts[-1] + break + except Exception: + continue + + # 也尝试从整个页面中提取最新的回答区域 + if not current_text: + try: + all_texts = await page.evaluate(""" + () => { + const containers = document.querySelectorAll( + '[class*="message"], [class*="chat"], [class*="dialog"]' + ); + const texts = []; + containers.forEach(c => { + const t = c.innerText; + if (t && t.trim().length > 10) texts.push(t.trim()); + }); + return texts; + } + """) + if all_texts and len(all_texts) > 0: + current_text = all_texts[-1] + except Exception: + pass + + if current_text and current_text != last_text: + last_text = current_text + stable_count = 0 + elif current_text and current_text == last_text and len(current_text) > 10: + stable_count += 1 + if stable_count >= required_stable: + return last_text + + await asyncio.sleep(2) + + async def close(self): + """关闭浏览器资源""" + if self._browser: + await self._browser.close() + self._browser = None + if self._playwright: + await self._playwright.stop() + self._playwright = None diff --git a/backend/app/workers/platforms/wenxin.py b/backend/app/workers/platforms/wenxin.py new file mode 100644 index 0000000..7aa51b7 --- /dev/null +++ b/backend/app/workers/platforms/wenxin.py @@ -0,0 +1,204 @@ +import asyncio +import logging + +from playwright.async_api import async_playwright, TimeoutError as PlaywrightTimeoutError + +from app.workers.platforms.base import BasePlatformAdapter + +logger = logging.getLogger(__name__) + + +class WenxinAdapter(BasePlatformAdapter): + """文心一言平台适配器""" + + platform_name = "wenxin" + platform_url = "https://yiyan.baidu.com" + + def __init__(self): + self._playwright = None + self._browser = None + + async def _ensure_browser(self): + """确保浏览器已启动""" + if self._browser is None: + self._playwright = await async_playwright().start() + try: + self._browser = await self._playwright.chromium.launch(headless=True) + except Exception as e: + logger.error(f"启动浏览器失败,请确保已安装 Playwright 浏览器: {e}") + raise RuntimeError( + "Playwright 浏览器未安装,请运行: python -m playwright install chromium" + ) from e + + async def query(self, keyword: str) -> str: + """在文心一言查询关键词,返回原始响应文本""" + await self._ensure_browser() + + last_error = None + for attempt in range(3): # 最多重试2次,共3次尝试 + try: + return await self._do_query(keyword) + except Exception as e: + last_error = e + logger.warning(f"文心一言查询第 {attempt + 1} 次尝试失败: {e}") + if attempt < 2: + await asyncio.sleep(2 ** attempt) # 指数退避 + + logger.error(f"文心一言查询最终失败: {last_error}") + raise last_error + + async def _do_query(self, keyword: str) -> str: + """单次查询实现""" + context = None + page = None + try: + context = await self._browser.new_context( + viewport={"width": 1920, "height": 1080}, + user_agent=( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" + ), + ) + page = await context.new_page() + + # 导航到文心一言页面,设置30秒超时 + await page.goto(self.platform_url, timeout=30000) + + # 等待页面加载完成,尝试多种可能的输入框选择器 + input_selectors = [ + 'textarea[placeholder*="输入"]', + 'textarea', + 'div[contenteditable="true"]', + 'input[type="text"]', + '[class*="input"]', + ] + input_element = None + for selector in input_selectors: + try: + input_element = await page.wait_for_selector( + selector, timeout=10000 + ) + if input_element: + break + except PlaywrightTimeoutError: + continue + + if not input_element: + raise RuntimeError("无法找到文心一言输入框") + + # 输入关键词 + tag_name = await input_element.evaluate("el => el.tagName") + if tag_name == "TEXTAREA" or tag_name == "INPUT": + await input_element.fill(keyword) + else: + await input_element.fill(keyword) + + # 提交查询(尝试回车或点击发送按钮) + try: + send_button = await page.wait_for_selector( + 'button[class*="send"], button[type="submit"], ' + '[class*="submit"], svg[class*="send"], [class*="btn-send"]', + timeout=5000, + ) + if send_button: + await send_button.click() + else: + await input_element.press("Enter") + except PlaywrightTimeoutError: + await input_element.press("Enter") + + # 等待回复出现并稳定(检测文本停止变化) + response_text = await self._wait_for_response_stable(page) + return response_text + + except PlaywrightTimeoutError as e: + raise RuntimeError(f"文心一言页面操作超时: {e}") from e + except Exception as e: + raise RuntimeError(f"文心一言查询异常: {e}") from e + finally: + if page: + await page.close() + if context: + await context.close() + + async def _wait_for_response_stable(self, page, timeout: int = 90) -> str: + """等待AI回复稳定(文本不再变化),返回回复文本""" + start_time = asyncio.get_running_loop().time() + last_text = "" + stable_count = 0 + required_stable = 3 # 连续3次检测不变才认为稳定 + + # 可能的消息容器选择器 + message_selectors = [ + '[class*="message"] [class*="content"]', + '[class*="answer"]', + '[class*="response"]', + '[class*="reply"]', + '[class*="markdown"]', + '[class*="chat"] [class*="item"]:last-child', + ] + + while True: + elapsed = asyncio.get_running_loop().time() - start_time + if elapsed > timeout: + # 超时了,返回当前收集到的文本 + logger.warning(f"文心一言回复等待超时({timeout}s),返回当前文本") + return last_text + + current_text = "" + for selector in message_selectors: + try: + elements = await page.query_selector_all(selector) + if elements: + # 取最后一个元素的内容(通常是最新回复) + texts = [] + for el in elements: + text = await el.inner_text() + if text and text.strip(): + texts.append(text.strip()) + if texts: + current_text = texts[-1] + break + except Exception: + continue + + # 也尝试从整个页面中提取最新的回答区域 + if not current_text: + try: + # 备选方案:提取页面中所有可能的回复文本块 + all_texts = await page.evaluate(""" + () => { + const containers = document.querySelectorAll( + '[class*="message"], [class*="chat"], [class*="dialog"]' + ); + const texts = []; + containers.forEach(c => { + const t = c.innerText; + if (t && t.trim().length > 10) texts.push(t.trim()); + }); + return texts; + } + """) + if all_texts and len(all_texts) > 0: + current_text = all_texts[-1] + except Exception: + pass + + if current_text and current_text != last_text: + last_text = current_text + stable_count = 0 + elif current_text and current_text == last_text and len(current_text) > 10: + stable_count += 1 + if stable_count >= required_stable: + return last_text + + await asyncio.sleep(2) + + async def close(self): + """关闭浏览器资源""" + if self._browser: + await self._browser.close() + self._browser = None + if self._playwright: + await self._playwright.stop() + self._playwright = None diff --git a/backend/app/workers/scheduler.py b/backend/app/workers/scheduler.py new file mode 100644 index 0000000..2ed9688 --- /dev/null +++ b/backend/app/workers/scheduler.py @@ -0,0 +1,94 @@ +""" +定时任务调度器 +- 使用 APScheduler 的 AsyncIOScheduler +- 每小时执行一次检查 +- 查找 queries 表中 status='active' 且 next_query_at <= now() 的记录 +- 为每个符合条件的 query 调用 CitationEngine 执行查询 +""" + +import asyncio +import logging +from datetime import datetime, timezone + +from apscheduler.schedulers.asyncio import AsyncIOScheduler +from apscheduler.triggers.interval import IntervalTrigger +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.database import AsyncSessionLocal +from app.models.query import Query +from app.workers.citation_engine import CitationEngine + +logger = logging.getLogger(__name__) + + +class QueryScheduler: + def __init__(self): + self.scheduler = AsyncIOScheduler() + self.engine = CitationEngine() + + def start(self): + """启动调度器""" + self.scheduler.add_job( + self._run_check, + trigger=IntervalTrigger(hours=1), + id="check_queries", + name="检查并执行到期的查询任务", + replace_existing=True, + ) + self.scheduler.start() + logger.info("查询调度器已启动,每小时检查一次待执行任务") + + def _run_check(self): + """同步包装:将异步检查任务调度到当前事件循环""" + try: + loop = asyncio.get_running_loop() + loop.create_task(self.check_and_execute_queries()) + except RuntimeError: + # 没有运行中的事件循环,使用新事件循环执行 + asyncio.run(self.check_and_execute_queries()) + + async def check_and_execute_queries(self): + """检查并执行到期的查询任务""" + logger.info("开始检查待执行查询任务...") + async with AsyncSessionLocal() as db: + try: + now = datetime.now(timezone.utc) + stmt = select(Query).where( + Query.status == "active", + Query.next_query_at <= now, + ) + result = await db.execute(stmt) + queries = result.scalars().all() + + logger.info(f"找到 {len(queries)} 个待执行查询") + + for query in queries: + try: + await self._execute_single_query(query, db) + except Exception as e: + logger.error(f"执行查询 {query.id} 失败: {e}") + continue + + except Exception as e: + logger.error(f"检查查询任务时出错: {e}") + + async def _execute_single_query(self, query: Query, db: AsyncSession): + """执行单个查询""" + logger.info(f"开始执行查询: {query.keyword} (ID: {query.id})") + try: + await self.engine.execute_query(query, db) + logger.info(f"查询 {query.id} 执行完成") + except Exception as e: + logger.error(f"查询 {query.id} 执行失败: {e}") + raise + + async def shutdown(self): + """关闭调度器""" + self.scheduler.shutdown(wait=False) + await self.engine.close() + logger.info("查询调度器已关闭") + + +# 全局调度器实例 +query_scheduler = QueryScheduler() diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..203ceff --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,35 @@ +# Web框架 +fastapi>=0.109.0 +uvicorn[standard] + +# 数据库 +sqlalchemy>=2.0 +asyncpg +alembic + +# 数据验证与配置 +pydantic>=2.0 +pydantic-settings +email-validator + +# 认证与安全 +python-jose[cryptography] +passlib[bcrypt] +bcrypt<4.0 +python-multipart + +# 缓存与任务调度 +redis +apscheduler>=3.10 + +# 浏览器自动化 +playwright>=1.40 + +# HTTP客户端与工具 +httpx +python-dotenv + +# 测试依赖 +pytest>=8.0 +pytest-asyncio>=0.23.0 +aiosqlite diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c1c9735 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,70 @@ +version: "3.9" + +services: + db: + image: postgres:15-alpine + container_name: geo_db + restart: unless-stopped + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres123 + POSTGRES_DB: geo_platform + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d geo_platform"] + interval: 5s + timeout: 5s + retries: 5 + + redis: + image: redis:7-alpine + container_name: geo_redis + restart: unless-stopped + ports: + - "6379:6379" + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 5s + retries: 5 + + backend: + build: ./backend + container_name: geo_backend + restart: unless-stopped + ports: + - "8000:8000" + env_file: + - .env + volumes: + - ./backend:/app + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy + command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload + + frontend: + build: ./frontend + container_name: geo_frontend + restart: unless-stopped + ports: + - "3000:3000" + env_file: + - .env + volumes: + - ./frontend:/app + - /app/node_modules + depends_on: + - backend + command: npm run dev + +volumes: + postgres_data: + redis_data: diff --git a/docs/GEO 平台 MVP 开发指导文档(Trae_Qoder 专用).pdf b/docs/GEO 平台 MVP 开发指导文档(Trae_Qoder 专用).pdf new file mode 100644 index 0000000000000000000000000000000000000000..0fa46bc1e2f41e8e21b9db887455d5f5540569ed GIT binary patch literal 561745 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

*9JaAmuNdh9iSqHzi2 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

6#z8 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

  • r~$%M_(K8mVE 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�Es9v5<{J;lgJ{ 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 z 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≦eE*F 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 literal 0 HcmV?d00001 diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json new file mode 100644 index 0000000..3722418 --- /dev/null +++ b/frontend/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["next/core-web-vitals", "next/typescript"] +} diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..fd3dbb5 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..e1c7a6c --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,14 @@ +FROM node:20-alpine + +WORKDIR /app + +# 安装依赖 +COPY package.json package-lock.json ./ +RUN npm ci + +# 复制应用代码 +COPY . . + +EXPOSE 3000 + +CMD ["npm", "run", "dev"] diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/frontend/app/(auth)/layout.tsx b/frontend/app/(auth)/layout.tsx new file mode 100644 index 0000000..dc0fd0b --- /dev/null +++ b/frontend/app/(auth)/layout.tsx @@ -0,0 +1,11 @@ +export default function AuthLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( +

    + {children} +
    + ); +} diff --git a/frontend/app/(auth)/login/page.tsx b/frontend/app/(auth)/login/page.tsx new file mode 100644 index 0000000..9d8f115 --- /dev/null +++ b/frontend/app/(auth)/login/page.tsx @@ -0,0 +1,92 @@ +"use client"; + +import { useState } from "react"; +import { signIn } from "next-auth/react"; +import { useRouter } from "next/navigation"; +import Link from "next/link"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; + +export default function LoginPage() { + const router = useRouter(); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(""); + const result = await signIn("credentials", { + email, + password, + redirect: false, + }); + setLoading(false); + if (result?.error) { + setError("邮箱或密码错误"); + } else { + router.push("/dashboard"); + router.refresh(); + } + }; + + return ( + + + 登录 + 请输入您的账号信息登录GEO平台 + +
    + + {error && ( +

    {error}

    + )} +
    + + setEmail(e.target.value)} + required + /> +
    +
    + + setPassword(e.target.value)} + required + /> +
    +
    + + +

    + 还没有账号?{" "} + + 立即注册 + +

    +
    +
    +
    + ); +} diff --git a/frontend/app/(auth)/register/page.tsx b/frontend/app/(auth)/register/page.tsx new file mode 100644 index 0000000..b3d8742 --- /dev/null +++ b/frontend/app/(auth)/register/page.tsx @@ -0,0 +1,127 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import Link from "next/link"; +import { signIn } from "next-auth/react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { api } from "@/lib/api"; + +export default function RegisterPage() { + const router = useRouter(); + const [name, setName] = useState(""); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (password !== confirmPassword) { + setError("两次输入的密码不一致"); + return; + } + setLoading(true); + setError(""); + try { + await api.auth.register({ name, email, password }); + const result = await signIn("credentials", { + email, + password, + redirect: false, + }); + if (result?.error) { + setError("注册成功但自动登录失败,请手动登录"); + } else { + router.push("/dashboard"); + router.refresh(); + } + } catch (err: unknown) { + const message = err instanceof Error ? err.message : "注册失败"; + setError(message); + } finally { + setLoading(false); + } + }; + + return ( + + + 注册 + 创建GEO平台账号 + +
    + + {error && ( +

    {error}

    + )} +
    + + setName(e.target.value)} + required + /> +
    +
    + + setEmail(e.target.value)} + required + /> +
    +
    + + setPassword(e.target.value)} + required + /> +
    +
    + + setConfirmPassword(e.target.value)} + required + /> +
    +
    + + +

    + 已有账号?{" "} + + 立即登录 + +

    +
    +
    +
    + ); +} diff --git a/frontend/app/(dashboard)/dashboard/citations/page.tsx b/frontend/app/(dashboard)/dashboard/citations/page.tsx new file mode 100644 index 0000000..351026b --- /dev/null +++ b/frontend/app/(dashboard)/dashboard/citations/page.tsx @@ -0,0 +1,293 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { useSession } from "next-auth/react"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Button } from "@/components/ui/button"; +import { api } from "@/lib/api"; +import { PLATFORM_MAP } from "@/lib/platforms"; +import { Check, X, Loader2, Quote, Filter } from "lucide-react"; + +interface CitationItem { + id: string; + query_id: string; + platform: string; + cited: boolean; + citation_position: number | null; + citation_text: string | null; + competitor_brands: string[]; + queried_at: string; +} + +interface QueryOption { + id: string; + keyword: string; +} + +export default function CitationsPage() { + const { data: session } = useSession(); + const [citations, setCitations] = useState([]); + const [queries, setQueries] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const [selectedQuery, setSelectedQuery] = useState("all"); + const [selectedPlatform, setSelectedPlatform] = useState("all"); + const [startDate, setStartDate] = useState(""); + const [endDate, setEndDate] = useState(""); + const isFirstFilterEffect = useRef(true); + + useEffect(() => { + if (!session?.accessToken) return; + loadQueries(); + loadCitations(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [session?.accessToken]); + + useEffect(() => { + if (!session?.accessToken) return; + if (isFirstFilterEffect.current) { + isFirstFilterEffect.current = false; + return; + } + loadCitations(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedQuery, selectedPlatform, startDate, endDate, session?.accessToken]); + + async function loadQueries() { + try { + const data = await api.queries.list(session!.accessToken); + setQueries(data.items || []); + } catch { + // 静默失败,查询词筛选为非必需 + } + } + + async function loadCitations() { + if (!session?.accessToken) return; + try { + setLoading(true); + const params = new URLSearchParams(); + if (selectedQuery && selectedQuery !== "all") params.append("query_id", selectedQuery); + if (selectedPlatform && selectedPlatform !== "all") params.append("platform", selectedPlatform); + if (startDate) params.append("start_date", startDate); + if (endDate) params.append("end_date", endDate); + + const data = await api.citations.list( + session.accessToken, + params.toString() + ); + setCitations(data.items || []); + setError(null); + } catch (err) { + setError(err instanceof Error ? err.message : "加载引用记录失败"); + } finally { + setLoading(false); + } + } + + function handleFilter() { + loadCitations(); + } + + function handleReset() { + setSelectedQuery("all"); + setSelectedPlatform("all"); + setStartDate(""); + setEndDate(""); + } + + if (loading && citations.length === 0) { + return ( +
    +
    +

    引用记录

    +

    查看各平台的引用检测结果

    +
    +
    + +
    +
    + ); + } + + return ( +
    +
    +

    引用记录

    +

    查看各平台的引用检测结果

    +
    + + + + + + 筛选条件 + + + +
    +
    + + +
    +
    + + +
    +
    + + setStartDate(e.target.value)} + /> +
    +
    + + setEndDate(e.target.value)} + /> +
    +
    + + +
    +
    +
    +
    + + {error && ( +
    + {error} +
    + )} + + + + 引用记录列表 + + + {citations.length === 0 ? ( +
    + +

    暂无引用记录

    +

    添加查询词并执行查询后将在此显示结果

    +
    + ) : ( +
    + + + + 平台 + 是否引用 + 引用位置 + 引用文本片段 + 竞争品牌 + 查询时间 + + + + {citations.map((item) => ( + + + {PLATFORM_MAP[item.platform] || item.platform} + + + {item.cited ? ( +
    + + 已引用 +
    + ) : ( +
    + + 未引用 +
    + )} +
    + + {item.citation_position !== null + ? `第 ${item.citation_position} 位` + : "—"} + + + {item.citation_text || "—"} + + +
    + {item.competitor_brands?.length > 0 ? ( + item.competitor_brands.map((brand) => ( + + {brand} + + )) + ) : ( + + )} +
    +
    + + {new Date(item.queried_at).toLocaleString("zh-CN")} + +
    + ))} +
    +
    +
    + )} +
    +
    +
    + ); +} diff --git a/frontend/app/(dashboard)/dashboard/page.tsx b/frontend/app/(dashboard)/dashboard/page.tsx new file mode 100644 index 0000000..27d3ef9 --- /dev/null +++ b/frontend/app/(dashboard)/dashboard/page.tsx @@ -0,0 +1,226 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useSession } from "next-auth/react"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Skeleton } from "@/components/ui/skeleton"; +import { TrendChart } from "@/components/charts/trend-chart"; +import { PlatformChart } from "@/components/charts/platform-chart"; +import { api } from "@/lib/api"; +import { Search, Quote, TrendingUp, Target, Plus } from "lucide-react"; +import Link from "next/link"; + +interface StatsData { + total_queries: number; + total_citations: number; + citation_rate: number; + avg_position: number; + by_platform: Record; + trend: { date: string; citations: number }[]; +} + +export default function DashboardPage() { + const { data: session } = useSession(); + const [stats, setStats] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const token = session?.accessToken; + if (!token) return; + + async function loadStats() { + try { + setLoading(true); + const data = await api.citations.getStats(token!); + setStats(data); + } catch (err) { + setError(err instanceof Error ? err.message : "加载数据失败"); + } finally { + setLoading(false); + } + } + + loadStats(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [session?.accessToken]); + + if (loading) { + return ( +
    +
    +

    数据总览

    +

    欢迎回到GEO平台

    +
    +
    + {Array.from({ length: 4 }).map((_, i) => ( + + + + + + + + + + ))} +
    +
    + + + + + + + + + + + + + + + + +
    +
    + ); + } + + if (error) { + return ( +
    +

    {error}

    + +
    + ); + } + + const isEmpty = !stats || ( + stats.total_queries === 0 && + stats.total_citations === 0 && + (!stats.trend || stats.trend.length === 0) && + (!stats.by_platform || Object.keys(stats.by_platform).length === 0) + ); + + if (isEmpty) { + return ( +
    +
    +

    数据总览

    +

    欢迎回到GEO平台

    +
    + +
    +
    + +
    +

    暂无数据

    +

    + 您还没有创建任何查询词。创建第一个查询词后,系统将自动开始收集引用数据。 +

    + + + +
    +
    + ); + } + + const statCards = [ + { + title: "总查询次数", + value: stats?.total_queries ?? 0, + icon: Search, + color: "text-blue-600", + bg: "bg-blue-50", + }, + { + title: "总引用次数", + value: stats?.total_citations ?? 0, + icon: Quote, + color: "text-emerald-600", + bg: "bg-emerald-50", + }, + { + title: "引用率", + value: `${Math.round((stats?.citation_rate ?? 0) * 100)}%`, + icon: TrendingUp, + color: "text-amber-600", + bg: "bg-amber-50", + }, + { + title: "平均引用位置", + value: stats?.avg_position?.toFixed(1) ?? "0.0", + icon: Target, + color: "text-purple-600", + bg: "bg-purple-50", + }, + ]; + + return ( +
    +
    +

    数据总览

    +

    欢迎回到GEO平台

    +
    + +
    + {statCards.map((card) => ( + + + {card.title} +
    + +
    +
    + +
    {card.value}
    +
    +
    + ))} +
    + +
    + + + 引用趋势(过去30天) + + + {stats?.trend && stats.trend.length > 0 ? ( + + ) : ( +
    + 暂无趋势数据 +
    + )} +
    +
    + + + + 平台引用率对比 + + + {stats?.by_platform && Object.keys(stats.by_platform).length > 0 ? ( + + ) : ( +
    + 暂无平台数据 +
    + )} +
    +
    +
    +
    + ); +} diff --git a/frontend/app/(dashboard)/dashboard/queries/page.tsx b/frontend/app/(dashboard)/dashboard/queries/page.tsx new file mode 100644 index 0000000..693eed5 --- /dev/null +++ b/frontend/app/(dashboard)/dashboard/queries/page.tsx @@ -0,0 +1,525 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useSession } from "next-auth/react"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { api } from "@/lib/api"; +import { PLATFORM_MAP, PLATFORMS } from "@/lib/platforms"; +import { + Plus, + Pencil, + Trash2, + Play, + Loader2, + Search, + AlertTriangle, + CheckCircle, +} from "lucide-react"; + +interface QueryItem { + id: string; + keyword: string; + target_brand: string; + brand_aliases?: string[]; + platforms: string[]; + frequency: string; + status: string; + last_queried_at: string | null; + created_at: string; +} + +interface QueryFormData { + keyword: string; + target_brand: string; + brand_aliases: string; + platforms: string[]; + frequency: string; +} + +const FREQUENCY_MAP: Record = { + daily: "每日", + weekly: "每周", +}; + +const emptyForm: QueryFormData = { + keyword: "", + target_brand: "", + brand_aliases: "", + platforms: [], + frequency: "weekly", +}; + +export default function QueriesPage() { + const { data: session } = useSession(); + const [queries, setQueries] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const [dialogOpen, setDialogOpen] = useState(false); + const [editingId, setEditingId] = useState(null); + const [formData, setFormData] = useState(emptyForm); + const [saving, setSaving] = useState(false); + + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [deletingId, setDeletingId] = useState(null); + const [deleting, setDeleting] = useState(false); + + const [actionLoading, setActionLoading] = useState(null); + const [formErrors, setFormErrors] = useState>({}); + const [successMsg, setSuccessMsg] = useState(null); + + function showSuccess(msg: string) { + setSuccessMsg(msg); + setTimeout(() => setSuccessMsg(null), 3000); + } + + useEffect(() => { + if (!session?.accessToken) return; + loadQueries(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [session?.accessToken]); + + async function loadQueries() { + try { + setLoading(true); + const data = await api.queries.list(session!.accessToken); + setQueries(data.items || []); + setError(null); + } catch (err) { + setError(err instanceof Error ? err.message : "加载查询词失败"); + } finally { + setLoading(false); + } + } + + function openAddDialog() { + setEditingId(null); + setFormData(emptyForm); + setFormErrors({}); + setDialogOpen(true); + } + + function openEditDialog(item: QueryItem) { + setEditingId(item.id); + setFormErrors({}); + setFormData({ + keyword: item.keyword, + target_brand: item.target_brand, + brand_aliases: item.brand_aliases?.join(", ") || "", + platforms: item.platforms || [], + frequency: item.frequency, + }); + setDialogOpen(true); + } + + function validateForm(): boolean { + const errors: Record = {}; + if (!formData.keyword.trim()) { + errors.keyword = "请输入关键词"; + } + if (!formData.target_brand.trim()) { + errors.target_brand = "请输入目标品牌"; + } + if (formData.platforms.length === 0) { + errors.platforms = "请至少选择一个平台"; + } + setFormErrors(errors); + return Object.keys(errors).length === 0; + } + + async function handleSave() { + if (!session?.accessToken) return; + if (!validateForm()) { + return; + } + + try { + setSaving(true); + const payload = { + keyword: formData.keyword.trim(), + target_brand: formData.target_brand.trim(), + brand_aliases: formData.brand_aliases + .split(",") + .map((s) => s.trim()) + .filter(Boolean), + platforms: formData.platforms, + frequency: formData.frequency, + }; + + if (editingId) { + await api.queries.update(session.accessToken, editingId, payload); + } else { + await api.queries.create(session.accessToken, payload); + } + setDialogOpen(false); + setSaving(false); + showSuccess(editingId ? "修改成功" : "添加成功"); + await loadQueries(); + } catch (err) { + setError(err instanceof Error ? err.message : "保存失败"); + setSaving(false); + } + } + + function openDeleteDialog(id: string) { + setDeletingId(id); + setDeleteDialogOpen(true); + } + + async function handleDelete() { + if (!session?.accessToken || !deletingId) return; + try { + setDeleting(true); + await api.queries.delete(session.accessToken, deletingId); + setDeleteDialogOpen(false); + setDeletingId(null); + showSuccess("删除成功"); + await loadQueries(); + } catch (err) { + setError(err instanceof Error ? err.message : "删除失败"); + } finally { + setDeleting(false); + } + } + + async function handleRunQuery(id: string) { + if (!session?.accessToken) return; + setActionLoading(id); + setError(null); + try { + await api.queries.runNow(session.accessToken, id); + showSuccess("查询已执行"); + await loadQueries(); + } catch (err) { + setError(err instanceof Error ? err.message : "立即查询失败"); + } finally { + setActionLoading(null); + } + } + + function togglePlatform(platform: string) { + setFormData((prev) => { + const platforms = prev.platforms.includes(platform) + ? prev.platforms.filter((p) => p !== platform) + : [...prev.platforms, platform]; + return { ...prev, platforms }; + }); + } + + if (loading) { + return ( +
    +
    +
    +

    查询词管理

    +

    管理您的关键词查询任务

    +
    +
    +
    + +
    +
    + ); + } + + return ( +
    +
    +
    +

    查询词管理

    +

    管理您的关键词查询任务

    +
    + + + + + + + {editingId ? "编辑查询词" : "添加查询词"} + + {editingId ? "修改查询词配置" : "配置新的关键词查询任务"} + + +
    +
    + + { + setFormData((prev) => ({ ...prev, keyword: e.target.value })); + if (formErrors.keyword) { + setFormErrors((prev) => ({ ...prev, keyword: "" })); + } + }} + /> + {formErrors.keyword && ( +

    {formErrors.keyword}

    + )} +
    +
    + + { + setFormData((prev) => ({ + ...prev, + target_brand: e.target.value, + })); + if (formErrors.target_brand) { + setFormErrors((prev) => ({ ...prev, target_brand: "" })); + } + }} + /> + {formErrors.target_brand && ( +

    {formErrors.target_brand}

    + )} +
    +
    + + + setFormData((prev) => ({ + ...prev, + brand_aliases: e.target.value, + })) + } + /> +
    +
    + +
    + {PLATFORMS.map((p) => ( + + ))} +
    +
    + {formErrors.platforms && ( +

    {formErrors.platforms}

    + )} +
    + + +
    +
    + + + + +
    +
    +
    + + {successMsg && ( +
    + + {successMsg} +
    + )} + + {error && ( +
    + + {error} +
    + )} + + + + 查询词列表 + + + {queries.length === 0 ? ( +
    + +

    暂无查询词

    +

    点击上方按钮添加您的第一个查询词

    +
    + ) : ( +
    + + + + 关键词 + 目标品牌 + 平台 + 频率 + 状态 + 最后查询 + 操作 + + + + {queries.map((item) => ( + + + {item.keyword} + + {item.target_brand} + +
    + {item.platforms.map((p) => ( + + {PLATFORM_MAP[p] || p} + + ))} +
    +
    + {FREQUENCY_MAP[item.frequency] || item.frequency} + + + {item.status === "active" ? "运行中" : "已暂停"} + + + + {item.last_queried_at + ? new Date(item.last_queried_at).toLocaleString("zh-CN") + : "从未"} + + +
    + + + +
    +
    +
    + ))} +
    +
    +
    + )} +
    +
    + + + + + 确认删除 + + 删除后无法恢复,确定要删除这个查询词吗? + + + + + + + + +
    + ); +} diff --git a/frontend/app/(dashboard)/dashboard/reports/page.tsx b/frontend/app/(dashboard)/dashboard/reports/page.tsx new file mode 100644 index 0000000..8f79b8b --- /dev/null +++ b/frontend/app/(dashboard)/dashboard/reports/page.tsx @@ -0,0 +1,199 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useSession } from "next-auth/react"; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { api } from "@/lib/api"; +import { Loader2, FileDown, Info, CheckCircle, AlertTriangle } from "lucide-react"; + +interface QueryOption { + id: string; + keyword: string; +} + +const API_BASE = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"; + +export default function ReportsPage() { + const { data: session } = useSession(); + const [queries, setQueries] = useState([]); + const [selectedQuery, setSelectedQuery] = useState(""); + const [exportFormat, setExportFormat] = useState("csv"); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(false); + + useEffect(() => { + if (!session?.accessToken) return; + loadQueries(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [session?.accessToken]); + + async function loadQueries() { + try { + const data = await api.queries.list(session!.accessToken); + setQueries(data.items || []); + setError(null); + } catch (err) { + setError(err instanceof Error ? err.message : "加载查询词列表失败"); + } + } + + async function handleExport() { + if (!session?.accessToken) return; + if (!selectedQuery) { + setError("请先选择要导出的查询词"); + return; + } + try { + setLoading(true); + setError(null); + setSuccess(false); + + // 使用原生fetch下载CSV文件 + const query = `?query_id=${selectedQuery}`; + const url = `${API_BASE}/api/v1/reports/export/csv${query}`; + const res = await fetch(url, { + headers: { + Authorization: `Bearer ${session.accessToken}`, + }, + }); + + if (!res.ok) { + const errorData = await res.json().catch(() => ({ detail: "导出失败" })); + throw new Error(errorData.detail || `HTTP ${res.status}`); + } + + const blob = await res.blob(); + const downloadUrl = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = downloadUrl; + const filename = selectedQuery + ? `report_${selectedQuery}_${new Date().toISOString().split("T")[0]}.csv` + : `report_all_${new Date().toISOString().split("T")[0]}.csv`; + a.download = filename; + document.body.appendChild(a); + a.click(); + a.remove(); + window.URL.revokeObjectURL(downloadUrl); + setSuccess(true); + setTimeout(() => setSuccess(false), 3000); + } catch (err) { + setError(err instanceof Error ? err.message : "导出失败"); + } finally { + setLoading(false); + } + } + + return ( +
    +
    +

    报告导出

    +

    导出引用检测数据为报告文件

    +
    + +
    + + + + + 导出设置 + + 选择查询词和导出格式 + + +
    + + +
    + +
    + + +

    + 目前仅支持 CSV 格式导出 +

    +
    + + {error && ( +
    + + {error} +
    + )} + + {success && ( +
    + + 导出成功,文件已自动下载 +
    + )} + + +
    +
    + + + + + + 使用说明 + + + +

    1. 选择要导出的查询词

    +

    2. 目前支持 CSV 格式,可用 Excel 或 WPS 打开

    +

    3. 导出文件包含以下字段:

    +
      +
    • 查询关键词
    • +
    • 检测平台
    • +
    • 是否被引用
    • +
    • 引用位置
    • +
    • 引用文本
    • +
    • 竞争品牌
    • +
    • 查询时间
    • +
    +

    4. 文件将自动下载到您的设备

    +
    +
    +
    +
    + ); +} diff --git a/frontend/app/(dashboard)/dashboard/settings/page.tsx b/frontend/app/(dashboard)/dashboard/settings/page.tsx new file mode 100644 index 0000000..0265b93 --- /dev/null +++ b/frontend/app/(dashboard)/dashboard/settings/page.tsx @@ -0,0 +1,171 @@ +"use client"; + +import { useSession } from "next-auth/react"; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { User, Crown, Check, X } from "lucide-react"; + +const PLANS = [ + { + key: "free", + name: "免费版", + price: "¥0", + period: "/月", + features: [ + { text: "最多 3 个查询词", included: true }, + { text: "每日查询 1 次", included: true }, + { text: "基础报告导出", included: true }, + { text: "2 个平台监测", included: true }, + { text: "高级分析图表", included: false }, + { text: "竞品对比分析", included: false }, + { text: "API 接口访问", included: false }, + { text: "优先技术支持", included: false }, + ], + }, + { + key: "starter", + name: "入门版", + price: "¥99", + period: "/月", + features: [ + { text: "最多 10 个查询词", included: true }, + { text: "每日查询 3 次", included: true }, + { text: "基础报告导出", included: true }, + { text: "4 个平台监测", included: true }, + { text: "高级分析图表", included: true }, + { text: "竞品对比分析", included: false }, + { text: "API 接口访问", included: false }, + { text: "优先技术支持", included: false }, + ], + }, + { + key: "pro", + name: "专业版", + price: "¥299", + period: "/月", + features: [ + { text: "最多 50 个查询词", included: true }, + { text: "每日查询 10 次", included: true }, + { text: "高级报告导出", included: true }, + { text: "全部平台监测", included: true }, + { text: "高级分析图表", included: true }, + { text: "竞品对比分析", included: true }, + { text: "API 接口访问", included: false }, + { text: "优先技术支持", included: true }, + ], + }, + { + key: "business", + name: "企业版", + price: "¥999", + period: "/月", + features: [ + { text: "无限查询词", included: true }, + { text: "无限查询次数", included: true }, + { text: "定制化报告", included: true }, + { text: "全部平台监测", included: true }, + { text: "高级分析图表", included: true }, + { text: "竞品对比分析", included: true }, + { text: "API 接口访问", included: true }, + { text: "专属客户经理", included: true }, + ], + }, +]; + +export default function SettingsPage() { + const { data: session } = useSession(); + const currentPlan = "free"; // MVP阶段默认免费版 + + return ( +
    +
    +

    设置

    +

    管理您的账户和订阅信息

    +
    + + + + 用户信息 + + +
    +
    + +
    +
    +

    {session?.user?.name || "未设置姓名"}

    +

    {session?.user?.email || "—"}

    +
    +
    +
    +
    + +
    +
    +

    当前套餐

    +
    + {PLANS.find((p) => p.key === currentPlan)?.name || "免费版"} + MVP 阶段所有功能免费开放 +
    +
    +
    +
    +
    + +
    +

    套餐对比

    +
    + {PLANS.map((plan) => ( + + + {plan.name} + + {plan.price} + {plan.period} + + + +
      + {plan.features.map((feature, idx) => ( +
    • + {feature.included ? ( + + ) : ( + + )} + + {feature.text} + +
    • + ))} +
    + {plan.key === currentPlan && ( + + 当前套餐 + + )} +
    +
    + ))} +
    +
    + + + + MVP 阶段说明 + + +

    + 当前处于 GEO 平台 MVP 阶段,所有功能均可免费使用。正式版上线后将启用套餐限制,届时您可以选择适合您的订阅方案。 +

    +

    + 如有任何问题或建议,欢迎通过平台反馈渠道联系我们。 +

    +
    +
    +
    + ); +} diff --git a/frontend/app/(dashboard)/layout.tsx b/frontend/app/(dashboard)/layout.tsx new file mode 100644 index 0000000..5d06427 --- /dev/null +++ b/frontend/app/(dashboard)/layout.tsx @@ -0,0 +1,26 @@ +import { getServerSession } from "next-auth/next"; +import { redirect } from "next/navigation"; +import { authOptions } from "@/lib/auth"; +import { Sidebar } from "@/components/layout/sidebar"; +import { Header } from "@/components/layout/header"; + +export default async function DashboardLayout({ + children, +}: { + children: React.ReactNode; +}) { + const session = await getServerSession(authOptions); + if (!session) { + redirect("/login"); + } + + return ( +
    + +
    +
    +
    {children}
    +
    +
    + ); +} diff --git a/frontend/app/api/auth/[...nextauth]/route.ts b/frontend/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..7b38c1b --- /dev/null +++ b/frontend/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,6 @@ +import NextAuth from "next-auth"; +import { authOptions } from "@/lib/auth"; + +const handler = NextAuth(authOptions); + +export { handler as GET, handler as POST }; diff --git a/frontend/app/favicon.ico b/frontend/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..718d6fea4835ec2d246af9800eddb7ffb276240c GIT binary patch literal 25931 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 LrJugUA?W`A8`#=m literal 0 HcmV?d00001 diff --git a/frontend/app/fonts/GeistMonoVF.woff b/frontend/app/fonts/GeistMonoVF.woff new file mode 100644 index 0000000000000000000000000000000000000000..f2ae185cbfd16946a534d819e9eb03924abbcc49 GIT binary patch literal 67864 zcmZsCV{|6X^LDby#!fc2?QCp28{4*X$D569+qP}vj&0lKKhN*HAKy9W>N!=Xdb(?> zQB^(TCNCxi0tx~G0t$@@g8bk8lJvX$|6bxEqGBK*H_sp-KYBnwz$0Q}BT2;-%I=)X2ub{=04r2*}TK5D+LXt~5{t z)Bof^+#0@Rw7=mKi|m$bX6?Bh~_rVfN!~Z5D+lYZ~eMdYd=)1 z?To(VG`{%|MBi{mhZ2~!F#vq`Pec9x)g^>91o^TxurUDvvGDqSS9st3-kw(m@3Xga z`qtIzyIr_nARq+I@sH7;0MG(2NPTSa#jh!1f4cEF5Xll)bpZ(>cyI|Q1wleT1wA5Y zq9^hv^x;~(?2G$>(CTL2)#Ou-rP=XDW$spn8<%0TH%F=^X^(F62Vd@bY`Wi$j$33w zf!U^8o_B|x>{pW$eFZG}b7#|uFueKt$`e9j!wHNBGQX67&nfgl(Ae`3qE-E+yBSfA zEnJSA6p%}|+P9ZIYR{w}nfaKIlV@b3YYzcH!?WNXRvg|J( z((lq^WAE%Q7;oE?zDk~Nvg1Dr_0)KH8m&HF%^&8bI!=#YAGqIx$Yf2lH9S*;=c=b6 zUHi?R*$?Q;>HU4-#?hGJ&dj2jq>d3;_NN_TeipMG!(E+ou)RL-kMQv(W$b9+k# z*%bh8;4)9Je-Giu+XwdbyoaSGei^KG*(1D)5+h{Kfg<`v)nU>dj}RiD_+VvZgb7>9 z-Qb^cdc0k1VSIW!onbm2*_uY*_+r1qe${8^DzXxMnX@F#u>I3_n0j_0ih#p?wd+gPI5niQVbIIsk zkxy%JZZqLeb?p_DXdh1*9Z(O`Nm%TZ(zL`RA!dd+$VNO>qwecEt;dy5w%UK1@1exK zD~__{?4}pb@sGL5CjI=xAR7Jym_*l%fS~I(m>6873y~E7k;IfdA_0)|1$o9?h92Js zt4eu6$WMaSodkz#g|LB%Iw?^B?6x^A=arKjpBhhH6ZCbk2{;io5x)B3eh9R{KEOQX z9|&Q1T3-YGeF+9$doOBzU`TntM~LF~ON3aEZ|p9Y7+wF9qBi`6(hl}&)@-uZ`4zJl z>R`Cps(&x90dBZ~SLeCp?oa*PgM%P!bZaG*OS96bkBT*gF)q0a zxEd&4ZXnQHBuCrYm@m@ffPQTObP*2j+P z_?=gLxmGc32nceW5l5oy=+SB$=N%F^{g}lKR9(TljKIPHw)zVyZ?3ODUL^k;0CuW% z!;ErXcl6|m8OB+{5iYNEq}!Y@o<%r_^{5a($V)INcxkIcMA}Gd8LUShZK5U!u)=PR z6ZALS*{0F1Oxl?y$xE;JA+eyc6mW}LqFTZ3ZvVl#h*UFfj`$%JE0l8D!JRBYUlH!L zJ!uZs@&)nqNg9x8t`fZ?k4Ihgdv(Ogzr)|%{JQ|-g@#=7rCIq(Oo={zr!i7F_F!6; zqpKdMO={?6)e1SETQW+U?L?WPzQx9x#RrVu%xa5u$bDgLQrF-K4Iwd}9a=yS3(f1J z=&B1p=UwPU_#kfxrJ(YnDYZkc%{pp&sn{<~MdR_9^8y%u``RUJaJtY*yi=~R9ryu@ z9kzsKGwMLhZ1egl=e5m~k^Ft9pSfxI5B!$g1WaeqpO`4?C-3aj(gSm%1+@BdqpyAV z@X|;G-&|(jA;zG>T=$%}2gC%)gu@pTPQ)SpSw*2DuSrX((%PM=kQ&E@b=Ygy)l&#k zn6Q419734+(;{THjU2Uy9No0H4_jV1#6O)c>u@tbG6oWD;-8yHLnM^;;b@dWvle!?{40o`dO)$$EZ zM^@JN7b3@-+?UUO*P#gtLsy$!7gZcziDwAj59PsCAJm>m6r+l^X1z|%wu-jJhnQ&_ znPJwq9_*qBLoo*W`sPdYk10kPgf$aH@4qU~%&pFl2rZ0AHR*E-AvBR{F9QCehDa@z z95xXU{QZg|=zb2Pq36>@3je4inO+>S(`ht?)Z#zrHM(i>qE+>iU#!8v4QnWDruR08 zihT~ec3TRJh#llhgk(NqF04=VE8}61FWwvTi_}KWRnkIGbxQ)CAyBfBoVsTvRsR!v zeeHuptQ&5sDmg3vV_f9UtqYjdrR(_D^waATK``ZJjfZD5Kduvl1+l2-u6Qf=6Ombx z7Sq ztJ92oU^LD6n$?=8G?#FGx#fF$d!2WBTf$UGVa}#`S@X&5dFIq%K!1Ikjs!+ybc~8&;<*f2$gyb>j{=&y@=kHsC%Xl#WTojY!)xQxm z+xUe-8Of9gTp&DDOh{Yy9#6leUk5m&-h{G7M@bsLtAJZq1|X(5;ulY z-D2nY-`lAFFZza${swOYsV>&wyw;MiiXw9Ze4so}{Flt`IeJQ5b1l1!d)yG4v?WEO zO3yg9oy--%g}hya8*T);IAWhS&T>>KL9Je(WS#9P#!$_f6!1`7cfKj*+i>@*tP8Mjj|un5Z`YGD>MiCU!adPX zx#5sU8_)@)5fHgRLdp7k;l9Mr_8H3SOvpCBbBRGBQ`Wih*Xpj<)C6}E4SH?GeM1wt)HAM~N<~ejyt^Wpq0tmp z6X&e+wbKjOt@{1ng^s>(semrGFCQLXu|@O1tvtmYwuZ`$BSe{a-011Sk2a~(>MVE0 zpIQ7LpuG+o?lOHuw%e_kJ6yAoXCpu*QQeY%8SNh6?$89*3`>%=;EOJb+gtz&Kp|yv zfPV+nw`uTKbxE3vpT)v3C@L}V3(f*@_3N$Flc(8e<6F?hmPF|Dt%$W})5dMX(nql2 zOMy&yEWPokJ^l?odvVv&l(un4B`x0UHu6T8LraPoL*NltIUElZ5m!YVjcyZe{0Gtx zK{scl85IYuMO$EBG$tHHu0zc0wi&8rW3`d{VJC$oYNJ?m2MBStoGQ!4xQLHS_tBeI z4=tL^Lv>Bj^g79fzfCc?aTHu%Uvn6&+a@&*N~Rba)gbaLl?WBo%1^Pjx=t&|S^9nh zu(^m2A5XEp+ZN2L2#w^7IpLW%BW#F@6{50p0liwKYe!&NWu2F@oIV-5r<}*;+3|bP ze>zfTOAXqW760vNex|NG!Xz~@Wcd5UhOk&n5clNgylEGuS)lF7K$c{a+Hl#rx-2Ic zD(HhN(=Sa(v|zonLt6q9;>ZBVh6n__yB8Pn7WCY*KX8V+u(@n9e zOTe7&?}Fvh8wHRCgku@eEVodSv4NBH%wJEO4wEp#-}%%$wR$2D5JR|@$vRkRb7}iIhxv; zshP$6ckt<2KCd5K9#gwy%I*Ey>Fe20M_29Y=)g1AcBH#@^pXEtP30j`IbaZgR2{t^ z`r?E$A9Zdf@wct0$aRwJ=i9-^yxU77e+%zOG9j-MXBP)nekEiIFHfS>Ba|3w;D?|dL35fhFX>Fi zQcepJaiZvXu&=IsDUMoZIo?5N1`h|7?WDfbJmXcY~w_lg&|t|BlK!`YFCDcu*n(Sa{%c z4$vg-+drB`)#x8&q6x0pG5p+BKvfIu#O32<*&LF;z8q?zL`41|Yicx^Yq4jz6>WcO z4=~f8fF;F-A=fL28*f$mLyZ)0X>6z$biG4VuDpiV4z zY~_evrt9XZfAzEyT`LtOtA^qKGM{Tq8NMHGIOL>T;4vaiE@lH-C<@aOeh_^m?<&&h zdXSPA^^n-i>Uj{Z%Lb+6v5B_zD^V_GWE1OBNlHndI9YW5kD^Kk@cZ&Ia z6oRdBan^1xma-m6+`d|wRJR`V~A;L2zw&Yu_yoTtgzTrhi-xxFYK659imn;^%TR%3!4mYTU`we=`K-=!r$)M^U|fng0gd4 zY&D|@id)hQ6lZ6$q#}%snpqqb>@aUApp7;*W>0UoVkg(l}MYC6COXI29 zGc~J-gZ4vC{yy!bjlkXM?rF2de*R#dL=(PI9-L-quUxck&u`DmTQjI#p*2mPjNqc? z$X9XK{UtI;@pJUK?cwIxV;%;lTG0!%y5 zJpWhb11vK@d2I=!;)F5vM`ML)^6b)LCj<7zlFm7!F$_T_`hyDZ>MEBe@A%a+9RG#y z_*KevIxJ(rEBNzd_KBWC<+$;IWH5}W4eTN}TM#4*`n;PelIth54aC}8|KHL1Kd9hY zdg6C1@KJ_+m6OHmY-}EB_QYaDnd8)^Y#fTGC1QB3E&Rq&s{PIUL5DzjJG<4E+;x=! zz3?hDSALlK#YF2II?cmMlq^D)riLWp(`LjFJNTY&BkIxb04C*yZ)Vjb*8{OJ&U(p# z3cxi}BFmgL+V%Ew9*g|D_V>-jj>E&_kXF}@LX&k)UuVIb+!>`~SGXZrZd9yBFoeR5 zNrxA*){}5*BIRJ3GSAb5CW!RX5}9`W*v3|J4v;znteT1Jn6BmRxF0|>v+o2A%ix3E z_}aH+5hk}2B`>5kW}hg%W`rkIVN-e8*j3!A(mQ&IFKdo(2cn%(!rGGG-la2y4dz)d z;cU;$Z5l<(tUS+pPC9~e+Sl_5OnGT=${=;{P%TayUQ^o1bm#Qel@0Ea2wDFsgpR8p z%{42-o*aWIGVFESm@;QGB)am8yb0`j>EazkuEVoKMd!r}nWzO!rg#7+BuCQ?4|TZ^ z`|;e56wJl>(SLl!DEUo1dvlUaqZZ{;%CQg!oaJ?FFxAmVK6uv$_;SHB!^)t!xv-f_$Bs$C)MjJg|HA#qe9b`BSwl8 z2McXH6Uvn|ClJyKV8|OT-V{LIG1v~h>gQprzhfK(DrmFQ4M!VgO!ZS8o6D1p%RSmV z+Xf5C09vC7w0t%eXb8L=U(~wlP)tZ3TaN#j4{NWJFL7# zMeiEPfaIS?IHAdP9aH+sm5udxfk^i!o76N(KewVyMk&0@OpX6rwAKG}3?0IvE?(cPM;r3Az!_xLiYFY&)}Sl<19#fU0x zj-uZ}`Ey9BnVxqbj#D{R24|$jM(dNl2KH#FvbDSz*@x<{sy48Gz=(yRiYW`ofYMu+ zzdPsn^PhpxWX2v}!sahrD*o$$3k;XDHq|HQU^rDKHq%xw$IafF=^BmtY8T@#Z%YDW zAdx@ahu2vaLq%D&-me?D(}&)mEb|5m{{oc6#p!vRnXxnizHWv)adXiBb>q0*jdBJ~Zv<2B}4vZ{P z>E)ayXwPyT&!MqX{ao=#mpGCX5|61&)PEQKmppcZigqM*Xe+;DOlb?AQ8hZ8S0~w3)(nNAK)Iuc7rg zfIT}yB^fVpt`B3Pkl;fBY6u~2&%W5O{d;oadPW=tcE^D^C>VI_JPYukh@TfhQoWZeCJ5B$7I19W@q_TM0($TkNK3wl)QIl3|@|1RCuW$X^KSG)YgdJf$ zD&q2EfNK5$`W1XPc!pW_jn16RK(}y~T4kUY!;u`93tAJiu%lz7ol{&ur{Q zrA4yCFcU|gV0|>p_`D&ByZc`)DL+`Qqx8bmSv%J+qdQd*Y<;Klb{>?OW@XKPzqewj ztIkvI-K;Hlf@9cCVRdISFG4&ME?xbBnin*J=9sxZ+*CAN{PGnwwyeqzbU^u}JEz&U zujyQvjy%LMauULwp0$59k|Lxd4Icntq<^uQ3!iJ0*EJT#GqBhF5^zk{hkBT< zKNwtg4Y`s4lJ-1VzUy%1!)~>kypou8iu}HY$;B}2qhX>w`(0ya>5ndBmNHvwz@<@d z)_T3Arr!pCuZ?)(&jZ=LnXHsU&B)ifpJd12LpQF3x4*zCIMUlbov*YMkDIX`ZQ}#B zDEm7;2>6H|!x9eQMZTTQ#83yK07tV{aiGreb{XKo=?{!()DRH+$I-(B{q;fyyO2n) z-rGbBGoMjZLapRim!$3W&f}tbELYcO^N@9^$@oA{Fw|v>Jo^sP%|m`>OsVrmyd1`r z*_-ScUuU|lzR~%OHT$uyWNQuw)pj`yF@eLl^+;zNjqf~|6huSAAIGYnALff2fZP5> zz7ARH{>mIa^RkT@w4ZV!CXF(cDn9w9CcPN-d;=6xcKKM>?vd2tUshA!XM9hA9JplyPAlKHA3W}2f4;=EdS9$VRk zJd#7BDuS+qpm{NTo#0B*Oj{$Z2l2)5j>joob07T0UCp(y#jl_ioRJq7;CrcFZ;7+D ziT+n)gme?&`MZ8Q3URYd1 zUXO6*c;TeIhsi*l(c2?lau-s#yIh8Vm$bBPLkB24pwd6-v8=f_57U7s_X=;?ZMPX$=V+KD?D%h69Plxj z6s25MR;B`_3y$P%?|Wl%v9)a+)Xt1ovYG0-8ZEx;{wk%oGLr8D(F1mGIiIYKO7qIT zkyAXybQE{@&#($=@kZpE5&n7R;k?&LuC|WbUG$$?mLATHDk-iOwVbXY!1z4~OSn zL9Iql5xuH}kpF|{#T-2i$=3HA7g2YTKZSXE!U$;^53~)*>eS`jehs0aZ z?~}w>o$4HP*axMt=ZuDj#B+$8z;s<~`^+`;?9euOJhNPximpeOXZLVk`?)op?#1LI zsEJ(3NA-`GoL{a>z!{Z>a*D$!ZnSUCRhF+h1{YrQx-{HFin8WzZefO{l z8cNaM;e7wxPv4B1qdM6*FoUE$-f@ij7)Qn+%qi1X#m$C)|q*>heV z_F1E1;>jFo_X_SxU4z7K=dzD=a^~oL!C9SEV-!KD$#mnz60qM-#pJFWBjB{A91?@LxNGc9%0{4?@cU#Y7z;WB&(t+Ux8ij z{ywC~@RW4y=k@~>Rr8pTmb$u=7qLo2Vpes~6>g_ENtTY7^pVeIg!wVc`DUmbY|`3M z-R+tCPAunS>R|zng`6f_20?)pLm}bSq%ja@pW1*wXr=T!IW0oYP6_8+GG^?eKvEc| z0FC0qr5|LsL5JWpacSeAuHLx1qO#F6G*`!D4x6a;L#0WM=HD&Vnsp=Ye)1&&^=NgK z$R=p#49`^kf{*a{V%70)-|osKU4qK8u*Ee`n^}AVgiVqOGq`)`$~)h-UbZ_TpWn5) z4AU%KuIEO^Hr5rLcT?KcOFj<^6-E5p*F`RXe_*jNQ-<*{pcs{>ypy$kvv5&h_=hdL<+0wfo7i8Zr zN2QPM2zwaYFfOrCFU7(G*GymiiuOMUH#o1w-P5{_<`RmBx9=5gvCW1?z*U9M+@ATPF1Psy-Tq}n0&H9|(XuzmZW30{I#a|z_}fb*J@}$Os9qoBgJ+y# zL#8>}`N|}X{(N$J8f*=>O{m7)%z$pbzMS2$yb0xce}L`230Nn-UPkBNZy?Asat0>M==4pw7^P*~|GtzfgB9oEz zSk=B0wEed=|Ip)4I}(ZDBYlprm6N!l&1a{)JCR@4>nZ9els~Gu+`<5ezJ3A;{B3`Ck6-7#p ziFkA{?4$2BcHuw~sGfB+sGG>sgP(eW)M^H@39}u3uf^6HSPdw&q^1jxpusc>E1p9-Su?Z)!3+F+@GwHP~|a`e`o(nklU0c z$M)W3BB{3Wn$(JgntlTNAP(iL>=b;wqp`!xMfLpa7@%+oG3L2vFv0Yd{WYP^a(Nq8 z;2jw%*$3xNJbL7%aTo}j30ZXHpm9k0sVi_dl8xNyUxDA006-~CjL%1|Og^BvD;u`5 z8eUsPX>1Jry+fY`?0PYEo<6g2_UycjSnM=1^3)pT)`AiKgWBpcxjSg3%AirFd5eP* zjvhK=PEj=}3VEoUv38N5?p1FxcdB>$Mz7(sJzqFUM>lEr#N`oGvZQdU_A z`K|dEXc~4j2p{1d#j?jW&BI$yC00u2CH5F#XOFeDJdb_wrIAZDw(D<$uoFNSLNQjK zmiC)`+pCCs75<1NJK7S?oxlh4Tt%Ivo^LVH@gw3D
    4)|DOKg<>hv+aNnO=o?qd) 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ϣ%UY8`RWp~V-U_7 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 CLkb=M literal 0 HcmV?d00001 diff --git a/frontend/app/fonts/GeistVF.woff b/frontend/app/fonts/GeistVF.woff new file mode 100644 index 0000000000000000000000000000000000000000..1b62daacff96dad6584e71cd962051b82957c313 GIT binary patch literal 66268 zcmZsCWl$YW*X1l87)X>$?@vE);t4{YH1mFe0jBE_;zih3)d=3HtKOj};a$8LQ z;{mKizBoEx@QFoo%Q3U|F#Q_99{@n6699-amrKppH2XhZHUQxC)koh9Z`96Da}z^j z06>M|%Z~L6Y&1qSu;yQl0D#8RSN+!)NZ{U~8_aE--M@I|0KoT10055byf;V0+Ro^U zCui_=E#qI~`=w~)LS|#={?)gfz?a>x{{Y1Z*tIpZF#!PdSpa}6(AxtIw;VAx60fHIlil?>9x#H)4lkwAf#?OoR zq}|UH1-_GP?ro-XFe6E6ogAsB_lMb{eMTseU$Q#8C1b*`2YJE2UbHtB7q=F#8c?(} z7MH~UQP;KATrXR0jxH^-9xhh?btgLZV8`yP{4?~5t>#`dU`oKckttiKqS}=0h)-TL zm0*m)Fqi`0;=bZIlJL!*^OrHroA}Fuoxd5CU8V%At$}@aT%_Z<7=JytQ)D?oC4fu; zC9haKy!Hbi0eF1ipxzXiPt=aQ5wop-RG^?s>L>gO@@+lUXG(XGZgCD!0D&Zs4~^e% z(4?{(WBL;9gTH%!vIjaaOL4-?5F%AuAhqP$}Z5*a}4%FHO z__`OOSOe6f$5}vgbHKxcU-p9ue+OOu{ZSHabi?^-WyLLrt+h>i_s0J8MO%1(?6KJ{ z63srC7MKwg5YmV8R^udkjP>c;o0jS%3s1#VZSd_ZMMe}<_%<&|(8tdaVsob9SlD{! zxA!4>pO-DKVwcU1_Qs8{!D!x(rP>~w#&w_8M_z*m4KGu9`d7DfIq*xDA@Pot6Re`h`d%{lBo3am-vR=-J-SO9A>&egV84q&m&9c$A=5 z%sfs3V4GByk@8gn49E{h<(XwIcWcps58AEdX7(zpG>h`7(%)_eh+vz{k!pm%BiGC` z_=5Uzd3aO%4=d~2*uWjw8`-E&TB2z!BU(IgE;XDXw1NdI?B6(MBrV0BsbKgOQ)gVq zTiiW$Yclle$O3+`9mkU9lI}kdXSxZCVc3#pUpLeJh8n71U(M+H_oIWzXjf>?Ub;nl zgr}Vj|2|%YuvXf+F+N$AD`H8>BgpF)5=3ZV&6AF!QO#3~-9`j5fsyJ#B#%vv4OtoE zoN*Lf4;gCHrm9!=;fkWSwnDPm>OzFyN{<}u3vWw{2o9!32OW3*>roJVbmjZQzlG(e zE4}U2iH!Q@$Q{J!?*)q_&o{ma{Zw*#>>xizG(K?ovKtF`xdX~MyHu+y&V2B#8?UA} z3)GS+=ALKVHi<)w-QE08#-CNleh`G&y`sLDidTfmrv{gWy`!r=i}Q2v#-<1h==FuW zo4*3ygV;zyKBgxN{?HQ@hj_U+#I$gm{DHH5VFhB{&2 z43OeSH?8bW8=avoZjrZrTVFiF@fH_w@Xx3vrm3WK)B*ir9HxIFotJ&j?Ql0|_MlDW zFAFtz22CtP@SyIE`u?GZ)=dVaum({0Bk5$QOjPFeR;d)dg^tAMWb#XR zx1N+SC{!SJ|LgCF#-Y>9V0n)&ec+ON<`=rB^tflD@PO&5dd1P!f>fx9N5?Gz0tYaF*sLZO0G1fGI zJBmO(<#@h+D1mjw+HK82Tc@$VtNxi% zE|8*n7FS*<*b%&+mElheV^vn-j|^j#B3O7EpDyIt*oZgUdgrVD+nieQ%oCn z=tvim?Kk=%r6-5a5KYn{cSN(c#);ls)$rs z$>2WG89OeQn+$u%7X^jeuG!?UPZfU>)k2TT`WR;^in+~$27hvw5jonPA>KXZH+n=U z-HdTmV=8Uz@-l4RwROKIHX;)pYhnQ{-gA8{I9_E$1U2#W?a|Z=G1jId8eMbFB2X74 z`tO++;x+F#xG;{RF=LA2>8C&>LFr85=i$Wb6{aFrO{Wxnxot^AOP6_d{#zLQ$rDOh zmx8VSzye=SUQ$IMq75xI4HXEA59Fnh)i7cO!uVPQIAC%WY#)85)HZ%qC7?%_55Ys0-MmZ(mFLWpk4!|Q@tKYGc|M5aQKvdmMnP?P5ZYRPA@UcNk!m! zYM=N4>}|X9#ViD-@-{OA)mQFn9XsaS7Y9(?%-TyN$#35%!F`M`?q#}XOl%HVhbwjt zCD9hq%W@?Vb7iv9#SQ!^zs1Ahj*)z0u^gwJ$gQZK>LPl(dju$D&tWsLLmc6KaS3pr1Z2W;DVO|v_@95?1- zMM>VRwrEw^(?(cgn2z03cSM3w9re}A9@&J-iar~ThaWK;6qbgl9R+_nN+$C===>ifAHw@+mVJro54y_ie`FBKhGpGJfp{7P=$nYHDU85j@aE6xcjU`6`n+UdYu z;k~!=E%i><*SAqRV{@mB5+D#ad!{z`YfsejCwwfQ^S{HX?u$eA4ev+DnZ3iM@r`m+ zLRU?0^iI5+CYyk-JQeAW21GoJm#CuR4}=^0OawIPmLf^Bj+NP;px>mQ@ju91?hU?A z@^6NFDk5sm}DxK#dVoV-L%Npvrr+ooO@;l>4Y7QQ- zdW3cE{K)ywgL|nTIL7??f&XRGbC`}V$#eCsHr>w^yd7NU`;^EDQzm7ei3K5D%lm`+ z_NbNiy=Tm2b-)>1W5&6%wKhpFs?&aw_c-nSe6$OHn}oFM`AT6SSBsV1dD$@{#%ECO zaiNNq2pee!IeZP@I^E+v@_!MPqwA4mCt$2(@-z0LcW4k^>Eo>KuM~B@sNL97E6TFl z1)4A2mU)d_2f0GJOww_Oc7q4(mz@Oz)qi8`E+3Ka*{~&X^P|?>khUM&hA! za-0+zz-fA;NCpK8V8&lEAj~kov2%5g?yoc=(AvRjAGX}w(W#TavcyO)!zy( zBwy-z_~z`5c)^_D?7n6Bk6s#PY%1IH^>8*9DYTP!!0{`s;pmNC!t)DD8_4WWoHDid z?f}^jLEV%i`>#l)r6O{$EICF?lGtwyEIZdkw3-n3GcpRG_G3g24WI%{ z$9%gN{?t7?aUhEagsS=Crvcft)p%O>j4XBnA15^iRW@>yZTAu@VcFtzH z7Pjzcy@{m*?pI;}+Li)cVqSjK+o9$8<#htd>v|Z!spzHUXXhL2&VAWwmO>TOz#2F* zLKBCt%h1UO`bcZm61+W2uiv-$*AWdy4%*JD#Q%mVN~LX?P?L)W5)_vf~Eysd%ifN06o<4DrIb zo`rgBZ)aY-Er1H(R(loTgeRKc`aiNY*ov~%7tdG23sIk0S|&| zI`ym(F~+g~Z@5Ak*#hsXsk%wMma1o}98R11$`-WqDhE~YQA+mXDy(Q>%<^37G)?hj z+kV3owb?Lm^=xvbUF5qgnn3}%i9dP8l?^m`M069e_$gUu1G~Si$r#Db>RW?Xxr1i3 zU}3e66CnC_N(ryScVhF%p7!Zs;o9%K&6EYZ3oRWH+nY=r>ML5RV}UVM5LU3?&R^3c z*yGY}>NGt9GBX1LpI6=voIS=^Xvm|6n<>r?b&=nFv_-Z%Mm7gp! zSI@=w{S$c{z45YBG@x~lPoG6l=DOXaZPZVlw2+33otl)CnYysT!Y~2K-zCtw?30-Z z+j4f4G}f{>C*}kX%RUJeNc7CBpe@lm@?8X1D0HyuJA7fg9{pXg(i_i5pHz&enAz99 zWY3;MKvcgk8C$XtDv6Yv9nuV?irv9MVk&VuUm#O*IQgealiPX?FMl0-hGD?jlbT|; zME&f##=f<={Z30HDUKa?&A?`}^JL%n$By&#!^_LLX#Hw!dL^x^o6ADIYq{oZ_wI$f zBPDV!nu9vX(9U=M4q63-<+v6a=_auzKjbnp>~RgNBkd^lU158+SLy@%Fg|_0De54h z^rK{5>e-9~goCutBe7pS^s-`ZU@;qFoc`@|Uwyz__~mA3V5aaYCZ<4e6g-K3SmT;h z@it4I5vQD*>)Q*Fk+6`Eb4vzkclOo0&Bf~(wh1Wr-GBRg!}h;jXKPr10(}{2!1D1% zZnFF}mr~=Vjw0b47Mu_oQ`l$EqB>V3NVJyRF^Qh4r|cIXJIkCIu|e32zE3D{>g4&%2EEepV0ihrnN0lI*h$OJUUNEJ+f5_s5*kt zmQfjSrXy0*UszZofNBGqi063mn#*;wW}5WUXL;JVcPLTyPpbj}@IfE`+)C3>1iy6( zj@xZ`!%VYN^QX6s+4^nia$?ubBc1sgz=wkk0rC;u!2s(j`^WgqwSUq;DL&UAG&u(% ztx2nnfUn_>ZkfgUW8E9g}L@NcOjYNW~s;MKbcH~h0cpk{_HWNdfijblYz+h2z03P3!{w_^F+Z{6(m;mYyc?e=$R~S7W6r)rmnhc^ zWDY8UgC=qhHXPr6E&p}OFapx)Yqfq0c|%ScJfo!5%;`l<0^eYMGZSctYCudt4D;QS zllZXAwPzujN)eGld?PN9>@xFHYu!q3RYPgwD4^+{ZX+R4pqMO?|LJJ$&|pqT%}z(2 zws%$GBS~6_4OO$4U!NF5sidchXC;p!pWSoPq9I=D?mxL{Zt)>jI<~1LE1+Oz;S?N` zsjnlQu+gxjSKXW_*MzO^o#-wU70)7mu(uLfuB-0YqK5E?-e-<1nICGBYERzbSu?t- z1J9I?E{8Qu_&Px*?|>1;GK>itJ}M{~z2zc|c`DfS=_rwR>wbvoH*rc9Ca=CCq-4Jh z+IxAat$A_beud7*u*t20_~6e9o9BJn_Ho1ME|LyR2HWhz8j>^3+Tpo;1 z#OP$C#H+-wZB1(eXsCdjH8Y>Be8*l^l2z0+y_nU@-|33tBxzRwJX*%MM2dIi{#=IoY<7?7I@41JDTMl z|9r8UIP#bjPm~nR+<#Sib?~q)WS#taf5E>&WYVfkl0n+1X*26v+XO>&f<8pb)x%vS;$rMu{Rcy+BTIL?an0i7iczQl+`d} zYwfz$K@_rR)TcHqJ%uE`{3$4djVoPQ;Hn?ilq^IOYxj-eWN$8weIZ>f`k+fXTv4XV zxXVid5tejj=$k{SJ|9C8d_7#uwA^RYU!2J#ik0bpw9U$J7X!0I3Cu;srmBFnZmXU! zu!~xOmIrL+e;d4Fy_Yn8BTM_b>7-kEqBb{bS3=bJ-^ zArybG{xTk8B}Ff%l0yRj=@m6PP)-nCvyy%R%;|U!{>YrP!}BK`AZ-hu>ElmSHK=&> zEupkk&(|o!b>Z|PcSs`6=3@`isI1|I>wG~8HCk8BNXvslF zb2qb{NmN5#uR-97^5i7Y3#R5QJ74sp0$r%yKu?ed&+ivClsUAJZB~9o<~Q6;L}dp| zgxwnq#X_ME*@s7~+yMyT#C>E|gD=JjzeA}2|Gfez+Cs^Y@3HvO`zi4Y z2oH@RhUH`=t1aWXIifih7aEhgjrV*`ZHH6adZ_+ar&ZyfD2E$B z6i?p|;Ppl5a{2F&Nn$CdcSjfBzTQctXYmW#oGbBx!zpUKne^JrV-1O*A zte39UNS;l(F=?FNaY}cPnV{;IWxW<}kbX@ieFQx@krv%HfvG%4XlKg9O7V3+8>hFt zsZ_-g>;fy72bHS{qLMf>2diP8r87W*IH+%^i_F?^Vcf&!KcIFoE=h>1+K_QCN5_s_ z4q#&aN9h^Ld$%bf!>GnfOUhgzxE|*hE-EA?ojuK5A@-75Y%0`lR@w?JsH>*y%6tpk?I`Tui&N%cfoY1R<> ziTCSG=en`fKl@2rmFUkA)=$oTW&^T_;Wp@KWjYX;@4#NB@x@!36O)_Th#4Bu=8*MK zKC=NwyP~_@yce6Gz$)Y@)bwMU2i2q)9rf>$?y76AlgTZUdG4W6;#_}FOmo!8WcV9? z=tw8waqML#6=2IOVbtwANc83v@=3>m-{G0{Ny)8;7W=g^yEtkE^>yoYbICa)d+sE5R5 ziLK%3zGNws91-!M=Gf<__>gK>e=N=WaVosXzjacH1QSgiHH~f)O#=+XaX|Rsy<^PZ z+N0swA*aXW@XXfN_}RltlFet{@n-5?bzS1KAire&KbctG3g4A!B3yFxfvaUB0=oHU>7e+qgGXcrRVL zaJBKZ_7?3UZ~OFGJ@XP}4U>$LdyBF54(1j_{1m|hWwpUDgwKj})AR%%l7uYevu|w~ zkBOe1zQNCkzkSc_-nZ%ZL1wYmEb(6jIMU>7Yg+K%!3ogU`%s>|sEID}D>#`ArT1Xg zY3DbPR2EFVq|exiDiMyL{;h7zv1OiG^7pKqV>Nm=z2UX6`q@g1l92J6cc+a@kZm*I z1)8d3#;T!<7VjIabqo@eyQoJ)37|fr}Z$3c;pZLeiyn9}` zOV#On7kX{lo-U2XtHNsMgs1tS-$8(nM4yol$L~+TU_|hSo}B(aT+{L@Qqtw>&LoFVZ&5)JcX<|jF-?{%dp72IDUzD0V*CKhi2*j^8=68STUt&br&iVp zT&BuNStFLR+Z&i$V42R4;X^c+lSmq13oJAc!GbaOKI=Lp0;>JnzgjCjp67xP4qg9a zdR?9CTpwbT3D8_T3Xu@c7&a8<3RUEg#=nkbg0w+8cqc?u^a08zbMm@Aj|2z%eC+0^ zql|__mJH(p_&ZY9I9)`pcdL0P#sxFdeI2ZfGdQl2{heylGP}w_1jKaz3a+xS@%id) zUXNpAXIJ~d{kp)a&3uJ>KeBkF0>+^h%Q=^5J_{f0O-z>PK22*&cP1cXs-$D9ble+= z=~ByXN64k!9VyHHrr*1R(d9x1ns%vcOG)`V zQ)GPJ#*rwA?dc^MkkKtXkNRsa6q5~dJ6-YNo3j!4o!ms;ejpQ=^?m|rTJiRsg{K^5 zM7|8=3C>L;f(3o71q@ZNtzz4^=Fuj+G^&VWgU!g5T&)PxJb%5;=Q=oV5ZTVL+>-dx zhhj@57~9XMJMd%ThH!JwXU+%2)FLU@1Uk_VOT~m8v)Dkv{-tP3(1{W3lsxylL+)Ams{`mFkBBHjmQA(dV4hlVkETa_SZqb@%q znl$-FD&x1SE-}P^LFZj6804F6E=n>Fjh=Og^ix@pmsBrc;SD;KvAb}^#tTq|XnPVJ zpT2sEeG7j1wQD4@_IZCbtQ+%9$cJfH+nzm7ZuJ_=8dWlMMAS=kbX_atKBec%d{?j6 zMT6`Wiljm1dZ+vZ>{ozBVSFPAiexw&_`jBDO04g7sG4t^{7&T_s(;7^OJkPNAk7EeNPJB+3 zvnI>9baeSf@IPpZWe^9Ev^W9*!{4{x=I31$Z|j8kg4qYeZnj)K>zaEC-uPo>RSdLE zc5^nm$Is!d8}Ln;f6P3~vKgXj)_-B2uSEdl}Se4P3<09 z^@w?vWg%xH_Jh8+7{G4dT9PLFNw#Cn%B3(2XpP%XOtP_Pkbs9kV z$Q-3kxGQq+N6qKq^axgH)t_hF!-n7lva+Iw5CB1Z-2D814juglNK5g0+ch`iw<~fn zBWiwk;dB}#ap%1RpZax*IFkCNe69y@xvGr^2Afgy<;hRjPZ&4)J9UVSLbPd*Li8;& zj#t5gx0#(>uO7y{KHFrUSnY5iQ0@N6dsnw_XV|c+=cU4sBcs8D_UkF3q_a)o2PEyF zbx!;+GWe_i*JgQHGt(zo)>&;KdH-r4|K=fgzy_@zMbL|azNlnsLrvmF=z&Dr_F>=o zOyF^3ZU?9&s$M>Umkl(GgqVraCNJfNUCn%G@b_nHt!Eto8>uzL_&DQ#UKq=` zEOCp8rf~adZdQ?Loa}6dzb~63LkY2ne7g0#S%1Qt>FW9*{J};0(eM>Uzxxx+Jc=Sw zNbr5M_&QPzoZD-!SVIZ2uWzT1bQFtWLBLeutjw; z$)QUUFgL}$slTMW_j9~~-^lx*3A=|OsaHGxyolndAN+|6ft0Ht44TqVo7R95)TnNp zQPr`<3|W_hYJ{+oFnY|oclbRNqpM?1ZI3)7DWPW?MC-KgzoKB4o$cuW)CsOirDD1w zYu)U^(;c3@$p6$5*I$McZuo=gLiFH--|M}MGVvfh^UWW1Xk z488s>afB{8n19#I#%Qg?lGX-cA!ZQ4>3`_FPJvUKpF0!VF%u(QnO~)ezL2D@n4T!J z^TLk=W9ioU>M>iMaW}C(=-VESzwQY4UB6i(J)vX3hlOv*D;9`p!YA;Jo09ZALCS0x z``9xT+*}tmjgwkb^Ht;=)Ha!3m$Ej3da-!tbc8;59KaUhVqo*5YWio)fbPmVPBcs1 z+E63@FJJHMU>@vmiQydDtYDEDw-;?c`FlUhl)EW~JP2Mw#)x;w4hND9y52uN1_s_U zbd_D{vg>WVjMxf{SyxjYYv!SG;qijw`Avz%TbMSMhM?mvIZsNd^g$c$N zjY3h7e`WP_q^S_Dy4f4fx-AJ5imltL_1J#=C9HNs((E^m&@8SiY?#ONNoMOI@>V{| zzt8Ato5|}rgG6+Vlv&z@Jl89_!mE$lDYbygNM$O9HcfPZ8)J&)hQ5)GD`$Pp07xQF zz?AEtd23`xy<1Ka)JF^Wrs@gF){X)*UPwPU%$$DHY3tQ6>{Qy( zI+f9}N*VO;dNX^!aO=whm+vK|KxofHRE+nIq|`WcH)SPb3^IW+jjZ=GtMEFhD9ZBe*g4qo_y3(B`47t?#J9n|fsREt^6+oZnYE|O>VMg+UqNs?XySy+NRDe)ZhJ21Dg9^xuAx;~ADlE4?&9K+FY zLY4OquJPQc%9&G=agFz$sVapHEv;W~Z~-$7(71afdx?2z$CZQEcPm+W`E#ptJe_EF zNs=>4HZsJh-4Qn(h6^Ly;cS>|l~Oy?Vb**xPSqlKMvd+md;Jbp5$L(AjPu#&qk;SC zAt$%M%wCWtQ^L+WOVlob&+GL-GaUCk#gJ^FLpSQBfr6E<#a#buo+bMG8I6`=zw;r!Zr#``Y6%cj7(T>{_-N(%43famwv!j2H*;aMnE} z3GVb9&|gq~f{@+%UQ0=%)KWoB_Ja5(-oZW5k!XrVeL$#1)yf?DPP>*7gtBIkO=2|+ zk~!gxywqm20328+c`k!6&&}#+`iC12b(fR~H@v`kgQjgjkhYliLxiiTJFyoT;X5wY zcxSuxt=;A-b_ohLABKbb?a(Jhv(SoLXjJ*6#VgC^Io-IMR~6zl(u$kjz>u4tzd>T> z`OWiT@O8#+O-b3Dj>Cs(NV8K4hT@nw0v)>J!1}~dmAfC&V&Zcm*7+tb&a0Z2n8`=t z%UU0!STkH%} z$Gl|&T*vRGX=^F|=5m3yDO-g-DW8gQsZGYyk=GWZYos0>I=7MG=mlij%mv9*cE`-i zOfyQu?`5;Xqoa6A?@IAVZTZ+GKMps-AN9#tA#vufqKlEtZ$svUYH7;UrL&7ymjs2h z|KJgsm=GK=mx9x=_IzQv$QXlsJgVYsJOU@iW2Aue47K{Mnr(% zls~)ux`ll{bGrQkeB|0MiR_WX)dU3Fd+OF-Ge_2T_8?>Be~_-;ZvT)7Zx!wtQpoYp#(5_i;Y-fOez&Vj(Be{*bW0QNL}yF}Evr-^v_z zz`DK8xp-uCA?9=`PCl{K9OF*$Cm#5y5;OM?SL#}a#eLWpBhNG~@!M4?Z$4jfC!=gm zwl??6gY&C;;dY!;dQ0gQq^Oe0;%f}`irfoFJIxYe)A6OkkC#f3**Mwr55;81L&Q#h z4uWd~D;nFML_bM6Oc{`GjE-N8*A4VR6tbVinQavNGX(AZ9ne1yAqUQbT+waTR?Mf- z(1^OPqjl>UaH%1+UOZPb@dmn)9aTIjh$&r~avj7?&MSZ7ScL*zE({Z&cFZKv6Rs=B*a|GANc994A_xCl+Q`(OY-EcW-Fv$LZe zgIZN8U4pg4tAIGcvk0PLjwhoB7aq8huIOyN z`E5b`yf>PB|DN`}Lu}QTO#It#`Hguqc>QFXWJDlzEvMW0boIu_)MOBy(+b7MyFJ?xJ&+m}|daP2c&rshQpR z)GHe(QM5MdovXb$_%7Y(vrNMUtr4Yjn!qiQA=ixG3GH;1o_+P|hR5akMmE-M*Ms|i z1zcxF_VRVeWruX?W?FoDYr)}h6sI*;r_srH#qEkqTOKig7dN0^n|V^>(b-Xe>rT4A zPq`G!qtB#EBi#=wtL+upix1#Ta)5CyiF1vB6@sz*`dEY%4RsHD^&B9-h4mg`dY8x7 z_qZ?9dG$;j%KN(2{QcDTEikCJ_Yp)=duVdShqLMXqUZcR+3_cbp=_-2mp(`Io)J~S zFAl*AZH*t-rHT3z-tb6K2+XM0&3jcV?|oi06Z^?-6K&(f?2Z{PdVr08yrcFtJ=|C( z=PdRx-g375e6xI@43*Vhqn4SE;3Yl~Psq70Wa5WZ^LtC`1H@ip$VdGCBQf)3_^>k4 zr8Me`cr1T*IO|7V`=tNF%G35Z>{6%pImj2~0Q;yab~CH1QLk2})BHu3Nua~R0DD-H z>A@MT%`-#?+5~~3RlX7mc6-3{YnmIpgXfG=rKza{J>QoaRBXcUsfJY*4uWc4>uX>f z;YN5AT$9%>?^qn-sI$j#<{O|-pa1DOuQJgXN#A`IctZ)`h%a1qXvX{lQzj*xYo&<$ zIb$i9ixGfSF3|K1a&;?++Es`CP>1Sx_`Wq^a^Se*?(=izf-dxS^D=3}sYHF&%Wb0k za~X?P_o-`s4p?eSoIb(zv`qwQMo`-^0!B>BB+T+wm3*IbheA#Hfnr))SZBHSAZ z4eS_C>y$B@v{{G>!U8*7kWc{peLy0kp=;NT3SR=uIp1x3KEH90sVP5~g!6&rn@eo8 z)nZ&OldlPLX+U5!^1U@L)6d%grvfNvT7d~YvxXx0yJV+JW z>V$;VyO-ZZvijEI@THu7SJuJ(+inZ3f0%=5tYhab7?M?1VO-R7eYBwUm2FEiVl{W` zZsI228CZIWoMRr6?Gcg7e9e7Bm3{3${S-VrdSRM!kyYZW<<7V>3@JJj6#^W}Q#Oyi zN%4)!(CAN#GA-bbNg-<&troPLENSK6__zm49n`e(>h+4tVQV~{ntLxMDPP2`Nz9UJ zH_j{E7~py=u6`1GlT;;)+-1FmlHe*=2^YZYYFIU}s3x(QEt;e_dp5GsE}GS;Yjfwh z7WJAw0GcYg)F&#+_2+-yZTA@Mp9OM>drJzdj~zNDCUWcYDbb~6$2~;H&5@&3F5uyu zlpzWm>RN&8xG0O4^Ei0%)0XknL?Gpx5$Fvbj zrjP@9?#yj#Xi7eUK;y80gEP;1%|p0ir#CX9vKy}2+TlYwuq!QV4cjgh&3SdJ;^KdA zrd5@meTVihq&d?MrBRe1Lvi)Yf8#DlpkWs*b>Dg(qi}a)aFM=VoUPy8)Vd+T${eM{ zn89PbY{>3iDWyJGZ~XnG9eM0MKSccm4XG;XWQ%qRs+l(S3R&(59I)|IoeUosjNqhM zul>F@wJs_|#T-%vEua08J4^~3u%sFcdd&PM?upyceQ%p7e}XY*D5+1vJLo>+gy`M# zOXV{DQ0gX?5jtyb$ECyt!sTCR6s&`L{8?GvqU`*yxEA@yX5<-_Th;O~_UK4KL-(=U zgY*m8?FK(arYzh(_X*T2IqCB>qWd2pI>l;Cdf9nyNZ6I0^fkMVV=UN4-YDjfAN*9y zuGA&CPxFNRUGl;+pIsOao{pxAW5)x0aySe1>=7zh9G#0S{5Z@B+>?cFp0qknz^GCS z6Bl=f@_agDx+q83L8Vgy6^e|c04=289z#@%)S~3u$sGQ@#O=fR_;%re z{piCv?e+oLQf;nbp!Ya-t1~tpDHqL@F!dX6y%tVVF(E6JmelcdSdJpCHb}2;}aa zkk@zgTc?BFnc!0xqF%uxtrDf|_@ll}db$DzXKtS0nY$x)?oyw_<^k($+OZp!^JV3t zqH5tCLsBDTLEhi8`b=bhnJ60o|M94@fr80rc=m=vRMl{963-HZnm{mC(<||dNX8Lw^k|t^_-o{YXWA-TsoICH6tPD%?-ZfK2mpkDK zHKi;bEQ?_1qCcToxpUrTS(0QyRXrj`DSAkSu&^t51+cny?fdvNZgWPtp5Y=K{br>y z$ueJ`_-D~ANmmIx-c6(N{tjp;N!Vgxu`cM@hv^ve=8GF?zR zK=wg!M(GxY7zq#JgTlCd*rj^aIc%A`z4T~MeoS~-L$7tAqO@8?D`jRg6LZnH{+iH5 zsqdFfY~M#4AN`&5w;;*w=>1y3etqDPDNNQQ&;*UP9xbpL-8+bRstIN`Gjz0UZ(J#` zb5V!yFAQ$C^iF*Ib-~qE{BI>0DIP2a8KgkXn8~2JW=rs(roFg(d+xQ5{G~gRYcLP2 zvpxnoOKx#=3VU~tZyiKjK8;euXsnS*G_BjL2ozE;;ozoD*-Id}SCnyDq>g6J?ac@q zYtQz3*CPn8_C^exl^@oW>{DwX=u~i8@NFfLedDg<$f-MYd#yOQ$?3lZ7x=P}MZ_iG zlJ7>8Xab@bK@qRtYOg5(K;I+!z-N9NsOl+j{(mxiPTW1=EDeEB&S*32c{p8cAq2 zL-QEor6gyn{fpi$?UZdOh8;}^EcDPo46s&;TWsLb**!d-^UK>_-1y-}Jcu(7B{I8x za%>O##Iwe=R|0O=hR*i_5)Ix4L6vT%0M7~P=zec>+bfO`jH5M3@8f!a{m`j4dquPR zH_iLI2iDDHSElfWyDqG48tP>a=%I z?|0#@f`xRF@)L76(_pQ%Z>Qxv6_p$PDKAYWr_i7m@tEFPv_LU_!9@=I=3%z%KRi(a zvdOJ~bDuJ>*^y(lGt6XAHu=?Xk)O;_{6Y>hK9su*UW{^45yDx#At2tg!huQ5gq!;z z=bqLpDqHH1c5Z~|skW)Z2r0{M99}}a3r3G4=*rc`o1JiVEy*8&!Ih^?7cr;?Jipx4 z{0FUX?VG?B)}wPC&QD1c#++01q;9HUv?#Tm-7)jMX=Wt!dmbh zpWusIE@O`jmu8<(HkOy4|CEQLZIkXWYm;jei4t+)W!kBf@ML|H#M>~a`_~=ee(Nt7 z5Lhu5(x`IZgL}P!kOziuX$zKO#1s-a1Cbh;&9=*)O|~Ff4w8+~ZmwOZ^Dz1y@ATWP zV$dx^85>bx^Tde_2v(gX@_Mn3cl{)0J=G5XYOBxqw>_xj1%gLdZBTu_JvfW+f%)lQ zT6o_EhwP?1r+_(RoXlrqNHAfIAkVipcMEJPD13cfBt*f=UozVzQ9$;r(#tyc5g&fB zR6ilW?pNAe=MIEn_5bBVvx}U`Bzego8U0XWPM`I+oCWeI9UB}|Nrep<_p#0X>{z5% zD8~JGTyqiSu5rgWKXX!=-}6uS-5Z-b|AZK}v-F%&S(6 zEPe;|5fF5G|7eKpC2P5Hu@ zxXbm|NgqQx`l7Vy%KtK|P9APXPkOJ%QcpOaCG4i4Xeuyhb$w?AR-fN-UTc)L+T(FQ9VOHyPqPrC? z)grB4n=O;n**2AA=1=Yq=_l0n9+A}L**0X4Vs)YqRQZM)FQPynYW>(j->PDH{cQA7 z;z+-c0;7&W{q09lboEzA?YUd#mE41DMVt~D8t3GsmyBw{%2Er%A${%Hx`|B`HB}X_ zb4WWqF+IsX-IZd>y^L-)bxC!Neb{|%Sk{5uGyj{FKk1Y63yBbEX9|}MiAnBb500$5 zx7VE7F)#S1oo?g71etXDHPL#-%0NfmLs!}NCqH}lU+8C*GAJsH^lDL>Wtj!_RD`?< zaHfiI*blCmi>&wQD4JTq$*Z2GuQTg{;sK5M-B^^eh|UR8=khTgXo>kx50V8|r;inV z!)B0AhurOYjrd+-SGDpEThfjoK7#SYCsMWY= z>P7YkL5+9PBB1LBe=C7)A={TPH?y=;=u%4D>q4$|kgI_0(cn)AM?EKQC1+_ zKtX`)Z&cci!uc8Au;pf$*HS*@=7AL4=I*WYUQyXMoirTQcf1}d?K&q&=6^RNvgi~4 z9t^(us$1rfxe|!T=JH|w3pv*Jp|}^Re$@y;eC*>{b4_#10U`K_`~zK|CXzznaLMSQ zM88*atx|VQ(@>+G8n~djt&3|BZ!4f%4m(OHQjz<96m0ixKXfpY-=2VC!R5^CnxF*( zwKtBn{gb*N-NpN|qeQR=g8@KpQXDmac0nBla4)}2?r)G1c2LXIoX%&_!h&k6Zlxe7%cZ#Cp>b_Z#CMUt7GEg2T2-l1VO(=3oEh!?bzm z&>D)f3*B74eq%kzJ2tBGupu3k;ayq}f_rR?wA!Uivbkqe^h;{{pyZTmMSYNUz2Mam zlPq15NX;Kirpnns63I#}cUF-qq?ssZ6s^~quu%x3Ygls-sb{0Yz-X6y!kiPgQxj;a?=n<*Vp3XayHTD@# z4+Kx|fC>H$%O_?rHA%z&Yz09}1$an>(m!E8bJm-s_=QF?#~{aET=lUZEd(p8bHhpj zbu({YXPZHzKrr?rBoC4T4@#lLdWUL;K;Ark!9`|;78CR+3c{Aad~tXIOpgeA&ZUi+ zmR2VTFF0z@#$LX1+tqA2=K&wrCwY7rOs`~@J&hC>7;KjywBz(^PV7X=KY0fLj!^;d zNU((50g-@?a%j-(qJH@$o6S?V#vV$Rt~eGx3rs4iQ#%^CdhWq<*{n)R76NFhMkzy2 zgK@sU(m#7#K)|0Wm<;q)zB8p{0s5w&D_Wo)z@`@%cpZh~--IGAE`9K=mSUS+>^$Xu zeqW8$3>z9&6tWFNnqJ{Fn?-b}uvg_^%?#7R$a4K>2Gf1aBgbo%X^QLwIP$>pKBkCB zLO%UxlLbl3sjL+HZNntR;+Q;`GOG0Z>jg zmlY&Wc7YiVVHw`nZ>%*#%7Fo)p?~SI=nfO28*T;G_pQZ!sD4_62;v~;%j#8D z*q=JSpA|d$&6QQqBQe9VjC3 zh9o2m;i>M00DtxAVHEMw4=N1Ew(RWiY8FZsEiB`*$`=+<)dQB(=hiOOK44XwAuHy6 zamDmm^V<^NVe~SilUnwr*1p}T=C(|B@1tT~SQ3}{otzI=k~-!pS9H;5pCu~&`THa+ zXa0_`E<-ZbP}YXe~ecQe!#dJ*3NoDRAb<jpsxKx1@jJVeo=*MjpnVj( zEE$NdEEJSe@?tM9E^x};X)+Cdi)Cl_Gr!OJ`%D@q_N}2!8|BRZV}VzIPC8Y)kO!em z{P`^`La-O-bi^C`km6*B?ZZ!WFi%7gX|RYiV}ZrEO-+!B^(3vWxzlZorFZ+20AI16 zsk3?L%H~0FvcJGb8APAmE^m4~a-zvw>U_+;8Ur`Vij3nQ8f~P81WH49EkQaLNWm1t zM7o0H)%p{oIs0dG`uoluD3^0?Iwf0T$HO77n?1>O`-8||n5atn!MnX@D_5(>O2uAz%5r!#A7&QQqQWT37#AdY44R=aACIL%i*Vn zD1kB+ac@8e(U6LP3w*FU27y+5TGSbT6Xg9MdctdOHFnfeh0^6c%2ARj7G}QA9~p!D zIC~01GSW-?fL3JqX^ZaW0#x-9tbHN>hA|#DYRNY)Wv`;MB7<9ZtgUO&xL38?#n?eZ zq9(T;=Yh;D+iyktMfRK~xWASX%nuWkI)~qU38o5S$uN14?kQm(Dnq;Q^F8fg*cg>TA4oJQ%ZRlia zmQib%rxv0jS0I2m9;|A*qlIusT~9EdAgoJq@~=lMuzq?k24_6H&Z7^>VHNKb(zxxh0=$Op<-76-3k7Eq5H35 zhiuHU{rGE*qK5bYJtPvH6!(UZpeL90y+hvpwUK~&!I+-uL&=tfRXk!4fy7<>mg0tM z5gF2*zxlCKh1W~S3>`rYk&WRC+a;pEAN9SXOy{ff`2gWH#@>(9XYxcmc_BIEiJg!E zP6c}dE~s#gXT3(@VPW28<@VkUawKroZ!OpS$FM`CI1r;~oRo$Ph;w5?P;}beNgZMjCx#g4!?? z!&LY_^-$vBc0N2cSQCj6NAI6f>7F|H2m*!)h5|37#U=ZoIu=U-3d-WF%34!MX#A=^ z%z5PI$)x4R;g^Y+YDSs6oPji3g+>0T4J#P_qWe_nY`>vwl9pHQlJRVc zPR1Iy(h^veY%P|fu4G=7Z5WjeSRsYh=RsxWXQwHi@)BLmi+_`^mUI( zU$+l*K4j(~_z?KfLxfLCT@_ytJ?ZMMYwP*yK_XV#d1PFJtFw6I1t>;5UZK!F%l^{B zoxcsbS~yjiQVGh|!N?pHqirr2u0JA1#vzF>YU>%X3OYaK9$z?qB)*g}h(%|(fe9YD z^$pD7c%k>HaPB?O#14wkq{Zp9zD+XCE6<@^w`@k1H=u5Dtc00Q~_-C_jie3UGaF zF7FBlP>@V|{o%B^XZAV+>uOr0)LlGr`=^`Ix6(8T`ycn%zK@%6cAl<1P3K*ujBRi8 z!N)~r8u-{Ah=u5rVTP>-G0~EN*`uRe8YKQ5eSA+7LpC-NM zR!QT<-p-KjZ(F@#BAk=EU80_U`f)b$R91 zh&lcuyf`*4ETc&Jpjx7JH<2{6}dyAD#bMhmt zPI(>Lz@=zngFxv1B>?~l6D4YRAPv{OE>!)`J2ZV~?_1<}%&vLDdbr%N0S-39S+h`~ zf(cRcP^+)rJ!-yW2ejKSi^F63JjdeYhH`?Z+b?c=;Xd+)FWpscIf$x9#ZzwLPxnvy z_CkH|4d36FMx5ObxicOgwbyScPr0L*n;yk+upRv37iF~9@2s15ywam9M@lgmuIfe! zs3Pk`TjHIXez0JR4AVjXc@(8l4M`^$FojP1_1G2fs5i0YmUVaf$sgd8zbAXYaBIJ4 zaPR>700;nj0HD7!AOJi7@L$BVUm!F9U;t2eK$t$@-h6HVfLYCogCVy$$YXoA5Y3@xh)+T_)!ZjoX`QTufJRt&hP{XVFZGdlq$*Rk~GED^ZXW-&Wi7HPzgu`!Dy4PQ3K<( zywFs-+cCOHb!UPhD7lO9((Y{*j!=gcgpO^J>OS7vRtGo$`9d2+9Y7 zHHKGd*OE#6pc}7nLfksM}n%-ekpXs9W2`}q5{ zEbEwW#6gl%E-O^p!L*8bGwJHe8J9zh-kzGZL391=oYs!L)pafLQvMO*Fcl5~V z8P%27S-LGoH!k&H^)dA|?d#{)$hY+~F5J~{>%X@JKrQY*M_fE_)pG$f?6K5069Y9Na~@+#nS z0P-$QE0Apf_%5b9FmC|9JasY(ps+%?<6pynNabOge{IbXu)<9LaVpT3DPEL9U^*=3?(8-QjidsBtc1Z6$#8Uo~1tuf;mQO z%is~(#lMW=AL2{?V^&xv=Sc<}$2v;M)TJqLRb(@dV3DdQd73}Am}nGQN9HMxb=G-# zr1r$_3ghMHEB;|n#2O4|ki^)E_8lfS%5?A_E;uWb<)9I%n4@(D(h+KzHG0J964jf9 ze~iP-T$|K1rE`k)822_FY67YVR2jiCk*SB%(5vKgHRNiFxrA~>_sa2^lDJ@Y0At6_ zrkZABE1uY5v}J3_tQ z3k2`W+69lAQDn;SpoXUE9k0czguLi|uSK+m(&}BVHRGn08((njr+{}S&5c6eFLo!{ z_IKL_eg*0Fx7!7O1^xE-L#Pu`Owj$;kDMWlry#A2&?Jn^AXJIyCWvGTnH3_{ucL5D zzVl-xtWy9vmu)W7NW_Vx6Y-4-0#ENeBoDx!wAO5+I`eAtbCnZg&l>bQ+t6kI<$TtO zH?c-Iag&77e3CQ?)tG~03O7lQ1!rbdYJrP|UV9o|QR$h?d$z9$g*qx)L#Q=3*C=g6 z=_S`pFZ3C3NmUi0<4JEoR%~S^pFEpipu1D z)$y|YMV-#VwdIa8CC9F{^FrIy*3q@dOHJDF#2)HHIJmBqU9sD`*M-@AG2c=TE(*jt zm{QO{-$;CL%s{NcjlFRz4>uMsOphpLfuaHiOWd+3dSTeyiTX&+!QS1byO%d>0?{8N zB@oaCH}>eW!#ZxUy0e%`^UCxa&#X-|k4!r_%w;oQ z(xIgY1P0$%akLD@E+c##$YY1f*wNGWH8&%@9QbmFDqb5!Be5>|&Z2kgepR|Vppm|@ zzP>&)Yp$Y&HsXxkLrOr#8z?XWw_+Mn;B2Je&&{XWp0c4X@L@d@eSk0^w-NMzrobJr zDh0UGS^^=oLT;wP#%fzf`go1iEbo780mSluHlfSw#md;xacA>VDUr_4jYU??O$GNU z^)Z1@Bv454(0gvCz|5HcHhoaZkCGFY1 zBL15WE8sgG9YuNgTVz&AlXQ&$II(fOm!2Y@tRSy=SLju8KjS`UK^)l`*NLo`tT8U% zU|D=1d9z;~n!*8&P5k8HnBb=2O*>FS5o#7C*@QZHb1Xy4BTr5M!liKVCvG=)arM=M z8U?^LX6X+BpA@<{yENYyo1IdlpJ-HpU4>n7RAkW)D(PuIug-iAL%F0`e)}P@ zF0wZj%WDcn6LE{eS8WHGoHR{ha49V_Bot#VlvD1LA{&u_l0-J!Q1QQN4_X1QXS#rr zg2+X9qy3Z)`|n|rtIoca2a%&xz(1V-JiIFc;tJdGwsYL94|b4K3eI^fjJ9XD*}nI+ z=EDv#tBFKY`)FH(xHhSlmhj3iZcjN~xq`?5`GE5<0N!e8{_K7V#(e z=I56iKKyZna&ofkn~JG-0Jc)UrJq*`6mV;IXx#^DHUv7@-V++5sMAstmb*iJda>x6 z(C@R>%bg@3ZO#uREUef2(gtUO6vur(Ou8S4uezfBpby(j=$gTa$6MA$e!!#QE9*|I z#&MsDa|pJ1U$n^}uj>$5h_I%mcmQaId6-j$6N69KAM!-Bh#v?OD&g*FT}Iqg+Az;r;Y+l zV48VoQ)MbOdayno99glE@g2}(W^E2NfqvknaGOAIXTFKq+NH z!Z7V_J?breAgSDl(|F|iVp$zj9@(5~C0b3rYN#PUsy33YgKLS5K^8B{MhH=`Wb%j> z7Gf|--&xy(c;HwXfr)Y*l00V|0KTIcl9chy_il%DC0WlCzm@n9 zcWe)LLL!maQh};T2yI3B@`dG&c&yxQ@vS)l?o5i}2ZF_lLpR1bFVTWou5F(4Z!AW= z?2>bnsezZ4QD~%dW%9E0E-T9CaW=Wkn7b^i-m%Kfx5(*3pV-DtBSS7X%wX)-0X!LF zw9O}}cZ$ASB&ZjmTIIH|&{h|oQs>9D^FE6k*loa-@^tWo3F5ewm&uGbg3nK%GaKn0 zbZ`bd-}1{t;fm8#QUPZRhIZQ@OaD82^48c*!Qi(G@x!&GkiMG?E~rHx7LXbRC(8K1 z;GS^%5w>%3AgucVn9PN)`Tu$>_f9Y5PYBcAPmbSswj@6yO7A2%KtcxS@PB&F0Lmb{ zw|Bg^Z*d5vueWy>_AllEMl=QoW_+(8Sji7uw4C3-tAW5YFAO*aiZ2tx%xg`5e7|=< zf=obw0jGGZMEDs-yrRB7AVA3){4dh5JD~9la4kLq0@&@;QH9Np_5F3+`v3KYHq5qYD-Y#wFh@AZ(B%ghdn7P!NxVO&ElwQJDr& z@A@T;j+)N3KB|P4IWA&@qbUx?2j{827+bW-S0;k)G4=^rfZ|a(60qMC07&LgXyy>R z7?7Rn5UA>qy&Mom>`~cnA?R*teHFCU3a?0>4L*{-f|499n>8BJeiK-})+cRM*Fe!o-Dq1WG4@-tk0yb(LOUO^sTAb~&`N$WG>&uuf99z;YaIO1;F6$h0 zxGN0{4J%HoPMc0+PD@(7Y{XfUspMLb))p(W@7Le;+G*kG^$LKRqFTa^2_lE+Ln5FG zH1d8L+|7!i=QHXnBx9$HuKC;OvU1^Z%=YoHZSfn;YE<0kIoKI9_DzW63 z!1EoK;v6^Q9Pi^CDSsq~s>e%yQB2MKZ)pI+rQesDqqFffFfoyRk-OgyI=HA|oCX^0 z-7rAT5NyMCaUnWFZTgQ58VHbzK;=N;LEQxGjqFA2Wos$Yfy!LbazE|MRbofLih7k4`WE3lp!O7+LU5KeMq#~fmqCeo6J6Q*)nzcOo2v?1pc0S z<_^m4mLcyJcBdiBxqj3PpM*53-aM+MeR*_Ulk37-r!r0TLa}OY0INEpUA5($bE{;+ zxq93s*JggsQ~1QIk#;`lyaup*zJXIriCgr`x*=8pyGdC~h7^u0l-N+B2<^#2$VqcP zvhUFh0N7&O`Is?kjoLW&+87YLAqSWv99hHA#XURBJ-O5)y3{=s-6M|8Bg+j!oHRsP zw=^6|l7fkRMMqi7$;w)$D#L}P<$CY|M1flxNKP^B#G+S<`OxJ24k*SWg|t&tYrB-? zW{Dow^nqAF**n4k1;tS*d6fK>X7(6h7jq&s3}leG+9{0 zAw$TQbYXlM3Vo2_vCnB0o|rl| zTvIBJz6|@Orc-#+F1^(d!*W1UB{rE;`_r-X#RTSZm^t2GGQEY684MY)iz-&Fs=o)v z60|CzXI++58biO5u04{$j=XV% z`L28Dc9<8(TXrv+AV?yaGNzWl2~SbqbvsX0)AiD4rsw@MEc}9Tyxf2FuB~x0$A6|Ji!A(QdhsqoN$Q!l7WfjMHoz>v1~X^8`!V z+_`Kl#dJk;)7+(EDhCdp^K0=a&9+B~c~GdpY_DVFPv62V`=DT=x%l&^pMbrz{(mm# ztR5UeAlffVJU>VhBtq}7HBde%fahmUb8LG_YG}aU;Dp@x+Vr55n4F}B!ltUO;*5~C zvbv6zu(;Biw7jgSilXGsz{>3U$j0b`#B$C25A+{!Y)2^cUp+28O`?PRbgXUxwH+Rp=!&`}1O+oK2-)1yFUimoxl z)uYrVxKWyG)ROLsu%Mwath0K)DXvj4On#XXH?;J_83dE3v=HKq1XoD4=9Hb$Q;KZ1 zdd3+E(Wg`i0y9pQ$VAb(B=x2wC{ygrdMe4e`q+e1?}1c@f7p6X#CVETr`!X4CnO#? z5mx{pw5L#-p_whDsms9uAr5hiy=4^Lg{KGWab_9L?oC{5rtOpmn1g}Ft#wSt_JjK< zWE(83ApUq*_&cPsc%h0sV)&iQv|H&xfNvj&deJjt*`~N@#N4^ZJ+*7%#rCUV+`?0oFxes z#VA7IOHey}rEGLe)G29uQu_9Dq{ti3MQpM5XKgIwJ6DqWgPhAPM^M#~I&xNFMufp? z6<5fE{{-*~w2^7v+~*f&WDg1^+1Q=SGourJOtFSw&g#q;kPED@!yV8%m_?BIx3xf` z&L*0h*_KXs5FfZ_uKyR1TkH4cg;Qg91~G{H+5no!cZ2>ZM=%GYempSRTHTmw>Z(Z) zgu?e-Z#_*jQp1!hFS6MX92`e;5^~37^9TZD;%DOu?+32^>>ouqF2QvLS&oD39c}jG zR%GLB=g7*1>3FAQjuQ`|+(78im|DwZ!Zhu=;TVPk>-rI1l5V9E!~PcZo4YZHuXJmXS&w)mN?gKZXn$81IO$5?I zL0YHu3f15lgTDAqh3)|+QEt*MwuGYYODLO!S5(XAbF-T|$$`#|#}2qL=0`jQ6X_3R zAowK&5IKN8Ukh~{tJ43(AXSHykRy~sBvlk}NXnP~sh}4tpw*lksRs>{ub{wZHkmJ# z=!D7Yv_G9LmG1Zp2!+OAu$XQJODL60rL&lA2Z~6gR;f3cZiUKdHD9eZne7A!iN)p& z8cTD;5G$HZ>$Ex_t;cA&UGum<9bu{@j~C5UplVwGqW=MxsQ<$R?`1?v^3^Z9(0SPkzN7z`Gp_255- z15)WsMw{VEjt4Yq&3fyha+Zt#zNO7bHO~he4yWVgU>Va1t#-TP)o>Np3m&)U{pC;v z+YPVx`~B5OP58g`*5IP##^}myzrfu;I==_?{L?Sn<||FHO|fPhzK!Oo9e2@ZN~|L+ zw`mDEg$s-2+EkZHGhpnsLDS~iC8pe`?31ot5ju}GD&42dm99M*JC6;n?Wf!qpIssR zw^cIUr;HgHh9%|&%)K~F)B7|((+r!~w&M)DfDkkd>xkl14cm|uRSlb%rezJgpcvLQ z>!_;cx=2)OBd)H=;*_mMdKuCQYct+o-4K@Jx@HsC^}KciKn00#7#~D!Kq1CH%nQeU zSPK{w3WLpHIoS%C6w5vi(+~`S{6~_FCz@fJ8*O1P{XmxeEO}v?eF6_HK?JPr@HLQI z(dUdR_C5ur#QO?+=RKBLRAbkR?{!Yjmox_|^&tm;a8=?@$EpB_N%H)d!#cY-q>Jz0 zP|NkQcR2)Y1Yr~aeiZHP{p;B<@7XXQ^xemf?2f%@7?!JY!5lCdO^{&WLE<9gLzLvk zv)N*?JU}7Q=nQ(3;cQST)k=^340N9RaqJuK+cET=&)bQ-BUmG^1+DGpShubdANl7;aGW9Y+k#XhM{sM}`67t6(K$ARdRLi;RJ zl{V~Rips5R)N==_zUo2WyL;BE61q4i-#Txz#z9FbT?y)}PW3ViwxL>~ z0mjKQuF?u(-UY`YFNuwkz8l)vIRl4b#UzbhNyC zuX12_u~fVy7mo``N5y9k(}9OWW*@i_Ghhqa5$W>YvVIv4Gfk*`Bd&ZWSKsFklsi>J zCyf?&By_Jw4t;lN71}E0(^hv!?UFZ3j~9hX-ZG@Lrh8F#=I@8tSMUg)zRnR&ZM5T+ z?tI>3>#m+OylvH11G)DM`qEhicQD|Bg4A5>3rByJ+cfd42nUAhYcday?&T4W6}Omk z_io_(N(0F`QLv)2;I1D-W0Qx~*xn1SVbJ3TkM7X=$J7!AMcAoldZL@ue+cKcBCbWx zjb0Vu^>SPJ7B|uJF7Bmte5+30MQ5J0zO=`lxqNsqG~lDGdqUgtEvrTmP>U829?}&t=p^X zFgqi%udmGVI=RN{^ka_`7E<0sz9Z8bxvz<6UlP>po)Y{mJPLN<tNU_Zh? zq?&Gsil57+9up#eYjyDNgr{cOeJkQX=rXJQmQ83Xgtm z7Bmmc^!eT_A6}~;H|+b!LaiUje#XbhgT+ty9N&J@_ujK+(H1CEDFsRI>#gz><~4dm zg|c7EvB-K_c!Z8ZdN?#>pB5>DM2C-2|6jRu?Qk3vLhz7LgFp9;2xaL1OFF8DbEEx| z;tI~SCEiu^yw1v2p}--9wDX=qMqOY(j9eC^l5Q1A%ZesX{xFQ| zA%Y$hESfd9d(R#v>25wqJk0-0{|u0}$!vYOyXhQWJXXHd{RQlT*kI;IPR<`Vf49XX@pRgZ9ja2h$IK#oz?;;sHmt?@I~6p^`Yov zcwPtma5^yBKVf#i<57d^}DW{}Sy?13A znS6<4f|>W@1v$}!5Dl*71A76{>bnW}rbINgQYz~l?4H_xv(v*|{mfpKUh~0j zm4?yiP+_cWbjrI~lyFY;k07(k$XP$=ymaYQSo^8h?i*k-%ta!fo{G$?l0XvG_i&%W?PSYWux(ykS_}%|KMp@W z<)&~0#-;knw0<3r3(?4 z*Yk~A<-_*ij5(y=8~wFrlVDn7#5uEM7rMVtLaA5r15}AHk^OrfBAKiM6fgh)-lOCD z&H7^W@_XikL;v2u=;OD87$vSjj6^0~oNGP?#zHsCwg`}XbtGWr6y<`bC6wNJSQZHB z=4Hd`3AY}};pb=k*8^dg-aDA80aWB68r=a=f`9=k_yPFoE)Z%ot#3cMHK z)(#DTfk>>EZ?JNg4@n$~F(@#f`yaGsP_90EIuu$^%q~e%(%D3`sVU<`M%ARjG3-N> z$|{aEN%NnLfUB8Uqmz28)vZg3XRx$Hs)4D4W&4g+a^CV(@-rTY5i^t2oI4>gJ_0q4&m$)+_V~s+!Qg% zQj~vGk}}1yi+vn{+S<7_eanl~?kS5?GRF;$0v+W%3O^NDnqt=#u4-ac%qpmsw9cWQ zvPdmrQ~9MzkLHdoE1GiFJ+7Eg@?nvCA8Vnk!9RKx?7_6bT6!ODX}w|n2*FAC&*ZHZ zkzvJ@<~$qGb41zZoE}l5R)_B#yf)F}hMDdhJ5lk6(eHpi@qYeGyYBvp6q^qL9MHL{CrS=~6qy`BE()|<22ZF%{4Gy3BA zw)~0t;Q}IRBBCPf2_zOc&X?u_L`?9Xeh`D$TESJKY=mkE z_`yj+1g%J&A(ef|yM$y_q@vJyn6u1BVbw!^JZinfn=!lJ+;V=js_ehDCChWin1ykx zuEw@?imS|LA@rwXPp+;sUg^97zBxW@iD=hh*@J?+-d6)tHmgjTDY#>Pr>vAM$0|Zq zl8UOO5lzdS#$2tuD;QV2td;{;ijL5(SzRkWheWRWh2FDEYA3w5-leT(Te+9~wCRbX zyWA@VyVjPKnZ2}oGte_&I&=I|1U2$p1pPi6yp&OK}iH$00JPf z0%G+6FyM~^n)Kn>VXK2ic2Qp;z8T9hq@`s`0F<&VMxu>n>qRs&a7TDg5}j;XgEk?r zA@jm#M$!&Y@gAn$Y(E9RE91q;DU{J`=>^k?ve9gzYla#PdF!%A!@Guf6m`oQm6f0* zg)K>*QeCCci_z-|X5v@I!H*{HmEN$WAs>1b^ZoB@cZ4!0mq}E3MIpZ z6c!<4grR2zoR!8(8Wlq+p_6&W7yR+r(b>^2@jfxfu{6=AQLk~kvA(g(@DPbKiv)_K zjD?LAm?ato8+{w~9)&BFtu-%GBA3q27u>(ydtS$1zh6UMeP~)#6_^^I*D-9mTs6E3 zTNYPNKOU_@t({p)FtB5&hSijqz_lnUk(ZS&qH-3e4b|#dI=XoJc=hw#?m4m-dNYo+ z9eDR9TLDaK{5S_O4#G-;X{yyU$wQ{L1_${LX&zIm{6?1D5|nv6%C$XS$XKow;*n z(UxYN`Fdu4A8hjMW{$3h-dJfep2Y;uf&{9YQ&LusL$z1aHV?J8+dAdZ$lY`?M!2W7 zyu5dHz1-M%tz1nU6ci8wK`A0BN)SNC>uy`Ii*Fhq(iQ^0-Q_J*J54W58$VagZftIZ zw#c~+l+KC)!s7ru_7&}(77DUu$asfDA{CU^=`OHiD*b_>=9SCdK z3Hl*~xQ~U4E3J35m(RDf1R3t|YFYWa1kmNFfD*z6TVHs~w#S#Cwe4}tW}L(0_ipA> zABRQexw{|-`rF|QA3FZo)4v~EpXtJl*W=#U`>=16{rmY{W7wLt^ixRa8^?Dv3SVEj zmdZ()7ju9rMREf+D2d8hLt|}sS2?)i?DRA})6v>hlkH}wr>EoOuq^4-t6}-9+v}w| z?EI=2?N&&BXQLvF#!%!py=HAnA$4>WN;Gw3O@P4eIGFep=lyv%f)*9@Sc6P{3go|T z4+WkU31XHjohehcJK0s!^ZmZQ{D)${JDYjx4~+hivK%w=~%&b8TAF;M2z=)q(3=yLeG2(*J0eI_(4NfT{dzIl1YLgNjOL3s2|i+==U-#6lmGNjjorL zk%2|V#fl6Rdu8Qghd0fR?h^u2%rgZ7 zj5=DoP8Oq}1`RdqnH#5VzFm~rnAiqk3BkvTTEgXGMeG9wAzqmBw zJgy81tn5Pn;jsF^a4>-`igxs&hWZ76i5Ckw2-f`D6TV!zkPlL|T6=ly!bu>&a^Wl) zXt`n`8ECp}0cLTxULhRmS17E^t!dk3?Avt+Swxm#D@$GMZ@IagKST3*q{b}C)KX8+ z$A>R_xCmRN1;*QfJuV^s0JmaAvFLMXJa9$RAc0;k|K~vT7(1dw9(oA!4}Rl{F7I z6YVv3c{PWtPBnXf2~V{~1BvG1B?{X8i41yLMZ_#n{$KZZ=-t8jF6i{hNAbkurZ_coZ z3ELc%166D@o*>ab8c`!uRNA!OOOE=9#U2uTv8IINGi)wSyR9fJ_`l2S9RrEDU-u=l zD{E!RXELNL&^ChjDN~PGjJhvAI91rv9STm&BxYu?U;&WBNEzQqReUtl@bEUp9b1y> zl94HhXsL#h{mP2bWYpwC`@s~@m)!Laqs>G2B4#N!|1yDE}j~>b77}PNzdYxbT zL$j``C>9lenC{YmIdL_kG;>5+yjtLz^;6bxb7J2ZPCYF>_Swnm{W@h zffoE%GIRfdL)ifUb1|dbSuqiK(a&lnmBn1GHcRGj{=$M#yzH0ha`PBuQcz|D2JE{Tx99@?!K>3C( z?COjCP(C3hzhfd77@G-vDAz+7LmA^xJzJ~4qMe|4&C+^Tv|iGC6Q|mQy%c$e8YIvN zcu_1^_f`hSNH9d!icp9mmn0e*^fN0`%c)nPNFkNb)zXYM|6v+Z9b!T+o|u?0Gc!98 zRIrEk@g@~I;%+TE#!=?nuq*haJ;`9|sOUWt#(c)xRt-^kqDWp26?I6lR)ucV>`QH| z0B%{eRW6rnBB_MZKxKq={pa90*hUib5Gn_Gy8|)`t*lg{7gPma{k=yb*TJ5YhS){O zubtoR)>HJ2rN|c}mqL$ez+G=w&A+>*QrudOcs9GM&lg8iZp}(|dJC^C7dQBBpU9F= zWn&gvYm`r8;@OWB;+Qf@nNYU&^A;yWmFKr%1)^u*60yke3C`xdruu=S0Dn zHEWizn&MMs0c;=xKDU6<%uH?D_=wSmDOQa06=>#dHK zruB3@d<+Z>Iqa4^?}sTiIa{{hLgaTjG6CDF71wz)nZGk?3ECp_iTSsI#_6`np zeSFbI79N&)XY%x`TRu;eZ9#nq<8DwD-ax6TOs(Y8%v$+2TcS!T9U^hkk0YL*AkJuG zr$7~j(A-?@IsAJx*DH3NG!8 z(4AC&8}}|-wPQU`nwQbxa5@Gyl-T;Z zdfEPoLM&GiX{bEiGG#nV@o%WF)=c$-^G&B8(xKjl6=cX4UwX?X{ z9onZt#eH+P-izWybK*&Yp>YVSM8l(C8`@f%QO)>_vS)U z>NaUdNR}?W;t`Z&)m&W&&n`T>^*KV4C7KSm8{3__!m6sK?*4y@Wyz8>SS2>|{b)H`!gYk1?#iFvvqUh;x8F-j8o6*bcc4`PaZ(5y~Y+R^4 z4;wh238#OaeJ(6I1v_m_2?{)0KsdFl2-!u$H9H#1NJwTrxq@_k8{5dvA?;it0ys1K|vv>J($ zgxstXc?4laMUTr^nEnEytd24@ntmm{JHa20d+HAy1SIsM?)w+}8_ea1a^nrrdyOdh z@-bfhK(&?9fbTy)AJsrR08>JaUsmDeCN9c>YZOG&l#%0bj@;A2Fdb3~s4G}tOfHt3 zEwYR=-i4sTxDe18Rty{;>#Xw>Z+wm?xu!i#==6YIGDMP&K4lO*;vp*>Uh$0CMg;tB zFvSR-k%Rw(K5W>;c1dD0rZ_PwqBy=cdOyS#92bMsR;(-(2g!?t&g6>{QY*pGvfsU* zm}y1!yyh#dNA%0Z6=4d_w3=rwH;QL2$QnK~Hy3Gx3D7S`{6ybE>jAqK!vI;)Ir4M0Chl$znD&n4H0ILVjmM`m11Lrm5HqAtm$cHac=sF#grkL#qq#5GK(--$SUSm z;ufi_V*lo6^NGWSd}8e0XY2VyXfEUu<6?@okV|aIx?HQdM2Q^Aw z8NwLCBx83sG(Xo*cnsF(+6iO9PDp4~8PS}QIhR!XA7nUsT?d=szp0Vp>kaS{H1r%PO)+z+m z$YdZ|Yb|3Fo{}x;!nht;+5IozH{eJ$fZ&#&_YU3?W|!_p70WAYj*A|#BoX@ zucy%j)&)wSfj;$E1|VWpNYnlg=nloy4F0Q zWzW*TgY+LD?TV&x0kBl0%q)vMxpkX?Xk=k>GLcP1BUufeuSY`uQJi>JM5)I`pi?L` zd_JF_nusZ?+V^I%GKJ#BM#a*jsRKX@f+ihX2rdSrMqC-yOy0pV(1H1I)0ig-brn`K zpN_dk$3P~BRLZVSqN1f|p2cuvG0B-4>Vf7s8IP1s#zG+@COqm4T3V1TqTOCl zsn+cEVW8j`0N9@33k4i^_wKz(pGS-WTpk~VegVvT#*vJBLokOifUUzp-E=u1e_b== z2Q!YaUJ1*SLqiVRg)3LC__z|Kjn$qGW{#dOU=5L$<{ zq+aue^(qKWK1*L-o3lQaM)}Y}rKZAco}R`qOb!Vp{!+vjr%+T=i{hM-B&nU6zUiP2 z)CroQ$z|Z{R%I0s=PeY8;9u<89iBN+fA1G9O`+eXk)J`Xa8FLU;V1TeR#1p1ov?BL zxA?DK_5b8Cyd-ETDiVR8W*p~$g4Y3{nawQ3%w_UeaM3$6V~*#s$N6|w;1c@O`G(DDMO_<2mKjKVn^Ef_Z&wWk!TfY#I+_D@Tf$kTQMT)5!c1W zTC1*Xb^BO0?>%|p!i9I=?%u3hUc7i=f8CO9bLZ7}7vPwf)7x0Z5I?D~gT!Wm#y@AV zw74vw=!uH;C*;q0!u%8Ks9S$x_Bl@|)}Kf|=LzNd6XxeUkywAC{2NdF20rnd0MPLh zW?)NeYwNCd>jE!F>m%3e^g50V>CKCe!^^3 z@;onN3>QxJo;!E0_jJ!IM^7Bv+p@tNR~jzf~L);W8$JD78omzy2uvf zh;LsF-I5lFP^~mI6Us_cp3sJ3%9H&fQoD4?1Sz@cS^7&ze_5pME*Jcav)~h~t4jZ8 znu*;f&!0c}GtS0ApaA=#Tlg*jIsRo4NCE+mKiTMR8`YcBZ?fl?@0 z$0MX}Qoe|4H>4GWK9Qo*Ju6U#P=hp$5Ndjs@<>%81zJFSqmNl>B>Z|&=@cn#DXv?w zN=M-TBBc&NH~gPsd6L{7c~iPjwg#z9q{=X@$5c2TuDTWke2^O+9v=6l1S*xgA!9e$ zY;|>YN8oRW|JYwY%3>XguCA^_T}PD4BlS0mT2hmi+SghtqSd9e@ZJv2>(=S70xbb? zeuIJlcLc}^)MjJ91{e482OnNbZWh<{+k(LSfl_G@D5pgt;~OMdjkhIosf1Yxd-i=s zO`PMzgNjG)v9U!M!zdyi6j=8JN}^xG`g~sWp5FZ6;>89yfvon3z@B{>Wgw9o9wRI3 zL}}|T!uCmJI9S5Wg>svbZANC`R$NieWHREW_Aa^IS#Sxm=)9>43OzLVdXBo5#>PgE z9zA;M;?bi<*e}R*s$>p|dwLdYy#xSF+{nnp$e1fIGch_b<`20h@iH2XOm=1V0p{No zigYr(8n3}DO4}2OB<+lEVk%&#(|B4Uk1J6TR6^X&8Sz6kf1}CQa|)F~&#}XuFYfPr zv15;T!Ym#r)5bRZgbI_Y*nVtPC2bLmN~O_KrbG20$A5UKP)*3E@1vUd`mtM(yT`;& z6Yl=?cg@;Xb>YZ^@%v9a?loN)E$G6P;L^8PJ@!O*!{X~X(|z#3(IZ3;CUs3~dJtW5 z_f#4i)1gY5xQ8v=ohaESa;%QLRVKB1s|d{$Q!(^5yli*=yW zQVhj1_=8^k$7pj*4r61CM5tLbpRRs>C}6>0V}1xsMoN5!JV-uKj4_W+VgrUAuQbRp z)WC?i>$njeKwb>TX*gJou{egnP#XKXNQ`=1(zn=<))6`@O_hY2rD-{#ercK@w7fux z-8>@Fx_kFvC5t8~yAlr0O;1nH1;c>noDiPD(~Oxg+!OweYA67f_28_Y*>uSEG-=TO z%0-k?JBkVAw3a$R@AbNx=1^Sg`3u!r{$e$8P~1O?^sjQQekJ z$lbq>3o7KA!aU6M+@kN%@CeR}9Mdt}N@xO`n+(Tc4!719pHJCYIS&a`0Os9?4q|jX zzZ!0C;vntBF8<#TYbE^v3b?I7vnv8VYWv^xvZUvI0enAdd~a9AO3K7i8FVcI^`&mp4qH7sxm9Up{FUM z;*1{c=k)Y4Pm&AM=x07zO=d9%5A8PNaaIC&xt*T+{0qBg$e9Li)B1`a(qo7K$t{Ww z7gf0*&()S!qS5805FUH`UMuq_%C248(p8@0Sqd^awH9*>C`mYInY zx%X(=J32ZwGq$Qk9^q`xxR>l4CWJRBd9)g@zj5j6)weERzIy56s;W34Xp~BiJAOKE)|Wwd9|xS83+U-w1rFH*3-1V`r$96sp?%Pam&4SwEe(oOe?-@gOftvR&nK) zi55*kC8G=Bg=mUHVKC9?JSIgJGxD;U`i9yvE!SUivJoJ;xswuJ2Vn*&W*}^v6f57L z&N9Mm1@;cI_mJ)4^07$Bi&@@>ckhl)qaE?i2k}a3(Vpni;>Va$G%XSTqx<*oa~!w@ zDwDCR^EpVz@mh(e8P0A&=}s;zC&hdj?mu4)thj9I6yMtAi`N{!@SA_}7k}|9mo9zq zhxq%KUps?WcLTohy7l)ZoV*hmZG)i^>PTB~YVLyE+{W_@j%9k>zB1amikO z>eQ*O27P84`%qqPm4~M8{_p?&zyHq=zu8ID3C6&Sx{?lDRe!)>vTM);%J;aBq9!JnBWCZ&Q`2%D_QLxGszN(P0SX9kkZ0 z?zec+|H8>QSjS>OeCABpA5Eo#&>sHT2|xh` z*W}i)_6-taWO6=?5wU9#c~}Nah38$$;uojZ^xXMv{f5Y8=-z_swT8Xnlgmi3RL0^A-b84 z+>9)-gKf|;EHL>WGrisLUFy}->lE}76os1g|dZn!BMBH6^A`UV;Q(0+{6&-|c&q^JHLn5D% zsijy#?Zyc$ zU!%pI1)+^dOLQDXSnV?<3+Lj5RX)p(BRhetK_(X+UKypfh$m_WQ&|}W3$(>tMlCLi z+0{969GFUiTyCdk1|4+A!3K;N9t6-liU-^vMhp$%C7jdcXebz1Jxg=rOP%xTB|J=9 zQr905Cv){cP?gPbD(z|xQ8Z0VHj8IzTQpqOg(fe|RhC9W9L$mUyh}=6IYP^%X$7G& zX=>iE<~l-Wq^WYlb`ykJ)@ZR`KDpojvPlvXH{K9|Une5_)_Oz;BIjmt`8g0pLxU`0tLSg|$(UtwwL zCFq79NO&+L$9e?*V1sN(6pnA;bD?jzfj8iX-5XfN)bniS5|QQU4K!U84sEc5BG4t3 z`JNPoK;GoKRr*HS6#P$-UO@V{OQ{b&5$RQ=|F)FghJPv2-$gq3l)i=ZZKQ3S0x#NZ zmMskrDfrBi=Mi2{FjL`+rv6`N{{h%mk?oJ;bGy1^NtR_x?k#TV)r61)0tqY-Ah48O z>Qc7w-tu~XzETXk|JQqO-}cHbKiI+smR^>GkhsN8;@)l9mMrVaRxkh0NOCuMW$Y_m z&D^PX%9(RM=Zsn{aY;fgad?LTfdtZEMwYdyNN6!^uC1+=1lDC>nYl5r>8Q#wVI@)4 z3o`tltEv+vovpkUZd+YVO{KliXfzp&S|g_7(rwtQRyfFB zSynMD$5Ux=NH$A|ETk=Ya3qyV5rL#+O`e#JB$A8>&BSaA?xXzwGC~UDs0b8TP<&5- z>hS_`fI^Q3=qk;o(u|8`(f|YW_|j%bu`FqCPmf!prsxVmU{HLuMN`xuR_)wbw7*5g zimXOSsI42VQG5zY13mKWM)WX%!W2L3@hPi{WtvckDtO8wcAj&gc-p19I35zfo1&_4 z`}ezxFl|{XvI=HnQ$V9mQRJ|6=#WIJ5DNmV{5-wjg7Jbp1=}F1<#z6zdt-^N(h}96 zL~G|po})G5!fkx41%rTVK0S7G3)D?Et*)`G#?#Hq{lY*PTtq~RP$vww@q?BTng-KM zgcnbby_o(s5<*F`&+7?;YxVglK5!wm$W1yBLns-e`Eu0*%QyZ}9v@cMIcJTzOxH^LT##=ZVMj>`O0w`z7*a znFpNqUbG4{f5lTU;BoTgsg0E37;T+Ww9bFc9>xtUZImLk7NM$Jf^Tubci#=Z3v4C# zS~&a~zQuRBw}Q7|jQ$nhcJjB_%46hD$)7TnFCHV)KusEy9|Up3@u)6uXWgvIsi*Lp|sJrCZJ zBDa)))3G>)PJZ2=Wb#VO%4TQh!VJj=Y`IjY)(EXCE|TO#E=|%e?=dma==0AVDUqfi z8SzNA!a|#B7Dj%e1v~D2U}knv>ufj-!OQUzx1G2R?r?*X97Yx@M}0jtN^_*%sab^a z4uioUE(~6xs(rl!Gf|fg<6cmyBhdu4Wz$O5>rEFFys1`Sxzac~N=G5N%}p-6to`uA zrfEo`#&_%h&E5i?X*YDIUnVPD>3xV%>9Gh zhFSBE2(~l-pY+fYB{0Gd;hsHB9)b6UaTLI_bj_fe^c!tMOa~c`9~`t;Ixl_R(a)37 zOdlVLxVioNN#fOn^&Yf#0e0k$|pQJtdhVmBgV^jWbyd%<413SdM^2SnQ`b}-mt>4NGyk<`|k1^I98U${pVW=!>}v=EX&h> z&N?4qn8>^j<^{%mQL`C}n5ypn7A~3KIa$N;i6pt`&)c8pcU7w*8C}?d>V1Gb?yD{! zLv%5O%4|kceS5*w$&*uPi55PUBpmBP;v|`ZHu6DeBVWKkxd7S8!BeMRS#2pX(^5-l zsiWkt<+Ceu;|}=SV++0+&n$(jV$vU(oeu%@{K+RVazSRD>9m`HN{Qs_$2R4vFZPPP z6Ply5b4yVS?&qIB*<_ssC-RnCI!U?AX&px1#f0W$Y1?j$=tGUQudJnI)mUqDPSsX0 z%D=a`Kt3WDUF=1W398fQ_m4fLP<7o?F7^~TC9hi_sEv{=Zh?cXh(TW0V;LNkNybpb zFN_7B;(r0Cqh)&x1&C9K!KK3sSdPWAy7xlMG2hGNOD>*8#?T4VHY_L7)bLx#o}4;M z^CvVd8{TSu*%}R(YkFGtN!Cv;x+Rg8iu!gRr{za~-lPNG*0!Pq&hz+@U9GW-wn$iw zru?B;+O5J0on5Nk1z4h&mB6X49-mbMCslYJntF{D&U}?yHH!he*U7GEBke_Q)XJ%2 z{CnRU|AHJ}lh1CMBdI$EJ+r^G*L^|GzlL~Uobv&~;6l#)M<0Rx6jFScvwccPrNR$2 zRL<2QDi70O?%67H$5=EvcE=qWYc+(e)mBY!?;Ur<`yfT>ixUT;ojXUi&U>T96MvS% z)-R97n+b!9kWxCkwoOg7jgAUT0zEsyK&KKv?ATY^1yI*+9VH63EL|y`hKpW(wP^qT zC}#zIWaXk%Z*umt*Is)Kn&uir-n(~p_6B9#Fn{e?o~KR{1{WcfIja`_si9$eLE1l& zF=jF0PuuK6gOmP`J{lS#BanzuvkGoA01YM7Dnrif+sNEpROTF$lMZ*KHXaNHY;8uR&~%jcU9*5vcl5>(?#Isg}=`TJ4e8jVJjxk;yU(!HT{agM!k zaWs(7gTB=#0;8W@VAxn-7UcTyI3z%;B zE-KGHvA=-H0En4_{ZBlr1jT~#j46)tf?eCT?II0G2ONtUlxKf_)@a1_rKQ+%Iw%}U zw-q05_hvqvF1w$8m+q&xT(?%@?8{NqPOiV7d-wdsw)V^Kz542_=ndB{fA-0=6lBF815^G@t2V9{?dl6O-E*mZ_f%d&9p z+|pzq;bJuTvUI)eop;_j-`)EP$>@}0UU{&L6xuWMT1Ilo<=_DH13q@X?O)qI`Mmv; zbKigc+-H5TUGUzI{^hU!>R*2Js!YjU#%*8->~zouuc1adNKqluT80(iq7L_P9GgFO z8meVAHQVnz^X!W+K6~cQJ*HG@&r`?9Uy#3G?tDTPs{0uxod!oWjmB1=IzZ;motv|r zA{+J{3^Uk%`Q4Zh1p{$%@bk~{`@-w5zkXqmw4-xjt5GELCaqe-xmDv(Su9b7sn+87 z_?~?Sp7iz2BoYZ-8CVzNJMR7Z*S~)64!R@Gsw?uoV8kDFtBUd3yJp!Ht;ORx+;m0o zUA&#k7eD^sCm4Hg{_OJQUQBUUKK}Rv`i|(!!vrU@ct>ZsR5Xr_8wPQdQl@nl(M@+h z6;o&Mst)hpw{I8TRb5qC+0sWJeKZgkW#9cfui99RA3PuGP#%ufJ za=UwVFLZEa&ZBe7*0b%1tQ#7#TEAe@GZ@Bp>`)SVuy*wc<--qm>=^&(-~R32J{l*S z%&66_EhpSe-uL9Ja8&Em`YTtjbPW_5q{XS|TyNK>oI%^&t>r%akSiG&DB%VMsD7Im z^1+4DvLxkK!sSacn;svhMpBxZ=#|+Sa@UsZPaP+2@-O6nmHbM~HR`i%qgk4{xf#S78yOz*gz7E% zwnB%qw5+1C%Ij|a&#e7ycNRG+7)Hy6d{gt$g5p@Ay?W=N=9~9#HUqS6qY)du-Qg_S z)`S&n_pVvb-1OA7tDv0P+8w$6QI^wCH$j_yN1dJv27Qa6G_=}7=%F9&FL&`68pj`P zHHkleI3+Ya@Wd0(eC5kuLEAoy@Zah4yLjaF&iOSGpWR4J*Y?+c-FAb$;NQuAN4|E9 zbdfIMYyX8kA@I7}w*5_R_msmvT=>&Jy|8Xa@)z=-k!>0BfZ4WjXTqE&l$b;+f3kua zr;@3BTE0yd>OPcP*IKB{4?OWiV3U=)V>C7QT0?ak=I(wvcYkYn?kcJcAXU^DHb>Uw`^S=4!vO4_gzNwMcU5%*gH1e;??zJlU zKcHnlyGA>IPi~fQcKq$%c6hGog2RE;$nk=7DPx7#yl8kJlEQ9GOurXV&UN*lUV?H#4!A{4z4kMio z^x>_SF2H%dVBso&d0q@;jN_GIoNjvRDO-b3HE^R9Yjv*{%kI^h>Anu7--=&za=FIO zS;Kg}HhE5-+Qb_WXkB&#(0iDXnNB+1S>P*{d34XEkQ8eh75-XndY|OjAosiqGR| zYN{z~s6TYLx}>nEr12I^`^R>a>3zs;PF+N|eovp?T}o~Oi$quGFp2`u`PMvxA*J{i zXO~1tQmNroJj=+&n;I>AXaMCJ4D*&o2z;`&yCt_nwORVhg;&~@aY%MFX_rn5rkO9HDQs-?`ADV5wD-h`6AwTA^rQINljl(eFjSdG9$~_` z32PsDM2p=i)g&}YT7!yBFkHfwcd({V1Ct>K51P{pV~|su&1-le<}yN50&>qGXW7Qa zl2(Dw^a8%Z@{q?0e28kJbXO#!S^1H5mA}1_pXg~9JY};jSlXGLL^uM}d*@*RSQFjA z78VR}i2-3e)UBD~7t2Uvi7amSlo;=yF!ADfT7YbvLx^)YYr$YDC98USjmD18FMZxm zxrnj~EoAEJHIhD=!&q0&su~+f5#!QnIYf963U-jWeR3_TM`;a9i+0yCS8rWkeRtCOM9E<%#p_ zo+!=joK$tAKV`?h|NXI7kEWmJ{;<3I5AiL&%Kmh;j{GtBj-z+|YWlzl@_+Gn02uce z8DyS$<~SL|-5>GkU%hJ-0}fRd1d7DSd;_yA2=sEVS`>Sjzy;)O7cTY;dBJp_>xG-c zjc>H){Lct8KY9g5<}Q5t>1X)r8UjDOrI2Td2RN(ggub+-*yo)KaRnGv1tf)eluKhe z=3Z%lCGVS>?Ws}F*qHtxHb0p8VYJnJvQ4Dt@ zg>0khSR`o!98G__b%R~2@vQv2W(!*Z*)VZ6EHAf4>pTD8Q@wEcvY3^Z~6UKuJjCg z1@c~&e>m;t8XM#M%XuDj_0P{&RQ%{i^}BY}R(Oa;7NMJV;2_QJ^Upc{WwPE*kMNT~ zBWZ|wL)P|j8FR$4 z>8vx84|xu=8VJTVrZYj)xn=XpIY<5PhyRwAxCXkl!)zlm;FX*18EIla*KAJtI!)os z=Czm2$_Gmkw#;eF*&{1g5>%5>S;*)ijQbW?I#nzTQk!`Tnw}m_#sqXSNzLW)97liz z&|aJ-g`hqQ$@ImGuc#^+EI&-;@uzMhXUU&s{?3}8I(`$z$4$513FWLiZ?%8(n|6%k zR@o7YCIx+-$z+0%C>f2#b{7f(n1Blig}ZmlOftD?civ8G^x|@jw&&4kziFbTor3#D4^Up`fy|UF*W>IC- z&^4Ov`@pchX?K%GvqpYyS;upv-A4F0Dw7MO+r@T+02UsaJmdKlNhXhr`$&i!Ngk02 z;-a@$~)u@+;T4qvU_Hd)Fq<+MAk=lHb!DNoF&_r@SH) zGm>>YN?O-(HblDJ7#Osghj}K6O6JPdn3Id;qfA3tCxj@@Xb8XQ0!(qC(L~av>X}RE zD=I1=y3EH5sMw2jX>Wzc4{Wht_s~P&bJAHIvJEYla;bLOxp{2n0Tf!{f!;)AE8}3O zY?%{e%vs=MS0Z^JfH?iqorurt#VyAV#%zW z5vX61Nn&}#9xBVOspdSwavRE&C$x7PtV2FHp}Jb|4fz&iW2j<%v5L_Y9traC4$uY8 znwlD?rsLY1Z@zhL@yL-yVwV}MR@QDa1x8^`4=9hY}4kITblS-k;^ndestc>0OS z*38Wg+w%idg(Z--+J|SogJZHu(iKxx7K$WaiV;l1<;%($2k$#GF{8_AWoTz6&YV5~ zrbA&NMT*#$6*S1=;>3zchia=;C3A}1uH?#j^GbQhN=Y*15(She!d+||4=@DD1_c;=aBPHe-rRZJ&i zyoS<(^YgMgRt8zHC#EkebCVU$)_usU7F*Wx=6w$iWx%=qO8Uqxo4V~Ok~NGHO5~{)oo8fWhJX_D-`ad>b4;;j_?b9`?Mjd zl#Ak-_4;Ic5akoZ6DNkjS^W6Qu&h3M^ytk8_s-4jwYWIFK9O)|Y2@4tL*X2fkj1vE zAzjKJY#VGBMqGS;V^7aTxv>4n5w#7Y)uwL02A z`q^lVIyj`Z5MOm{kKE_Ngh4*XLJ)q43Fr7*jd?V(`ebSXUNCfO6`p`$L@OQ@#nsLL+!9TQ**YuHac`y4>*kI`N53)dB-j;gkIt>NfVT&V7oKm5Z_Zn(?( zyIYBiEa1=eU)pZX%K`&JY|Aaz%Fcz-V0n>`K8mc{NqhoMU(qr09r7KfXycB8d4PcY zSV?6{gNpD(l3cw-GHyq8Xi2@y6z3B{r&y^^(kbgf#qaO5)SNI zpOmV!baZqzxmB)UJ#DACH{O_Ahu1$RyVnBtiS-z95trV&4!BQA6b)@HvI^f{;R!ZV zp5W;BzBl?sbnxr4dkaF?srj{E(|i#z{G`k<%oh>FTgf4J-qF) zbwq!-wT$GMn2jr0i*am&R_yv^40!0R7BOp8)fURJ)~#2qjk^CUdna1H^|of|scz$+ za`Z$u($K0BpMIL`eL*BI$ZjyzTi4q>XLi?{(Zq@1{LC;=@}K?S-~0OJ=OfgHKCI$T zbyF$E`20MBDM7k;@%?s%8b*>BhA8dtqaT_scTY!&AtSmlkmz*x<<`1@h91~Og+Qe{ zsEnef;-;Has^}mH&Vi(D=jkV&c;enY)ztwAB&1U(ns+qqEaY91P`I;cNArnOvgy>_ z%{DUiDLuz)irAX(UPeFMl(RosvXImpVXRjbTj03R{74@-iGu_E0|N_O|L0sru9AkN zD^ZBK%Y|l^`S>hWS{Hh?c28q$iV< zU*%EqH|#Hq=;&@)ljhXggyDzpK$_;#LBsIw+mC`~C+P{cb%W;EQr4_-H}u2$rOr-C z=;#p06=4;wB}tNr#tuz=-ro|pg8(YZqyzVJ#Yu}A0 zzMDC@L0^r2R;|ySd!dd}Ntnh~z7t%UUFBe*BMOy-We@^Qu&KXniL90K(~YP0T8Q^^ zbgR$3#Ikq!1S>mXa1o-zCMZSH>2yzz7MY4QH6ggzD>^ZeNJ&K)=-NW zw3Q~EW;w#C*eRei%advUKwl4DhLV5a$>$=AoTZ%Z5pO>6rLX?RZyY(2B!^^UK~t^M zVP+IcbhSYX)1^s+wa%-N(rQy_KnrFdlVcFKEJPLt4 zUZ=v)^XbYgmNEvw38tj^!7uyf)g{fa#rLKA?>_^>11ApDk>f}@ufF~!D)6S z_l8I4Nqy)0hx{&0d@&k|gp?G9MXnB3!r;oRy-ZdHqjG4#iCz(?r4=7+b*GI&*_Jh(Eaz{dFK9y z?mP44haPy~fjjqCk-LzNlwYtNwXQSJ!xDQZCuQBab7qr71xFeKpWb*Dh?d&A;KP2; zY-O1kp6%?o-s@Rf3I+m!P+G{x(SLdIz#!Fq3vwg|L_s)}NW09Opr(hO@mH_T#^4eu zhLQD`rc!2bw<_|)&;UIPM1>Kobvl~vxNTuUEW){?XU^Pm_~>mAY#iB9!QySD3hGWi z_Sj=z+F49)M$)=`v({w}j19Fx&3(>l<)9e65KhDrvi^u8HU#9-Wo&91j~sDtI9;fy z5}KmZ)6t2EA`*}}!-4(#Wp?**38xEP{z)|IaNI;CpjMfSUp{wEX5SuPo&z95$AuTR zUqmz5%gU_y;?t=lMG1Na2Pg3rN~EmlzWS6Ot>8%+aG#f&!~J}U_E;^5Zz3>~1SK!t zrRCLt$xDntK$Xh{mpm~wkiY7f2VFX?D@KzQ>(YL|`#>>|#*r)*6Iyzs*5eNIg5#ry7l?z!jg*+;&C3{#0DsO(gPAw28S zvOHm8sWitVVV=I=&I1k(ATiEy;LbY>l9L@^V{}X=3kq^A_Eo~*!nia$9HUcl(cail zS(%r$4Jf8!0l28BDa9O8BECcYZIZA zwkmsI=F<4JYwjkSlz#N#V~rN?oM$=`3rA4Xl(uje)T?(kT7r1*3&x6l)b{872WrV} zNL*c0w;#Pi+uP-VmOY<{#F2Pxd`dR%sxhP%y0Q9QnNMh|cI|Snw~9+7YD}CkXUPQE z$D4WmyAcX%BeYc*n+@}96~<@7rnd^yWy9vT3e#u9rnU;>ZjhfU8>ZYK-o$@5O(`3e zB>9`eoY}C*`Y>TNP1lV>Hp#HF>G25rqBcq2IK?k$5$#rC+=iOnD8<`y`@w2mU!U&3 zu+rlk)ba5zSnjJsjsuqe!jiA1Vsmn%Wk1WAD$DZ1HR_Cfl%b#Mx4F=)cW&;(@O$D# zLf8M8i-t4Va1MJ#i5D}}z%KzGEgm2lTELa5E1yFrkUaNUHg8q(zT#gD|La@$Yv6C% z!e0x2?H2y|@Q-fcPxBSG@YloNu!X<*3(Bd3e|YP3Xn8hr3AwVskly_YH^P*r+&QX9 zmD^+S|G@xvCBMw46gw%EU)~TJV#dh?Lh}?0DcTs?!p$?pk5Ii)A+}9%eT5yftxMUtWj@Dq)H{<*yPWA{A|AzdJsM9)V9=??<`TL@0A_?1Y$QU(?=nfBC21Kq z#<4}>Xi&z+V4XrsCa>t-j81SB3Oa+S00&kTm<-f3Detr!I72>|qIMJ@2kkwZMavq& z)%ALeHXCTSC1SA$+-vB?GD2L!QY0Mi@24#wlvhZS#J(a5Bx8U`5J?(`QLxhZz5cQ`?)CW=W5fvjqu~`vFz1vU=o3!b{Bqc4ktk8 zsr=#5ATfeW)e}J=2HfaqVcaC`Vk6<0i(y#23fK>}D70-898_;G8KyL5luOqtqzNde zq>ODvE2HM*Z4QT7%TfA9ElFw)xRch6QgF zR6r`Wh(a#_rR-8M1SBxeLG$U0D06mpab$Lc{kUIc36ez%IkiYsgR_0nKy)xYrV8g1 zeVB~s$;yr?Yt1RikddL8C<8qxF1j!>oJ@v7BiFCY!1gvs&-p+Ios}9v)C5uAC1OB- z(6~7;wdPzr!xHR5h)OPX*o|rq=vz*0$SX*Z(o%b|-EK8o(G&C3YEl52oR=gcDrXSW z)S68^E^B9J%{qxXQOF@5?$2?h89{KFRT{#QbV;Fx#C&5D6CvztU3!M-=sV#%yHmw-E9OEo4l^K)ut6lz-l5WN7!Qh|>7B_f$nbCX1t zmfS>gv4T$Jsud0S7~NKr4WG2q45KnwQRjSv3ipyBANN)R9qKA-N1voQj&-S6jt+UA zQt~#7LBxO*4H!A;h~h(2_>@RGy=vq8bOw*Xuw&CH!CdMn(g+~W5kC=kVQdRp`Z`jJ zsK+7%9crGW7SXBrQmYH|0!g_r{LgAf7YTh%lX-0hKFO6jEP8fPSxk!@<0_C0dJ`Qp zTD3q&z1B)gof$uB6*O`&9GRt9E1Hx?k}QjthLl!b+R7~20zBO+=fP42AJw*PC&&(7QkPM{3E$~@Jy@Fo1kwAn6QS9iLkiqzp`HqfQX{lS#D9VWw z`($zeUbo)LClVXbT6Avj!Z5eGxrGHfTEWj=e>MjvG2nF)>)GrB`{ni4GGi2S3h%?vuAJ zqPPl5%avC<9J1sntSGOpzV+7D4fdmZI@^&ZMSjOZ_@=40a0#{uyIgA_n*bzl=h?hl zPu`70k@T#85vkH-`TpUdX=>1NvVXXry!&phE_dYS#7Z`aeZMG*ixbz*f5tK4*@@As z*!XpHTx`2^iDhwtyg)w-vD!RaC8*;9E{(CGWC%x1w}Unj*uRqC}!dGaNBNaFiG9y=KV^tE<%EJj=D-;OO~L_d1Ph zqE5Wq&0YJO*M`X7%fF{y$TKR=BR7?Re*C@cb0s<1lEDHq6$!!OdS4)nO@00(-+LR|?h={R6_VlmhpE4)lyd}F~(dNPhH@AED$cTI6 z88jX3v@Kr|7N7eXHBs@(`f$Nw9vdTL2%npI?5pJDa(F)4x&+}^$`}qUDsbFT`(PJ0 zHE=l~>m`r~Qb7%D9o7_p*3~9VWji20*U0pg75Gb7P}k$83ENMxg=O(q76 zL=Q0nK%VOfs%5DJCGxuH0Nni?!Ejura1Z2ULk>`gxxv`c)e~CeIBs!fh@QkTgJ}HB zymu06>%NJ}$q|<-Fhya${ZoNfM>M2>s{)&R_uYNhsh9;blLgYylaPf1XTWQ&j!woz7w_V|C_R>GGWLg zw0-LNlqB#x7nr_s;d6{`uXn5)qx(Wv_m#FbqM#Vcbf(tRbd;;pF;38FoK)?MO$)rs z3M=7SV{xI?Xt9vh_GuUypPL@MdbKC+IQaOJN-(Z3*>(V<{lwk(!3^Js7NmjJQ4f!L zddRwQ-_H69D;FL@At%xdCJ$RG8VDE|ySJVLAU3qSW%Mx8yC$A$ zdDR%<#@RswVI?KX!id2aJTZhP@)VA(?*AV@(ZcM^Jki3uNmhH`;f%IIM_VW45?#Zy z+zi?~>n^o*{P<^W5PrHqgS$+|(#3&`EAF#TeXUNc9|DmyMw>%fVm0QXa-9YoxNx|_ zt|3;rXsGXc@8A&JSW#(JRaIGGStY(oOQwg0+-q^z1f-7VC!;^{U>0Chk?*J!#e4UY zcY6W%W5n2ZvSl@`oECYV>wNRgPC8>S5!G20>t~<&>Q|q^!)_)f=34*09L-uAV^we> zMldJRJ2n=%etq;h+|b0t5WeV-2zEp!mZVv=$yVf;_IQ;j)v;!GHtA$tGR`m*?y=O} z#j@^Nm3I(sdJ&R^X?o{X6*(LSZim}dQL&4DA8b)5A)ziE{%>kovHv>GZLuz zx88jFLO2{_W2`9czvajga9r1y7lK?4E*Yi=R%CvRkM>@H>$%?7cfE(+^^T6Cyjr%a zdx>QQkc{!9%<7tUy7E|#M5*mhN0H5>X48b0mu07}!Fl6xFa4eZ*_6NQDBS+KhK9QR z^ln!^mnrX&Be(3AL>8qBhcCSS=36MQ1ZibJ<#djXE}<@b80Fmx>&m~{{p#y2%yvvw zV|Rb)?t5F9*H6pqsF~#_2e|KZuQOfSflXy!Wbb88zwRPyQzQ~c5%e7NH@+(=gZF&x zoJzlg zEA~z1uW*4Dc4sr;VtI{34X<3Ij~_sE~fL@P5Ei_B_332GIk zq9SO7(AEU|vI`bxq&L=B_j_HhcL0iE>BpR{f#juqV{m3cw{`4HY}>YHV%xTDCllM|#CGz; zwr$(CZ{B*p@5lXp`*d}k({<3hx_Y1L-M!YL%(Vv@Z?Qk8e~3bOdUkV_m9;CtCPXCT zSn}A~1YGLeXo|=~JZ}|%X%jnV`P~QwZh?#JcYk|5GpoU15Uslh3!+hoLO_V!R#Ebr zINvM~CbBXTR^^;?6AN+E*3}_y%<^0Z+vw5bUF3CF*UShQbHOIb_y0V1rg z+3{+2l|FoaCxfkIS-9TRsu@Pmc|Dy!JRnR+gsND&3D*x0)+yg_V#mih-5=hh)^d!Y z?x>6+)3TMLaR~DI&VEKKQpujM&V@BKJxNKChwnnadRl)z1T=o%tJD0DGQYWKj0`zf zSVUQC4~+kg%oFb2@O{tt^n@SX84=$K-=`vX;YEpW_dFO;=^LSgz-E(BZQcb+c92fV zQRtlP@Oi&9t_)EqDi!)u|6XxC8|&K{m6VEfShqs8p!H!_do3&M7A z2yD02R=ubKha0P0gtOQvS*5W4DlF~O?}<$mm0}Gc(V;-s@cH706!Kw5O_d2Zs04S1 zn8pfV*R&GR5t7jnDauwU^T5BekyX;xSSPeAVCcwqeXrJO&%(UX-C-O$4#X!PQvdCH zbWh3+Ol?Ud<6IAhuj}Fx&VET91&+Rl%~&2`<+>UNWU!))ZQIc~tWr>w$RGr!-L)2 z%XYOgt8CXyVA)mH>Tx|~BRc{5YQht<1zBKZcE!8o{8Ct^8{5Hl=ymrmuFT7`U+M|eDUNq|JpH>sUXVb1aXciU0K+e@BrM$Cz4m#fu2G&|LH3qUkx#+U(>4@j@3rbZ!(E2ny2fDlV@{$EA<~BZ`k2&}lQQV)<>6~70 zrOn%kKdZ<%b=TfV8-|OBe92-a{bw zuu7jk5H_4Ar@j2AXAiuU!V}YOzBAEse)_tM)6|$Vp zOAwbQF!fS0Rp$$5*{k;0meX09&JsY8aq=a~4yH$GE=y}K^t^>|GYhcqcMW0&zkb!= zmMa@^o#3Sf7WNRNwebh&0ozR8LK1ko^Xpr#_#OAh^12?0>s(F(9r4~RitXU@D=_#Y z{U8YOyna|Kf%gXD&mj{mbQ^)0m7<&|`XU&9D^msIo3x>V&IzDDc#1IwRmXaKAgQx9 z{?P|wuj$P{HnFk5KORo8RPcF*!v+)c3`Hk-WP^x;d2@6iRONdXzME zBM{sI=}2LC7yyp1X2!6oCxl^iszYyF(~*kC1S=fLvBaZxbrCv7XV#2C1gc~T(n;Xz z+5ICws2KxrpPE8ayVEg*?&!+Yd>; z%7(UQE}{YHn(}9RKwj9GI2=*m3VLa|yA+&Qb3fM^Lp_>FZvr!*2(8pmpPiKLm$g|fElhq+JDd)@N3zpl0(Gnk1o zca7tey(WnlX&lY7bF#fJzDw#Vx6{{|HTy{qCX^w% z_c7csci8eV4iO)d;G0h{<#EV0#bjYfJqFzh>#uc`L)~9MF8l-pNQ2OFHM|bvl}m)g ztVhGBuCCf~V`kXw@0F$)7Jp7vv|d0-$}D;khVlt_2{D9_ae3m4nCQoyYKDkM#Ya9a z1(Qqmhd^tx3|~0c)iX!V5Zw(QAMa_=QrL7B7Rmde8vBivh5HlMjnyej>#?t0q6vQo zkgfphGS&fhTY`2E%|9oj#6IeEQb(mhXNv$JSS+8#xFO zed`W+v%+a$<>krcWhhg2*Vb0dFE=3%V8#aULpJ#Lo`%h3c^1HDw%ge`1yCN%Mng$0 zrr~5l#-&%;D2X*f^k9(**%UHu#6ttB>ZgACEIe#9vyvjQl~uW91Y%xoVR`XTXW#gc z$YRcnz^VL{Z&RrdCj{xi;%{4u#3FRV`1F=PLl`(5h%%%$jD_`d*JF(J`KOX)F8M^zt$pw5!TXe_&Dx zsL^d2-o%86aSlz@4FF}Tr{~D;Q>SuK|jx_`&FFWdue87v#7C>u~L@` zUT)e`?YiE&U|^$oB%rb@AfAsebuN}McBkDac z=*%xM5u+5SX-b<_Z>YQTn>o1`eqCF#Od90`ym#c;I6dp@hH8U8pOhD`o!^ zeWrKQ!@HO6ot#jzfv1romiiN6okbRabli~v7YEf|8J;9*l}8OOtHOPf`TQyr?_Tec zTU0neOb?zkjNe)?h5n-lG^KVxhK`QD=YiI4*SQ}PA1)#^C=<*7cJdh-ah4H_$K%>E zCCWvr3Sqi0h49yERUhpGR7Z!eU`v0)BshG(tV_=CZ9Z2wGd4UWA;K|qvgi0HpC{Gj zDJ?6K26o+YQkoK!6PD@qas3GNMm9f#DhDLF%g9to8VP1opKJ?%!Gd|R*d+YUr~b{e zO93c%_y|J<{K<_U`w14cNrUVqbc@G~i7`@g3JI9fUpT-LkeU2-j@rDGhuBZAU*eX8 zR$(H6nnyx8V5k9ey=v0loHjmtQ!K3ivUjY>Cov%>E8TN|&&rWN{DkBR(H8zm==<(t zAZ4>SaAJsQvLq+>4>6Lu`cA*RE`#n;S66P|JMx@GErtM}_%PK?hrkv2KZP>|kYN zMOfa-uH$&OsB~)89oIXEC3efNJ3qGIq9MZZ`xAlh^=04fnp!0mVcY3hmx7#&58KYS zoMV1QlJ=519MbgDAw)xyxMK_AU$knbY=7mWOk9OE3wGfWnigpblta)|HY^nh=<+`m z4;%f1Y_}xB1=zqAEFv2XGRo9}u#663X^MJF?rJKCZr~CLo<38jmcUu=KT+IGaI|X9 z`Aj^?Bx0zB#Ymx{I>=DxdA3lB#>sSS4$!;qN;J$G+Cj=U9}m{Zi9U{|*v*|fJI&6I zvfuANj$dSa9@dBj)Wiq zVa})!t^B3rsxrja7dD%DN>N>ryjv{w_RLU0K>@fwiH9;l2%JPF(P;58rjVHrn1hXZ zn2{u>HQp*rIy4BtBKgqxo(Lw<9tp-ji7sDS9}dJ-lxO#Y5%vA@PSAGcp!RR4gyG*M z#ui)L+Hcmw*@d;V3*=uRk>h=ocDgTk-hMuiQjUpXs;c;jSIi+h8k~qziBD;_I_6yY zkoQZ{N}C@eTgCKEaacIkWCf@S75U$DH7}K;tM9wM2gAlgu~nH=^ShL1=vEvxb&*vV z>hH~3Wk=I}Ftw;sMiVm(hkH|kQK4 zCX+g zHIt17W+01jqIK}_8ro@oAVIQ;)8(-s)|TJr?dAzN+EnP%5gCyaO~ClyBTnFZ+BScg zXKtmVgA`OR?6bSI_7swWtCWxs1Zd~Ro16_mPK~?`Ivtpc$Yz@#y6yS%d2>9AOFO6( z>o;e*eHsyx2DZ^_dGM?yPRr{Ib3S=zxLS&>CH9%~QtaENv5)jG{pPMN^CVK^GEe8c z2(w{xX<=9hBPML8#;sMZ1!ok)YJu)BEAyQj{8Xvxt|9yA(|Bs&IGE1*p}dnbGXm!` zd~elj?b$Y}sa5OwdtOM>Gs#aj6_QiYm{#(*n3x8f#MzTvANgbN8x0CBm$M7*_MUOq zOwRZ~n!AXs;j6lK;gUV&woLder$%pT3Y9msz8&HNd1~ZH+P9B+wRSEl7`~lTjqLyd z(z5qz**6JVv^xgKNq43h^Z*)zz`MTz-bOiCA>Goo_Ar^Ux@iu5Nf0XMoKPd)ome9! zycH?|aJWy}!)CwtsqgQhN05He(NapL4eI{G1!QadV-SK({KU)k&ZoRb`P(yRDNmdp z6P%RHsQm4Zcsm&lQo1KoLWL^3keMa#S!XDN2F7%OH%xpjRic5LFnNb91>GoMo<@1J zwXtimYRif#kA9R=!NJYUeyOL_N-XB!kO!YU-moexPp}p2(GtA6%1PV8eca*HyC_Ic zNB_2rUMC(EY9?0qG?9l(nLnltLRRilBwxit<-hM5Zd?)xifR&|!8k%w&#c|(=KG}K z?0NwMIe^F~Uaj&&sKg{KQ6?z48!ub)=j0Q&sH!E)s5IK4ZwK@h@q$I8uk4a7*wPlA zW`OqC+Sb;U*iWY?_-gMfyyXMb;% zqft0L9jNlfdUUge}RIgR4JD0wg^N@h(qC!?mxkV`nC3cQcp+i!n88O6qL zCut3MU3Wg`cqM_SLNP%cU=}aAaQk3SvDeo2B#YF<5e_cxI*GecCQ)4KG#MBQegd_P^D&tA0<6fbpSxb2z2j$?+3 zxl7`e0^lB*lQ?X)*Ufj)A=l~k&R`w6{;>;j*`EG>9^MaWyClVzX^qz511*TKIj-JR zZz9=0VR2aldy`I5b11{)!(~d5gwPJHsf%*yFc1z1kE zN^;8RdKb2fRW%$OmvK58w-fEPI_`c46C4j)-+pxv zf2k5|c{9Bjtg;@P#d}IwQ$EO8QAO>>DQ;fgeJ>Bs;mx*ZY+~0u|GDSX1y}DE-kka8?gO70L$=s<#5OR$?|z6#lQ<+pd#0O zmo(4$(V1+>O9$w(guern8|41!Ml%L&~9hV_5ChmxjIwW{W;$KG2ZRNgZxGRit-j}=O+3D zU#;gUV+8o(SnJfcX}1C+7je18RIgGW{O$u0=v9JaJR5X!8Wbjz(r~WsouP)2HkHVm zOR>3@wMR{(sVPDANkfM^Hl-;wpuhOF6w3TVS$Z&K4v6m=k`Ep-*{n3M+2}iDmPi-O z6K|9*uWU@D9Me!B#BJ9sMMoD@^dPfU<)=r4ShD;`q-Lp)Bl`u(b}X@fZ%enQtfI0O zOPLx+Au0=_{k^r2y?BN8+D5mI{{eaJ3nYtN1w=TOKY~<(qIkPFfq-ABLJk(yIsKF% zGw0FOUeI5eaYN$f0>V?29c^m1AlHDPPuzmqvYIo=@AK-Ybsammc%{N)yQrMm-LvLU z)XyCec)grdsC8ui$M};rLQr+QaM9RC*94|`SJq)kDSd9Ua5RbjzV5WMvaSOD0$~hvNY1J70Yye!*w>O!2zT}a0ysLPSnV;< z6!c<92ECUSC+7tWZFTho+M;#0YrArmbFR9U-WJjM<#5;8$FCDH_qvJJ^X2Jy-EBQ=Ja=PU8m5fYTO$&n=9ZiJdGHza$40<~8AcPls{DyZjb$T$? zz-teug&EOyM(?TV^f(M zE91n#z~Oj?1N;o2$c39O+O|u=_Dc5n+yv~PTAK7R(fT1wj^2)FquE z7?Pe&Re5PP0;IAWL`8n&xveoNhc&46-%RIe^SGyGsO zCQKu2>5sKMVCePa{iKl?0Mnbh6xNuibG3LsevY{Ap8Sp}I8h-a^rNo+vHb;49{YN9 zB<$2c>uSL|$+&i48aX&WTu0afU3t0fb&Xd-z%N7R@truK*Jj-AEP?(U6B{_+wcL4y zD~QHoZ+p5Qn>v!otS4njL#+vJvR#vC=Pfkk5%O_<@aVQ>vB~JWhziRgajY_trJ^;} z7TBucwmvjd!FrXH*_l36H4&_tGS1wSC8S`kq4~0<%gpMWvR(4=#?iG)yd8v4?zC=W zwrpvT_b^cueC`0Nh&GR* z?bWmjy)K48?diIt2p!Z*&*wNBE&Z%`Dk~VHY^{?!-#KnuAi3uRBbNhw1rjhAmo{M`tfnU_>lN$iPZ<`6PRQk^5 zxaGdsq|jv4r5>+6|K;Wv76fZC$bfhzOF%>t`! zo0sQp>px*k2o?j3#F@R2xBac7f#~2r?YhI!+XCQZh_z#BjxBt6j!#5SP{!dH`SnI8Bs$Eb(yrC~yX} z2rYSEEx8#3(U5YIt7c(y>m`(jk^;VTAuIw(TN2m?#ku5b0?dQ2{Zd&l!yx&OWm`FlCIymY-g6DM6N>3Ra;?`&w%z+>*!en-Yn~9H z^Pb}fOmnW@Jqd1iH~@)OtW^&*8{y*{0+058jAlkQ3TBK@pPbGd9$(s41%&qXjxc%e z8~aL!mmNW%hqJqJT}X@yW+$mA5NK?7bWcz1&T|#@x`yZk*j(KEmHO&Cf#$AlZHV03 zwU$Y8xvtKBuhFq6H;MWj{DWw=vB5EA4EH$SI1$%lI2NTjaW-v`Jx)O`A)s@*uvFe) z{B!b1j;wn0m_tTj1{|WIg|oAn{)mS}qP4P9E6%Ken^S >-Aun5A4Gp>4U0IQJ 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 G)cz05(PHWV literal 0 HcmV?d00001 diff --git a/frontend/app/globals.css b/frontend/app/globals.css new file mode 100644 index 0000000..00e1ea6 --- /dev/null +++ b/frontend/app/globals.css @@ -0,0 +1,63 @@ +@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: 221.2 83.2% 53.3%; + --primary-foreground: 210 40% 98%; + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + --accent: 210 40% 96.1%; + --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: 221.2 83.2% 53.3%; + --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: 217.2 91.2% 59.8%; + --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: 224.3 76.3% 48%; +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} + +@layer utilities { + .text-balance { + text-wrap: balance; + } +} diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx new file mode 100644 index 0000000..e556400 --- /dev/null +++ b/frontend/app/layout.tsx @@ -0,0 +1,36 @@ +import type { Metadata } from "next"; +import localFont from "next/font/local"; +import "./globals.css"; +import { Providers } from "@/components/providers"; + +const geistSans = localFont({ + src: "./fonts/GeistVF.woff", + variable: "--font-geist-sans", + weight: "100 900", +}); +const geistMono = localFont({ + src: "./fonts/GeistMonoVF.woff", + variable: "--font-geist-mono", + weight: "100 900", +}); + +export const metadata: Metadata = { + title: "GEO Platform", + description: "GEO平台 - 学术查询与引用管理", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + ); +} diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx new file mode 100644 index 0000000..eb68262 --- /dev/null +++ b/frontend/app/page.tsx @@ -0,0 +1,6 @@ +import { redirect } from "next/navigation"; + +export default function Home() { + redirect("/dashboard"); +} + diff --git a/frontend/components/charts/platform-chart.tsx b/frontend/components/charts/platform-chart.tsx new file mode 100644 index 0000000..b3df72b --- /dev/null +++ b/frontend/components/charts/platform-chart.tsx @@ -0,0 +1,67 @@ +"use client"; + +import { + BarChart, + Bar, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + Cell, +} from "recharts"; +import { PLATFORM_MAP } from "@/lib/platforms"; + +interface PlatformData { + platform: string; + rate: number; + label: string; +} + +interface PlatformChartProps { + data: Record; +} + +const COLORS = [ + "hsl(221.2 83.2% 53.3%)", + "hsl(142.1 76.2% 36.3%)", + "hsl(346.8 77.2% 49.8%)", + "hsl(24.6 95% 53.1%)", + "hsl(262.1 83.3% 57.8%)", + "hsl(188.7 94.5% 42.6%)", +]; + +export function PlatformChart({ data }: PlatformChartProps) { + const chartData: PlatformData[] = Object.entries(data).map(([platform, stats]) => ({ + platform, + rate: Math.round(stats.rate * 100), + label: PLATFORM_MAP[platform] || platform, + })); + + return ( + + + + + `${value}%`} + /> + [`引用率: ${value}%`, ""]} + /> + + {chartData.map((_, index) => ( + + ))} + + + + ); +} diff --git a/frontend/components/charts/trend-chart.tsx b/frontend/components/charts/trend-chart.tsx new file mode 100644 index 0000000..7226741 --- /dev/null +++ b/frontend/components/charts/trend-chart.tsx @@ -0,0 +1,59 @@ +"use client"; + +import { + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, +} from "recharts"; + +interface TrendData { + date: string; + citations: number; +} + +interface TrendChartProps { + data: TrendData[]; +} + +export function TrendChart({ data }: TrendChartProps) { + return ( + + + + { + const date = new Date(value); + return `${date.getMonth() + 1}/${date.getDate()}`; + }} + /> + + { + const date = new Date(value); + return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`; + }} + formatter={(value: number) => [`引用次数: ${value}`, ""]} + /> + + + + ); +} diff --git a/frontend/components/layout/header.tsx b/frontend/components/layout/header.tsx new file mode 100644 index 0000000..f4f0185 --- /dev/null +++ b/frontend/components/layout/header.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useSession, signOut } from "next-auth/react"; +import { Button } from "@/components/ui/button"; +import { LogOut, User } from "lucide-react"; + +export function Header() { + const { data: session } = useSession(); + + return ( +
    +

    GEO Platform

    +
    +
    + + {session?.user?.name || session?.user?.email || "用户"} +
    + +
    +
    + ); +} diff --git a/frontend/components/layout/sidebar.tsx b/frontend/components/layout/sidebar.tsx new file mode 100644 index 0000000..876016a --- /dev/null +++ b/frontend/components/layout/sidebar.tsx @@ -0,0 +1,55 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { cn } from "@/lib/utils"; +import { + LayoutDashboard, + Search, + Quote, + FileDown, + Settings, +} from "lucide-react"; + +const navItems = [ + { name: "数据总览", href: "/dashboard", icon: LayoutDashboard }, + { name: "查询管理", href: "/dashboard/queries", icon: Search }, + { name: "引用记录", href: "/dashboard/citations", icon: Quote }, + { name: "报告导出", href: "/dashboard/reports", icon: FileDown }, + { name: "设置", href: "/dashboard/settings", icon: Settings }, +]; + +export function Sidebar() { + const pathname = usePathname(); + + return ( + + ); +} diff --git a/frontend/components/providers.tsx b/frontend/components/providers.tsx new file mode 100644 index 0000000..8d42e47 --- /dev/null +++ b/frontend/components/providers.tsx @@ -0,0 +1,8 @@ +"use client"; + +import { SessionProvider } from "next-auth/react"; +import { ReactNode } from "react"; + +export function Providers({ children }: { children: ReactNode }) { + return {children}; +} diff --git a/frontend/components/ui/badge.tsx b/frontend/components/ui/badge.tsx new file mode 100644 index 0000000..9ec9a1a --- /dev/null +++ b/frontend/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const badgeVariants = cva( + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +); + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
    + ); +} + +export { Badge, badgeVariants }; diff --git a/frontend/components/ui/button.tsx b/frontend/components/ui/button.tsx new file mode 100644 index 0000000..de31d90 --- /dev/null +++ b/frontend/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 }; diff --git a/frontend/components/ui/card.tsx b/frontend/components/ui/card.tsx new file mode 100644 index 0000000..b375b06 --- /dev/null +++ b/frontend/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 }; diff --git a/frontend/components/ui/dialog.tsx b/frontend/components/ui/dialog.tsx new file mode 100644 index 0000000..35e22b1 --- /dev/null +++ b/frontend/components/ui/dialog.tsx @@ -0,0 +1,122 @@ +"use client"; + +import * as React from "react"; +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { X } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +const Dialog = DialogPrimitive.Root; + +const DialogTrigger = DialogPrimitive.Trigger; + +const DialogPortal = DialogPrimitive.Portal; + +const DialogClose = DialogPrimitive.Close; + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + 关闭 + + + +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
    +); +DialogHeader.displayName = "DialogHeader"; + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
    +); +DialogFooter.displayName = "DialogFooter"; + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +}; diff --git a/frontend/components/ui/dropdown-menu.tsx b/frontend/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..995409e --- /dev/null +++ b/frontend/components/ui/dropdown-menu.tsx @@ -0,0 +1,200 @@ +"use client"; + +import * as React from "react"; +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; +import { Check, ChevronRight, Circle } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +const DropdownMenu = DropdownMenuPrimitive.Root; + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; + +const DropdownMenuGroup = DropdownMenuPrimitive.Group; + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal; + +const DropdownMenuSub = DropdownMenuPrimitive.Sub; + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)); +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName; + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName; + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)); +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + +)); +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)); +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName; + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)); +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + +)); +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ); +}; +DropdownMenuShortcut.displayName = "DropdownMenuShortcut"; + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +}; diff --git a/frontend/components/ui/input.tsx b/frontend/components/ui/input.tsx new file mode 100644 index 0000000..6c0ff26 --- /dev/null +++ b/frontend/components/ui/input.tsx @@ -0,0 +1,22 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const Input = React.forwardRef>( + ({ className, type, ...props }, ref) => { + return ( + + ); + } +); +Input.displayName = "Input"; + +export { Input }; diff --git a/frontend/components/ui/label.tsx b/frontend/components/ui/label.tsx new file mode 100644 index 0000000..40378d4 --- /dev/null +++ b/frontend/components/ui/label.tsx @@ -0,0 +1,26 @@ +"use client"; + +import * as React from "react"; +import * as LabelPrimitive from "@radix-ui/react-label"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" +); + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)); +Label.displayName = LabelPrimitive.Root.displayName; + +export { Label }; diff --git a/frontend/components/ui/select.tsx b/frontend/components/ui/select.tsx new file mode 100644 index 0000000..482a1e1 --- /dev/null +++ b/frontend/components/ui/select.tsx @@ -0,0 +1,160 @@ +"use client"; + +import * as React from "react"; +import * as SelectPrimitive from "@radix-ui/react-select"; +import { Check, ChevronDown, ChevronUp } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +const Select = SelectPrimitive.Root; + +const SelectGroup = SelectPrimitive.Group; + +const SelectValue = SelectPrimitive.Value; + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + span]:line-clamp-1", + className + )} + {...props} + > + {children} + + + + +)); +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; + +const SelectScrollUpButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; + +const SelectScrollDownButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SelectScrollDownButton.displayName = + SelectPrimitive.ScrollDownButton.displayName; + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = "popper", ...props }, ref) => ( + + + + + {children} + + + + +)); +SelectContent.displayName = SelectPrimitive.Content.displayName; + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectLabel.displayName = SelectPrimitive.Label.displayName; + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + + {children} + +)); +SelectItem.displayName = SelectPrimitive.Item.displayName; + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectSeparator.displayName = SelectPrimitive.Separator.displayName; + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, +}; diff --git a/frontend/components/ui/skeleton.tsx b/frontend/components/ui/skeleton.tsx new file mode 100644 index 0000000..d7e45f7 --- /dev/null +++ b/frontend/components/ui/skeleton.tsx @@ -0,0 +1,15 @@ +import { cn } from "@/lib/utils" + +function Skeleton({ + className, + ...props +}: React.HTMLAttributes) { + return ( +
    + ) +} + +export { Skeleton } diff --git a/frontend/components/ui/table.tsx b/frontend/components/ui/table.tsx new file mode 100644 index 0000000..bb0650a --- /dev/null +++ b/frontend/components/ui/table.tsx @@ -0,0 +1,117 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const Table = React.forwardRef< + HTMLTableElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
    + + +)); +Table.displayName = "Table"; + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableHeader.displayName = "TableHeader"; + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableBody.displayName = "TableBody"; + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + tr]:last:border-b-0", + className + )} + {...props} + /> +)); +TableFooter.displayName = "TableFooter"; + +const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableRow.displayName = "TableRow"; + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
    +)); +TableHead.displayName = "TableHead"; + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableCell.displayName = "TableCell"; + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
    +)); +TableCaption.displayName = "TableCaption"; + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +}; diff --git a/frontend/components/ui/tabs.tsx b/frontend/components/ui/tabs.tsx new file mode 100644 index 0000000..fb01e0a --- /dev/null +++ b/frontend/components/ui/tabs.tsx @@ -0,0 +1,55 @@ +"use client"; + +import * as React from "react"; +import * as TabsPrimitive from "@radix-ui/react-tabs"; + +import { cn } from "@/lib/utils"; + +const Tabs = TabsPrimitive.Root; + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsList.displayName = TabsPrimitive.List.displayName; + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsContent.displayName = TabsPrimitive.Content.displayName; + +export { Tabs, TabsList, TabsTrigger, TabsContent }; diff --git a/frontend/lib/api.ts b/frontend/lib/api.ts new file mode 100644 index 0000000..097cbd6 --- /dev/null +++ b/frontend/lib/api.ts @@ -0,0 +1,78 @@ +const API_BASE = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"; + +async function fetchWithAuth( + url: string, + options: RequestInit = {}, + token?: string +) { + const headers: Record = { + "Content-Type": "application/json", + ...((options.headers as Record) || {}), + }; + if (token) { + headers["Authorization"] = `Bearer ${token}`; + } + const res = await fetch(`${API_BASE}${url}`, { ...options, headers }); + + if (res.status === 401) { + if (typeof window !== "undefined") { + window.location.href = "/login"; + } + throw new Error("登录已过期,请重新登录"); + } + + if (!res.ok) { + let errorDetail = `请求失败 (HTTP ${res.status})`; + try { + const error = await res.json(); + errorDetail = error.detail || error.message || errorDetail; + } catch { + // 解析失败,使用默认错误信息 + } + throw new Error(errorDetail); + } + + if (res.status === 204) { + return null; + } + + return res.json(); +} + +export const api = { + auth: { + register: (data: { name: string; email: string; password: string }) => + fetchWithAuth("/api/v1/auth/register", { + method: "POST", + body: JSON.stringify(data), + }), + login: (data: { email: string; password: string }) => + fetchWithAuth("/api/v1/auth/login", { + method: "POST", + body: JSON.stringify(data), + }), + getMe: (token: string) => fetchWithAuth("/api/v1/auth/me", {}, token), + }, + queries: { + list: (token: string) => fetchWithAuth("/api/v1/queries/", {}, token), + create: (token: string, data: unknown) => + fetchWithAuth("/api/v1/queries/", { method: "POST", body: JSON.stringify(data) }, token), + update: (token: string, id: string, data: unknown) => + fetchWithAuth(`/api/v1/queries/${id}`, { method: "PUT", body: JSON.stringify(data) }, token), + delete: (token: string, id: string) => + fetchWithAuth(`/api/v1/queries/${id}`, { method: "DELETE" }, token), + runNow: (token: string, id: string) => + fetchWithAuth(`/api/v1/queries/${id}/run-now`, { method: "POST" }, token), + }, + citations: { + list: (token: string, params?: string) => + fetchWithAuth(`/api/v1/citations/${params ? `?${params}` : ""}`, {}, token), + getStats: (token: string) => fetchWithAuth("/api/v1/citations/stats/", {}, token), + }, + reports: { + exportCSV: (token: string, queryId?: string) => { + const query = queryId ? `?query_id=${queryId}` : ""; + return fetchWithAuth(`/api/v1/reports/export/csv${query}`, {}, token); + }, + }, +}; diff --git a/frontend/lib/auth.ts b/frontend/lib/auth.ts new file mode 100644 index 0000000..5ab1052 --- /dev/null +++ b/frontend/lib/auth.ts @@ -0,0 +1,55 @@ +import { NextAuthOptions } from "next-auth"; +import CredentialsProvider from "next-auth/providers/credentials"; +import { api } from "@/lib/api"; + +export const authOptions: NextAuthOptions = { + providers: [ + CredentialsProvider({ + name: "credentials", + credentials: { + email: { label: "邮箱", type: "email" }, + password: { label: "密码", type: "password" }, + }, + async authorize(credentials) { + if (!credentials?.email || !credentials?.password) return null; + try { + const res = await api.auth.login({ + email: credentials.email, + password: credentials.password, + }); + if (res.access_token) { + return { + id: res.user?.id || credentials.email, + name: res.user?.name, + email: res.user?.email, + accessToken: res.access_token, + }; + } + return null; + } catch { + return null; + } + }, + }), + ], + session: { + strategy: "jwt", + }, + callbacks: { + async jwt({ token, user }) { + if (user) { + token.accessToken = user.accessToken; + token.id = user.id; + } + return token; + }, + async session({ session, token }) { + session.accessToken = token.accessToken as string; + session.user.id = token.id as string; + return session; + }, + }, + pages: { + signIn: "/login", + }, +}; diff --git a/frontend/lib/platforms.ts b/frontend/lib/platforms.ts new file mode 100644 index 0000000..48e9b30 --- /dev/null +++ b/frontend/lib/platforms.ts @@ -0,0 +1,17 @@ +export const PLATFORM_MAP: Record = { + wenxin: "文心一言", + kimi: "Kimi", + tongyi: "通义千问", + baidu_ai: "百度AI搜索", + yuanbao: "腾讯元宝", + qingyan: "智谱清言", +}; + +export const PLATFORMS = [ + { key: "wenxin", label: "文心一言" }, + { key: "kimi", label: "Kimi" }, + { key: "tongyi", label: "通义千问" }, + { key: "baidu_ai", label: "百度AI搜索" }, + { key: "yuanbao", label: "腾讯元宝" }, + { key: "qingyan", label: "智谱清言" }, +]; diff --git a/frontend/lib/utils.ts b/frontend/lib/utils.ts new file mode 100644 index 0000000..365058c --- /dev/null +++ b/frontend/lib/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/frontend/next.config.mjs b/frontend/next.config.mjs new file mode 100644 index 0000000..4678774 --- /dev/null +++ b/frontend/next.config.mjs @@ -0,0 +1,4 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = {}; + +export default nextConfig; diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..60b9081 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,7500 @@ +{ + "name": "frontend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.1.0", + "dependencies": { + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.13", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^1.8.0", + "next": "14.2.35", + "next-auth": "^4.24.14", + "react": "^18", + "react-dom": "^18", + "recharts": "^2.15.4", + "tailwind-merge": "^3.5.0", + "tailwindcss-animate": "^1.0.7" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "eslint": "^8", + "eslint-config-next": "14.2.35", + "postcss": "^8", + "tailwindcss": "^3.4.1", + "typescript": "^5" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmmirror.com/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmmirror.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmmirror.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmmirror.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.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": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmmirror.com/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.8", + "resolved": "https://registry.npmmirror.com/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@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/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "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.npmmirror.com/@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": "14.2.35", + "resolved": "https://registry.npmmirror.com/@next/env/-/env-14.2.35.tgz", + "integrity": "sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ==", + "license": "MIT" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "14.2.35", + "resolved": "https://registry.npmmirror.com/@next/eslint-plugin-next/-/eslint-plugin-next-14.2.35.tgz", + "integrity": "sha512-Jw9A3ICz2183qSsqwi7fgq4SBPiNfmOLmTPXKvlnzstUwyvBrtySiY+8RXJweNAs9KThb1+bYhZh9XWcNOr2zQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "10.3.10" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "14.2.33", + "resolved": "https://registry.npmmirror.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.33.tgz", + "integrity": "sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "14.2.33", + "resolved": "https://registry.npmmirror.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.33.tgz", + "integrity": "sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "14.2.33", + "resolved": "https://registry.npmmirror.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.33.tgz", + "integrity": "sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "14.2.33", + "resolved": "https://registry.npmmirror.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.33.tgz", + "integrity": "sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "14.2.33", + "resolved": "https://registry.npmmirror.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.33.tgz", + "integrity": "sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "14.2.33", + "resolved": "https://registry.npmmirror.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.33.tgz", + "integrity": "sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "14.2.33", + "resolved": "https://registry.npmmirror.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.33.tgz", + "integrity": "sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.2.33", + "resolved": "https://registry.npmmirror.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.33.tgz", + "integrity": "sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "14.2.33", + "resolved": "https://registry.npmmirror.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.33.tgz", + "integrity": "sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "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.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "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.npmmirror.com/@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/@panva/hkdf": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/@panva/hkdf/-/hkdf-1.2.1.tgz", + "integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmmirror.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/@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/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@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-context": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "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-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/@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/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "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-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "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-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "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-label": { + "version": "2.1.8", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-label/-/react-label-2.1.8.tgz", + "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/@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/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/@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/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.6", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/@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/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "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/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "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-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "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-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "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-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "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-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "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-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "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-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "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-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "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-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.16.1", + "resolved": "https://registry.npmmirror.com/@rushstack/eslint-patch/-/eslint-patch-1.16.1.tgz", + "integrity": "sha512-TvZbIpeKqGQQ7X0zSCvPH9riMSFQFSggnfBjFZ1mEoILW+UuXCKwOoPcgjMwiUtRqFZ8jWhPJc4um14vC6I4ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmmirror.com/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, + "node_modules/@swc/helpers": { + "version": "0.5.5", + "resolved": "https://registry.npmmirror.com/@swc/helpers/-/helpers-0.5.5.tgz", + "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "tslib": "^2.4.0" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmmirror.com/@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/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmmirror.com/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmmirror.com/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmmirror.com/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmmirror.com/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.39", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-20.19.39.tgz", + "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmmirror.com/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmmirror.com/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.59.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.0.tgz", + "integrity": "sha512-HyAZtpdkgZwpq8Sz3FSUvCR4c+ScbuWa9AksK2Jweub7w4M3yTz4O11AqVJzLYjy/B9ZWPyc81I+mOdJU/bDQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.59.0", + "@typescript-eslint/type-utils": "8.59.0", + "@typescript-eslint/utils": "8.59.0", + "@typescript-eslint/visitor-keys": "8.59.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.59.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.59.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.59.0.tgz", + "integrity": "sha512-TI1XGwKbDpo9tRW8UDIXCOeLk55qe9ZFGs8MTKU6/M08HWTw52DD/IYhfQtOEhEdPhLMT26Ka/x7p70nd3dzDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.59.0", + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/typescript-estree": "8.59.0", + "@typescript-eslint/visitor-keys": "8.59.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.59.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.59.0.tgz", + "integrity": "sha512-Lw5ITrR5s5TbC19YSvlr63ZfLaJoU6vtKTHyB0GQOpX0W7d5/Ir6vUahWi/8Sps/nOukZQ0IB3SmlxZnjaKVnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.59.0", + "@typescript-eslint/types": "^8.59.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.59.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.59.0.tgz", + "integrity": "sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/visitor-keys": "8.59.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.59.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.0.tgz", + "integrity": "sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.59.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.59.0.tgz", + "integrity": "sha512-3TRiZaQSltGqGeNrJzzr1+8YcEobKH9rHnqIp/1psfKFmhRQDNMGP5hBufanYTGznwShzVLs3Mz+gDN7HkWfXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/typescript-estree": "8.59.0", + "@typescript-eslint/utils": "8.59.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.59.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.59.0.tgz", + "integrity": "sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A==", + "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.59.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.0.tgz", + "integrity": "sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.59.0", + "@typescript-eslint/tsconfig-utils": "8.59.0", + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/visitor-keys": "8.59.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.59.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.59.0.tgz", + "integrity": "sha512-I1R/K7V07XsMJ12Oaxg/O9GfrysGTmCRhvZJBv0RE0NcULMzjqVpR5kRRQjHsz3J/bElU7HwCO7zkqL+MSUz+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.59.0", + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/typescript-estree": "8.59.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.59.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.0.tgz", + "integrity": "sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.16.0", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/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.14.0", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/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/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmmirror.com/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmmirror.com/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.3", + "resolved": "https://registry.npmmirror.com/axe-core/-/axe-core-4.11.3.tgz", + "integrity": "sha512-zBQouZixDTbo3jMGqHKyePxYxr1e5W8UdTmBQ7sNtaA9M2bE32daxxPLS/jojhKOHxQ7LWwPjfiwf/fhaJWzlg==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "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.npmmirror.com/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001790", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001790.tgz", + "integrity": "sha512-bOoxfJPyYo+ds6W0YfptaCWbFnJYjh2Y1Eow5lRv+vI2u8ganPZqNm1JwNh0t2ELQCqIWg4B3dWEusgAmsoyOw==", + "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.npmmirror.com/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/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmmirror.com/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "license": "MIT" + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/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/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-abstract": { + "version": "1.24.2", + "resolved": "https://registry.npmmirror.com/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", + "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.npmmirror.com/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.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.3.2", + "resolved": "https://registry.npmmirror.com/es-iterator-helpers/-/es-iterator-helpers-1.3.2.tgz", + "integrity": "sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.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", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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": "8.57.1", + "resolved": "https://registry.npmmirror.com/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-next": { + "version": "14.2.35", + "resolved": "https://registry.npmmirror.com/eslint-config-next/-/eslint-config-next-14.2.35.tgz", + "integrity": "sha512-BpLsv01UisH193WyT/1lpHqq5iJ/Orfz9h/NOOlAmTUq4GY349PextQ62K4XpnaM9supeiEn3TaOTeQO07gURg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@next/eslint-plugin-next": "14.2.35", + "@rushstack/eslint-patch": "^1.3.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.28.1", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.5.0 || 5.0.0-canary-7118f5dd7-20230705" + }, + "peerDependencies": { + "eslint": "^7.23.0 || ^8.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.10", + "resolved": "https://registry.npmmirror.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz", + "integrity": "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.16.1", + "resolve": "^2.0.0-next.6" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/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/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.0.0-canary-7118f5dd7-20230705", + "resolved": "https://registry.npmmirror.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.0.0-canary-7118f5dd7-20230705.tgz", + "integrity": "sha512-AZYbMo/NW9chdL7vk6HQzQhT+PvTAEVqWk9ziruUoW2kAOcN5qNyelv70e0F1VNQAbvutOC9oc+xfWycI9FxDw==", + "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" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/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/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/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": "7.2.2", + "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmmirror.com/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/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmmirror.com/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmmirror.com/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-equals": { + "version": "5.4.0", + "resolved": "https://registry.npmmirror.com/fast-equals/-/fast-equals-5.4.0.tgz", + "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "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/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/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": "3.2.0", + "resolved": "https://registry.npmmirror.com/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmmirror.com/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmmirror.com/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/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.14.0", + "resolved": "https://registry.npmmirror.com/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "10.3.10", + "resolved": "https://registry.npmmirror.com/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmmirror.com/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.3", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/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/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/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-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/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-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmmirror.com/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmmirror.com/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmmirror.com/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/lucide-react": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/lucide-react/-/lucide-react-1.8.0.tgz", + "integrity": "sha512-WuvlsjngSk7TnTBJ1hsCy3ql9V9VOdcPkd3PKcSmM34vJD8KG6molxz7m7zbYFgICwsanQWmJ13JlYs4Zp7Arw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/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.3", + "resolved": "https://registry.npmmirror.com/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmmirror.com/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/next": { + "version": "14.2.35", + "resolved": "https://registry.npmmirror.com/next/-/next-14.2.35.tgz", + "integrity": "sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig==", + "license": "MIT", + "dependencies": { + "@next/env": "14.2.35", + "@swc/helpers": "0.5.5", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001579", + "graceful-fs": "^4.2.11", + "postcss": "8.4.31", + "styled-jsx": "5.1.1" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=18.17.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "14.2.33", + "@next/swc-darwin-x64": "14.2.33", + "@next/swc-linux-arm64-gnu": "14.2.33", + "@next/swc-linux-arm64-musl": "14.2.33", + "@next/swc-linux-x64-gnu": "14.2.33", + "@next/swc-linux-x64-musl": "14.2.33", + "@next/swc-win32-arm64-msvc": "14.2.33", + "@next/swc-win32-ia32-msvc": "14.2.33", + "@next/swc-win32-x64-msvc": "14.2.33" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next-auth": { + "version": "4.24.14", + "resolved": "https://registry.npmmirror.com/next-auth/-/next-auth-4.24.14.tgz", + "integrity": "sha512-YRz6xFDXKUwiXSMMChbrBEWyFktZ1qZXEgeSHQQ3nsy08B4c/xLk6REeutRsIFwkjY/1+ShHnu07DN3JeJguig==", + "license": "ISC", + "dependencies": { + "@babel/runtime": "^7.20.13", + "@panva/hkdf": "^1.0.2", + "cookie": "^0.7.0", + "jose": "^4.15.5", + "oauth": "^0.9.15", + "openid-client": "^5.4.0", + "preact": "^10.6.3", + "preact-render-to-string": "^5.1.19", + "uuid": "^8.3.2" + }, + "peerDependencies": { + "@auth/core": "0.34.3", + "next": "^12.2.5 || ^13 || ^14 || ^15 || ^16", + "nodemailer": "^7.0.7", + "react": "^17.0.2 || ^18 || ^19", + "react-dom": "^17.0.2 || ^18 || ^19" + }, + "peerDependenciesMeta": { + "@auth/core": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmmirror.com/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/node-exports-info": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/node-exports-info/-/node-exports-info-1.6.0.tgz", + "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-exports-info/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/oauth": { + "version": "0.9.15", + "resolved": "https://registry.npmmirror.com/oauth/-/oauth-0.9.15.tgz", + "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==", + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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/oidc-token-hash": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/oidc-token-hash/-/oidc-token-hash-5.2.0.tgz", + "integrity": "sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw==", + "license": "MIT", + "engines": { + "node": "^10.13.0 || >=12.0.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/openid-client": { + "version": "5.7.1", + "resolved": "https://registry.npmmirror.com/openid-client/-/openid-client-5.7.1.tgz", + "integrity": "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==", + "license": "MIT", + "dependencies": { + "jose": "^4.15.9", + "lru-cache": "^6.0.0", + "object-hash": "^2.2.0", + "oidc-token-hash": "^5.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/openid-client/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/openid-client/node_modules/object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmmirror.com/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/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.10", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "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/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmmirror.com/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-import/node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "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/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmmirror.com/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/preact": { + "version": "10.29.1", + "resolved": "https://registry.npmmirror.com/preact/-/preact-10.29.1.tgz", + "integrity": "sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/preact-render-to-string": { + "version": "5.2.6", + "resolved": "https://registry.npmmirror.com/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz", + "integrity": "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==", + "license": "MIT", + "dependencies": { + "pretty-format": "^3.8.0" + }, + "peerDependencies": { + "preact": ">=10" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-format": { + "version": "3.8.0", + "resolved": "https://registry.npmmirror.com/pretty-format/-/pretty-format-3.8.0.tgz", + "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==", + "license": "MIT" + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmmirror.com/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "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": "18.3.1", + "resolved": "https://registry.npmmirror.com/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmmirror.com/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmmirror.com/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/recharts": { + "version": "2.15.4", + "resolved": "https://registry.npmmirror.com/recharts/-/recharts-2.15.4.tgz", + "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmmirror.com/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/recharts/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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": "2.0.0-next.6", + "resolved": "https://registry.npmmirror.com/resolve/-/resolve-2.0.0-next.6.tgz", + "integrity": "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "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.4", + "resolved": "https://registry.npmmirror.com/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "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.npmmirror.com/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.npmmirror.com/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.23.2", + "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.1", + "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.1", + "resolved": "https://registry.npmmirror.com/styled-jsx/-/styled-jsx-5.1.1.tgz", + "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", + "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" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmmirror.com/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwind-merge": { + "version": "3.5.0", + "resolved": "https://registry.npmmirror.com/tailwind-merge/-/tailwind-merge-3.5.0.tgz", + "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss-animate": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, + "node_modules/tailwindcss/node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "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/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmmirror.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "license": "Apache-2.0" + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmmirror.com/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmmirror.com/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.20", + "resolved": "https://registry.npmmirror.com/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "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.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/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..280bb84 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,39 @@ +{ + "name": "frontend", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.13", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^1.8.0", + "next": "14.2.35", + "next-auth": "^4.24.14", + "react": "^18", + "react-dom": "^18", + "recharts": "^2.15.4", + "tailwind-merge": "^3.5.0", + "tailwindcss-animate": "^1.0.7" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "eslint": "^8", + "eslint-config-next": "14.2.35", + "postcss": "^8", + "tailwindcss": "^3.4.1", + "typescript": "^5" + } +} diff --git a/frontend/postcss.config.mjs b/frontend/postcss.config.mjs new file mode 100644 index 0000000..1a69fd2 --- /dev/null +++ b/frontend/postcss.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + tailwindcss: {}, + }, +}; + +export default config; diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts new file mode 100644 index 0000000..e86b782 --- /dev/null +++ b/frontend/tailwind.config.ts @@ -0,0 +1,56 @@ +import type { Config } from "tailwindcss"; + +const config: Config = { + darkMode: ["class"], + content: [ + "./pages/**/*.{js,ts,jsx,tsx,mdx}", + "./components/**/*.{js,ts,jsx,tsx,mdx}", + "./app/**/*.{js,ts,jsx,tsx,mdx}", + ], + theme: { + 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)", + }, + }, + }, + plugins: [require("tailwindcss-animate")], +}; +export default config; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..e7ff90f --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "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": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/frontend/types/next-auth.d.ts b/frontend/types/next-auth.d.ts new file mode 100644 index 0000000..f8dd5e1 --- /dev/null +++ b/frontend/types/next-auth.d.ts @@ -0,0 +1,25 @@ +import NextAuth from "next-auth"; + +declare module "next-auth" { + interface Session { + accessToken: string; + user: { + id: string; + name?: string | null; + email?: string | null; + image?: string | null; + }; + } + + interface User { + id: string; + accessToken: string; + } +} + +declare module "next-auth/jwt" { + interface JWT { + accessToken?: string; + id?: string; + } +} diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..97c87de --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,122 @@ +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent.parent / "backend")) + +import pytest +import pytest_asyncio +import uuid +from datetime import datetime +from unittest.mock import AsyncMock, patch + +from httpx import ASGITransport, AsyncClient +from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession +from sqlalchemy.pool import StaticPool + +from app.main import app +from app.api.deps import get_current_user +from app.database import Base, get_db +from app.services.auth import create_access_token + + +# --------------------------------------------------------------------------- +# 全局 mock:防止启动真实调度器 / Playwright 浏览器 +# --------------------------------------------------------------------------- +@pytest.fixture(scope="session", autouse=True) +def mock_scheduler(): + """Mock the query scheduler to prevent real background jobs in tests.""" + with patch("app.main.query_scheduler") as mock_sched: + mock_sched.start = lambda: None + mock_sched.shutdown = AsyncMock() + yield + + +# --------------------------------------------------------------------------- +# 内存数据库 fixture(供集成测试使用) +# --------------------------------------------------------------------------- +TEST_DATABASE_URL = "sqlite+aiosqlite:///:memory:" + + +@pytest_asyncio.fixture +async def test_engine(): + """Create a fresh in-memory SQLite engine for each test function.""" + engine = create_async_engine( + TEST_DATABASE_URL, + echo=False, + future=True, + poolclass=StaticPool, + connect_args={"check_same_thread": False}, + ) + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + yield engine + await engine.dispose() + + +@pytest_asyncio.fixture +async def test_session(test_engine) -> AsyncSession: + """Yield an async session bound to the in-memory engine.""" + async_session = async_sessionmaker( + test_engine, class_=AsyncSession, expire_on_commit=False, autoflush=False, autocommit=False + ) + async with async_session() as session: + yield session + + +@pytest_asyncio.fixture +async def override_get_db(test_session): + """Override FastAPI get_db dependency to use the test session.""" + async def _get_db(): + yield test_session + + app.dependency_overrides[get_db] = _get_db + yield test_session + app.dependency_overrides.pop(get_db, None) + + +# --------------------------------------------------------------------------- +# Mock 用户 fixture(供单元测试使用) +# --------------------------------------------------------------------------- +@pytest.fixture +def mock_user(): + """Return a mock authenticated user.""" + user = AsyncMock() + user.id = uuid.UUID("12345678-1234-1234-1234-123456789abc") + user.email = "test@example.com" + user.name = "Test User" + user.plan = "free" + user.max_queries = 5 + user.is_active = True + user.created_at = datetime.now() + return user + + +@pytest.fixture +def override_get_current_user(mock_user): + """Override the get_current_user dependency to return a mock user.""" + async def _override(): + return mock_user + + app.dependency_overrides[get_current_user] = _override + yield + app.dependency_overrides.pop(get_current_user, None) + + +@pytest.fixture +def auth_token(mock_user): + """Generate a valid JWT access token for the mock user.""" + return create_access_token(data={"sub": str(mock_user.id)}) + + +@pytest.fixture +def auth_headers(auth_token): + """Return request headers containing the Bearer token.""" + return {"Authorization": f"Bearer {auth_token}"} + + +@pytest_asyncio.fixture +async def async_client(): + """Create an async HTTP client for testing the FastAPI app.""" + transport = ASGITransport(app=app) + async with AsyncClient(transport=transport, base_url="http://test") as client: + yield client diff --git a/tests/test_auth.py b/tests/test_auth.py new file mode 100644 index 0000000..500535e --- /dev/null +++ b/tests/test_auth.py @@ -0,0 +1,103 @@ +import uuid +from datetime import datetime +from unittest.mock import AsyncMock, patch + +import pytest + +from app.main import app +from app.api.deps import get_current_user + + +@pytest.fixture +def mock_registered_user(): + """Return a mock user representing a successfully registered user.""" + user = AsyncMock() + user.id = uuid.UUID("12345678-1234-1234-1234-123456789abc") + user.email = "new@example.com" + user.name = "New User" + user.plan = "free" + user.max_queries = 5 + user.is_active = True + user.created_at = datetime.now() + return user + + +@pytest.mark.asyncio +async def test_register_success(async_client, mock_registered_user): + with patch("app.api.auth.register_user", return_value=mock_registered_user): + response = await async_client.post( + "/api/v1/auth/register", + json={ + "email": "new@example.com", + "password": "password123", + "name": "New User", + }, + ) + assert response.status_code == 201 + data = response.json() + assert data["email"] == "new@example.com" + assert data["name"] == "New User" + + +@pytest.mark.asyncio +async def test_register_duplicate_email(async_client): + with patch( + "app.api.auth.register_user", + side_effect=ValueError("Email already registered"), + ): + response = await async_client.post( + "/api/v1/auth/register", + json={ + "email": "existing@example.com", + "password": "password123", + "name": "Existing User", + }, + ) + assert response.status_code == 400 + data = response.json() + assert "Email already registered" in data["detail"] + + +@pytest.mark.asyncio +async def test_login_success(async_client, mock_registered_user): + with patch("app.api.auth.authenticate_user", return_value=mock_registered_user): + response = await async_client.post( + "/api/v1/auth/login", + json={"email": "test@example.com", "password": "password123"}, + ) + assert response.status_code == 200 + data = response.json() + assert "access_token" in data + assert data["token_type"] == "bearer" + assert data["user"]["email"] == "new@example.com" + + +@pytest.mark.asyncio +async def test_login_wrong_password(async_client): + with patch("app.api.auth.authenticate_user", return_value=None): + response = await async_client.post( + "/api/v1/auth/login", + json={"email": "test@example.com", "password": "wrongpassword"}, + ) + assert response.status_code == 401 + data = response.json() + assert "Incorrect email or password" in data["detail"] + + +@pytest.mark.asyncio +async def test_get_me_authenticated( + async_client, override_get_current_user, auth_headers +): + response = await async_client.get("/api/v1/auth/me", headers=auth_headers) + assert response.status_code == 200 + data = response.json() + assert data["email"] == "test@example.com" + assert data["name"] == "Test User" + + +@pytest.mark.asyncio +async def test_get_me_unauthenticated(async_client): + # Ensure no auth override is active + app.dependency_overrides.pop(get_current_user, None) + response = await async_client.get("/api/v1/auth/me") + assert response.status_code == 401 diff --git a/tests/test_business_flow.py b/tests/test_business_flow.py new file mode 100644 index 0000000..a2da714 --- /dev/null +++ b/tests/test_business_flow.py @@ -0,0 +1,440 @@ +import uuid +from datetime import datetime, timezone, timedelta +from unittest.mock import AsyncMock, patch + +import pytest +import pytest_asyncio +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.main import app +from app.api.deps import get_current_user +from app.database import get_db +from app.models.citation_record import CitationRecord +from app.models.query import Query +from app.models.query_task import QueryTask +from app.models.user import User +from app.services.auth import hash_password + + +# --------------------------------------------------------------------------- +# Helper fixtures / functions +# --------------------------------------------------------------------------- +@pytest_asyncio.fixture +async def user_a(test_session: AsyncSession): + """Create a real user in the test DB.""" + user = User( + email="user_a@example.com", + password_hash=hash_password("password123"), + name="User A", + plan="free", + max_queries=5, + ) + test_session.add(user) + await test_session.commit() + await test_session.refresh(user) + return user + + +@pytest_asyncio.fixture +async def user_b(test_session: AsyncSession): + """Create another real user in the test DB.""" + user = User( + email="user_b@example.com", + password_hash=hash_password("password456"), + name="User B", + plan="free", + max_queries=5, + ) + test_session.add(user) + await test_session.commit() + await test_session.refresh(user) + return user + + +@pytest_asyncio.fixture +async def auth_client_a(async_client, user_a): + """Login user_a and return client with auth headers.""" + response = await async_client.post( + "/api/v1/auth/login", + json={"email": "user_a@example.com", "password": "password123"}, + ) + assert response.status_code == 200 + token = response.json()["access_token"] + # Return a small helper or just the headers + return {"Authorization": f"Bearer {token}"} + + +@pytest_asyncio.fixture +async def auth_client_b(async_client, user_b): + """Login user_b and return auth headers.""" + response = await async_client.post( + "/api/v1/auth/login", + json={"email": "user_b@example.com", "password": "password456"}, + ) + assert response.status_code == 200 + token = response.json()["access_token"] + return {"Authorization": f"Bearer {token}"} + + +# --------------------------------------------------------------------------- +# 1. 完整流程:注册 -> 登录 -> 创建查询词 -> 查看列表 +# --------------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_full_user_flow(async_client, override_get_db): + # Register + reg_resp = await async_client.post( + "/api/v1/auth/register", + json={"email": "flow@example.com", "password": "flowpass", "name": "Flow User"}, + ) + assert reg_resp.status_code == 201 + reg_data = reg_resp.json() + assert reg_data["email"] == "flow@example.com" + + # Login + login_resp = await async_client.post( + "/api/v1/auth/login", + json={"email": "flow@example.com", "password": "flowpass"}, + ) + assert login_resp.status_code == 200 + token = login_resp.json()["access_token"] + headers = {"Authorization": f"Bearer {token}"} + + # Create query + create_resp = await async_client.post( + "/api/v1/queries/", + headers=headers, + json={ + "keyword": "flow keyword", + "target_brand": "FlowBrand", + "platforms": ["wenxin"], + "frequency": "daily", + }, + ) + assert create_resp.status_code == 201 + query_data = create_resp.json() + assert query_data["keyword"] == "flow keyword" + assert query_data["target_brand"] == "FlowBrand" + + # List queries + list_resp = await async_client.get("/api/v1/queries/", headers=headers) + assert list_resp.status_code == 200 + list_data = list_resp.json() + assert list_data["total"] == 1 + assert len(list_data["items"]) == 1 + assert list_data["items"][0]["keyword"] == "flow keyword" + + +# --------------------------------------------------------------------------- +# 2. 查询词生命周期:创建 -> 更新 -> 暂停 -> 恢复 -> 删除 +# --------------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_query_lifecycle(async_client, override_get_db, auth_client_a): + headers = auth_client_a + + # Create + create_resp = await async_client.post( + "/api/v1/queries/", + headers=headers, + json={ + "keyword": "lifecycle keyword", + "target_brand": "LifecycleBrand", + "platforms": ["wenxin"], + "frequency": "weekly", + }, + ) + assert create_resp.status_code == 201 + query_id = create_resp.json()["id"] + + # Update + update_resp = await async_client.put( + f"/api/v1/queries/{query_id}", + headers=headers, + json={"keyword": "updated keyword", "frequency": "daily"}, + ) + assert update_resp.status_code == 200 + assert update_resp.json()["keyword"] == "updated keyword" + assert update_resp.json()["frequency"] == "daily" + + # Pause (update status to paused) + pause_resp = await async_client.put( + f"/api/v1/queries/{query_id}", + headers=headers, + json={"status": "paused"}, + ) + assert pause_resp.status_code == 200 + assert pause_resp.json()["status"] == "paused" + + # Resume (update status back to active) + resume_resp = await async_client.put( + f"/api/v1/queries/{query_id}", + headers=headers, + json={"status": "active"}, + ) + assert resume_resp.status_code == 200 + assert resume_resp.json()["status"] == "active" + + # Delete + delete_resp = await async_client.delete( + f"/api/v1/queries/{query_id}", + headers=headers, + ) + assert delete_resp.status_code == 204 + + # Verify deletion + get_resp = await async_client.get(f"/api/v1/queries/{query_id}", headers=headers) + assert get_resp.status_code == 404 + + +# --------------------------------------------------------------------------- +# 3. 查询数量限制:free 用户最多 5 个 +# --------------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_query_limit_free_user(async_client, override_get_db, auth_client_a): + headers = auth_client_a + + # Create 5 queries (limit for free plan) + for i in range(5): + resp = await async_client.post( + "/api/v1/queries/", + headers=headers, + json={ + "keyword": f"keyword {i}", + "target_brand": f"Brand{i}", + "platforms": ["wenxin"], + "frequency": "daily", + }, + ) + assert resp.status_code == 201, f"Failed to create query {i}" + + # 6th query should be rejected + resp = await async_client.post( + "/api/v1/queries/", + headers=headers, + json={ + "keyword": "over limit", + "target_brand": "OverBrand", + "platforms": ["wenxin"], + "frequency": "daily", + }, + ) + assert resp.status_code == 403 + assert "Query limit exceeded" in resp.json()["detail"] + + +# --------------------------------------------------------------------------- +# 4. 引用统计数据正确性 +# --------------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_citation_stats_correctness( + async_client, override_get_db, auth_client_a, test_session +): + headers = auth_client_a + + # Create a query + create_resp = await async_client.post( + "/api/v1/queries/", + headers=headers, + json={ + "keyword": "stats keyword", + "target_brand": "StatsBrand", + "platforms": ["wenxin", "kimi"], + "frequency": "weekly", + }, + ) + assert create_resp.status_code == 201 + query_id = uuid.UUID(create_resp.json()["id"]) + + # Insert citation records directly + records = [ + CitationRecord( + query_id=query_id, + platform="wenxin", + cited=True, + citation_position=1, + citation_text="mention 1", + competitor_brands=[], + ), + CitationRecord( + query_id=query_id, + platform="wenxin", + cited=False, + citation_position=None, + citation_text=None, + competitor_brands=[], + ), + CitationRecord( + query_id=query_id, + platform="kimi", + cited=True, + citation_position=2, + citation_text="mention 2", + competitor_brands=[], + ), + ] + for r in records: + test_session.add(r) + await test_session.commit() + + # Call stats API + stats_resp = await async_client.get("/api/v1/citations/stats", headers=headers) + assert stats_resp.status_code == 200 + stats = stats_resp.json() + + assert stats["total_queries"] == 3 + assert stats["total_citations"] == 2 + assert stats["citation_rate"] == pytest.approx(0.67, abs=0.01) + assert stats["avg_position"] == pytest.approx(1.5, abs=0.1) + + by_platform = stats["by_platform"] + assert "wenxin" in by_platform + assert "kimi" in by_platform + assert by_platform["wenxin"]["queries"] == 2 + assert by_platform["wenxin"]["citations"] == 1 + assert by_platform["kimi"]["queries"] == 1 + assert by_platform["kimi"]["citations"] == 1 + + +# --------------------------------------------------------------------------- +# 5. CSV 导出功能 +# --------------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_export_csv( + async_client, override_get_db, auth_client_a, test_session +): + headers = auth_client_a + + # Create query + create_resp = await async_client.post( + "/api/v1/queries/", + headers=headers, + json={ + "keyword": "csv keyword", + "target_brand": "CSVBrand", + "platforms": ["wenxin"], + "frequency": "weekly", + }, + ) + assert create_resp.status_code == 201 + query_id = create_resp.json()["id"] + + # Insert a citation record + record = CitationRecord( + query_id=uuid.UUID(query_id), + platform="wenxin", + cited=True, + citation_position=1, + citation_text="CSV test text", + competitor_brands=[], + ) + test_session.add(record) + await test_session.commit() + + # Export CSV + export_resp = await async_client.get( + f"/api/v1/reports/export/csv?query_id={query_id}", + headers=headers, + ) + assert export_resp.status_code == 200 + assert export_resp.headers["content-type"].startswith("text/csv") + body = export_resp.text + assert "CSV test text" in body + assert "wenxin" in body + + +# --------------------------------------------------------------------------- +# 6. 手动触发查询:POST run-now 应创建 QueryTask 记录 +# --------------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_run_now_creates_query_task( + async_client, override_get_db, auth_client_a, test_session +): + headers = auth_client_a + + # Create an active query + create_resp = await async_client.post( + "/api/v1/queries/", + headers=headers, + json={ + "keyword": "run now keyword", + "target_brand": "RunNowBrand", + "platforms": ["wenxin", "kimi"], + "frequency": "weekly", + }, + ) + assert create_resp.status_code == 201 + query_id = create_resp.json()["id"] + + # Trigger run-now + run_resp = await async_client.post( + f"/api/v1/queries/{query_id}/run-now", + headers=headers, + ) + assert run_resp.status_code == 202 + data = run_resp.json() + assert data["status"] == "pending" + assert "task_id" in data + + # Verify QueryTask records in DB + stmt = select(QueryTask).where(QueryTask.query_id == uuid.UUID(query_id)) + result = await test_session.execute(stmt) + tasks = result.scalars().all() + assert len(tasks) == 2 + platforms = {t.platform for t in tasks} + assert platforms == {"wenxin", "kimi"} + for t in tasks: + assert t.status == "pending" + + +# --------------------------------------------------------------------------- +# 7. 权限隔离:用户 A 无法访问用户 B 的数据 +# --------------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_permission_isolation( + async_client, override_get_db, auth_client_a, auth_client_b +): + headers_a = auth_client_a + headers_b = auth_client_b + + # User A creates a query + create_resp = await async_client.post( + "/api/v1/queries/", + headers=headers_a, + json={ + "keyword": "private keyword", + "target_brand": "PrivateBrand", + "platforms": ["wenxin"], + "frequency": "weekly", + }, + ) + assert create_resp.status_code == 201 + query_id = create_resp.json()["id"] + + # User B tries to access User A's query + get_resp = await async_client.get( + f"/api/v1/queries/{query_id}", + headers=headers_b, + ) + assert get_resp.status_code == 404 + + # User B tries to update User A's query + put_resp = await async_client.put( + f"/api/v1/queries/{query_id}", + headers=headers_b, + json={"keyword": "hacked"}, + ) + assert put_resp.status_code == 404 + + # User B tries to delete User A's query + del_resp = await async_client.delete( + f"/api/v1/queries/{query_id}", + headers=headers_b, + ) + assert del_resp.status_code == 404 + + # User B tries to run-now User A's query + run_resp = await async_client.post( + f"/api/v1/queries/{query_id}/run-now", + headers=headers_b, + ) + assert run_resp.status_code == 404 diff --git a/tests/test_citation_engine.py b/tests/test_citation_engine.py new file mode 100644 index 0000000..bb8757e --- /dev/null +++ b/tests/test_citation_engine.py @@ -0,0 +1,126 @@ +import pytest + +from app.workers.citation_engine import BrandMatcher, CompetitorDetector + + +def test_brand_matcher_exact(): + matcher = BrandMatcher(target_brand="华为", brand_aliases=["Huawei"]) + result = matcher.match("华为是一家伟大的科技公司") + assert result["cited"] is True + assert result["match_type"] == "exact" + assert result["confidence"] == 1.0 + + +def test_brand_matcher_alias(): + matcher = BrandMatcher(target_brand="华为", brand_aliases=["Huawei"]) + result = matcher.match("Huawei makes great phones") + assert result["cited"] is True + assert result["match_type"] == "alias" + assert result["confidence"] == 0.9 + + +def test_brand_matcher_fuzzy(): + matcher = BrandMatcher(target_brand="华为") + # "华伟" is a fuzzy match to "华为" + result = matcher.match("华伟 是一家科技公司") + assert result["cited"] is True + assert result["match_type"] == "fuzzy" + assert result["confidence"] > 0.4 + + +def test_brand_matcher_no_match(): + matcher = BrandMatcher(target_brand="华为") + result = matcher.match("这是一段完全不相关的文本,没有任何品牌信息") + assert result["cited"] is False + assert result["match_type"] is None + assert result["confidence"] == 0.0 + + +def test_competitor_detector(): + detector = CompetitorDetector() + text = "中国平安和中国人寿都是大型保险公司" + competitors = detector.detect(text, target_brand="中国平安") + assert "中国人寿" in competitors + assert "中国平安" not in competitors + + +def test_citation_position(): + matcher = BrandMatcher(target_brand="华为") + text = "第一段介绍市场情况\n第二段提到华为的产品\n第三段是总结" + result = matcher.match(text) + assert result["cited"] is True + assert result["position"] == 2 + assert result["citation_text"] is not None + + +# --------------------------------------------------------------------------- +# 补充测试 +# --------------------------------------------------------------------------- +def test_brand_matcher_multiple_aliases(): + """多个别名时,应能匹配任意一个别名。""" + matcher = BrandMatcher(target_brand="华为", brand_aliases=["Huawei", "HW", "Honor"]) + # 匹配第二个别名 + result = matcher.match("HW released a new chip") + assert result["cited"] is True + assert result["match_type"] == "alias" + assert result["confidence"] == 0.9 + # 匹配第三个别名 + result2 = matcher.match("Honor phones are popular") + assert result2["cited"] is True + assert result2["match_type"] == "alias" + + +def test_brand_matcher_fuzzy_threshold_boundary(): + """模糊匹配阈值边界:ratio 恰好在 0.4 附近的情况。""" + matcher = BrandMatcher(target_brand="华为") + # "华伟" 与 "华为" 的 ratio 约为 0.5,应大于 0.4 + result = matcher.match("华伟 是一家科技公司") + assert result["cited"] is True + assert result["match_type"] == "fuzzy" + assert result["confidence"] > 0.4 + + # "苹果" vs "华为" ratio 很低,不应超过 0.4 + result2 = matcher.match("苹果科技公司") + assert result2["cited"] is False + assert result2["match_type"] is None + + +def test_brand_matcher_empty_text(): + """空字符串输入不应崩溃,应返回 cited=False。""" + matcher = BrandMatcher(target_brand="华为", brand_aliases=["Huawei"]) + result = matcher.match("") + assert result["cited"] is False + assert result["confidence"] == 0.0 + assert result["match_type"] is None + assert result["position"] is None + + +def test_competitor_detector_multi_industry(): + """竞品检测应能跨行业识别品牌。""" + detector = CompetitorDetector() + text = "华为和小米是科技公司,工商银行和招商银行是银行" + competitors_tech = detector.detect(text, target_brand="华为") + assert "小米" in competitors_tech + assert "腾讯" not in competitors_tech # 未在文本中出现 + assert "华为" not in competitors_tech + + competitors_finance = detector.detect(text, target_brand="工商银行") + assert "招商银行" in competitors_finance + assert "建设银行" not in competitors_finance # 未在文本中出现 + + +def test_citation_position_multiple_paragraphs(): + """品牌在不同段落位置出现时的 position 检测。""" + matcher = BrandMatcher(target_brand="华为") + + text_first = "华为位于第一段\n第二段没有\n第三段也没有" + result = matcher.match(text_first) + assert result["position"] == 1 + + text_third = "第一段没有\n第二段没有\n华为在第三段" + result = matcher.match(text_third) + assert result["position"] == 3 + + text_last = "第一段\n第二段\n第三段\n最后一段提到华为" + result = matcher.match(text_last) + assert result["position"] == 4 diff --git a/tests/test_citations.py b/tests/test_citations.py new file mode 100644 index 0000000..d50a03d --- /dev/null +++ b/tests/test_citations.py @@ -0,0 +1,92 @@ +import uuid +from datetime import datetime +from unittest.mock import AsyncMock, patch + +import pytest + + +@pytest.fixture +def mock_citation_record(): + """Return a mock citation record.""" + record = AsyncMock() + record.id = uuid.UUID("32345678-1234-1234-1234-123456789abc") + record.query_id = uuid.UUID("22345678-1234-1234-1234-123456789abc") + record.platform = "wenxin" + record.cited = True + record.citation_position = 1 + record.citation_text = "Test citation text" + record.competitor_brands = [] + record.queried_at = datetime.now() + return record + + +@pytest.mark.asyncio +async def test_get_citations( + async_client, override_get_current_user, auth_headers, mock_citation_record +): + with patch( + "app.api.citations.get_citations", + return_value=([mock_citation_record], 1), + ): + response = await async_client.get( + "/api/v1/citations/", + headers=auth_headers, + ) + assert response.status_code == 200 + data = response.json() + assert data["total"] == 1 + assert len(data["items"]) == 1 + assert data["items"][0]["platform"] == "wenxin" + assert data["items"][0]["cited"] is True + + +@pytest.mark.asyncio +async def test_get_citation_stats( + async_client, override_get_current_user, auth_headers +): + stats = { + "total_queries": 10, + "total_citations": 7, + "citation_rate": 0.7, + "avg_position": 2.5, + "by_platform": { + "wenxin": { + "queries": 5, + "citations": 4, + "rate": 0.8, + "avg_position": 2.0, + } + }, + "trend": [{"date": "2024-01-01", "citations": 3}], + } + with patch("app.api.citations.get_citation_stats", return_value=stats): + response = await async_client.get( + "/api/v1/citations/stats", + headers=auth_headers, + ) + assert response.status_code == 200 + data = response.json() + assert data["total_queries"] == 10 + assert data["total_citations"] == 7 + assert data["citation_rate"] == 0.7 + assert "wenxin" in data["by_platform"] + + +@pytest.mark.asyncio +async def test_export_csv( + async_client, override_get_current_user, auth_headers +): + csv_content = "日期,平台,是否引用,引用位置,引用文本,竞争品牌\n2024-01-01,wenxin,是,1,test,\n" + with patch( + "app.api.reports.export_citations_csv", + return_value=csv_content, + ): + response = await async_client.get( + "/api/v1/reports/export/csv?query_id=22345678-1234-1234-1234-123456789abc", + headers=auth_headers, + ) + assert response.status_code == 200 + assert response.headers["content-type"].startswith("text/csv") + assert "attachment" in response.headers["content-disposition"] + body = response.text + assert "wenxin" in body diff --git a/tests/test_queries.py b/tests/test_queries.py new file mode 100644 index 0000000..786d3e8 --- /dev/null +++ b/tests/test_queries.py @@ -0,0 +1,153 @@ +import uuid +from datetime import datetime +from unittest.mock import AsyncMock, patch + +import pytest + +from app.api.deps import get_current_user + + +@pytest.fixture +def mock_query(): + """Return a mock query object.""" + q = AsyncMock() + q.id = uuid.UUID("22345678-1234-1234-1234-123456789abc") + q.user_id = uuid.UUID("12345678-1234-1234-1234-123456789abc") + q.keyword = "test keyword" + q.target_brand = "TestBrand" + q.brand_aliases = [] + q.platforms = ["wenxin", "kimi"] + q.frequency = "weekly" + q.status = "active" + q.last_queried_at = None + q.next_query_at = datetime.now() + q.created_at = datetime.now() + q.updated_at = datetime.now() + return q + + +@pytest.mark.asyncio +async def test_create_query_success( + async_client, override_get_current_user, auth_headers, mock_query +): + with patch("app.api.queries.create_query", return_value=mock_query): + response = await async_client.post( + "/api/v1/queries/", + headers=auth_headers, + json={ + "keyword": "test keyword", + "target_brand": "TestBrand", + "platforms": ["wenxin", "kimi"], + "frequency": "weekly", + }, + ) + assert response.status_code == 201 + data = response.json() + assert data["keyword"] == "test keyword" + assert data["target_brand"] == "TestBrand" + + +@pytest.mark.asyncio +async def test_create_query_exceeds_limit( + async_client, override_get_current_user, auth_headers +): + with patch( + "app.api.queries.create_query", + side_effect=PermissionError("Query limit exceeded"), + ): + response = await async_client.post( + "/api/v1/queries/", + headers=auth_headers, + json={ + "keyword": "test keyword", + "target_brand": "TestBrand", + "platforms": ["wenxin"], + "frequency": "daily", + }, + ) + assert response.status_code == 403 + data = response.json() + assert "Query limit exceeded" in data["detail"] + + +@pytest.mark.asyncio +async def test_list_queries( + async_client, override_get_current_user, auth_headers, mock_query +): + with patch("app.api.queries.get_queries", return_value=([mock_query], 1)): + response = await async_client.get("/api/v1/queries/", headers=auth_headers) + assert response.status_code == 200 + data = response.json() + assert data["total"] == 1 + assert len(data["items"]) == 1 + assert data["items"][0]["keyword"] == "test keyword" + + +@pytest.mark.asyncio +async def test_update_query( + async_client, override_get_current_user, auth_headers, mock_query +): + updated_query = AsyncMock() + updated_query.id = mock_query.id + updated_query.keyword = "updated keyword" + updated_query.target_brand = "TestBrand" + updated_query.brand_aliases = [] + updated_query.platforms = ["wenxin"] + updated_query.frequency = "daily" + updated_query.status = "active" + updated_query.last_queried_at = None + updated_query.next_query_at = datetime.now() + updated_query.created_at = datetime.now() + updated_query.updated_at = datetime.now() + + with patch("app.api.queries.update_query", return_value=updated_query): + response = await async_client.put( + f"/api/v1/queries/{mock_query.id}", + headers=auth_headers, + json={"keyword": "updated keyword", "frequency": "daily"}, + ) + assert response.status_code == 200 + data = response.json() + assert data["keyword"] == "updated keyword" + assert data["frequency"] == "daily" + + +@pytest.mark.asyncio +async def test_delete_query( + async_client, override_get_current_user, auth_headers, mock_query +): + with patch("app.api.queries.delete_query", return_value=True): + response = await async_client.delete( + f"/api/v1/queries/{mock_query.id}", + headers=auth_headers, + ) + assert response.status_code == 204 + + +@pytest.mark.asyncio +async def test_query_not_found( + async_client, override_get_current_user, auth_headers +): + non_existent_id = uuid.UUID("33333333-3333-3333-3333-333333333333") + with patch("app.api.queries.get_query", return_value=None): + response = await async_client.get( + f"/api/v1/queries/{non_existent_id}", + headers=auth_headers, + ) + assert response.status_code == 404 + data = response.json() + assert "Query not found" in data["detail"] + + +@pytest.mark.asyncio +async def test_query_belongs_to_other_user( + async_client, override_get_current_user, auth_headers +): + other_user_query_id = uuid.UUID("44444444-4444-4444-4444-444444444444") + # Simulate that the query does not belong to the current user by returning None + with patch("app.api.queries.get_query", return_value=None): + response = await async_client.get( + f"/api/v1/queries/{other_user_query_id}", + headers=auth_headers, + ) + assert response.status_code == 404 diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py new file mode 100644 index 0000000..115365a --- /dev/null +++ b/tests/test_scheduler.py @@ -0,0 +1,122 @@ +import uuid +from datetime import datetime, timezone, timedelta +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from app.models.query import Query +from app.models.user import User +from app.services.auth import hash_password +from app.workers.citation_engine import CitationEngine +from app.workers.scheduler import QueryScheduler + + +# --------------------------------------------------------------------------- +# 1. 调度器启动 / 关闭 +# --------------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_scheduler_start_stop(): + scheduler = QueryScheduler() + scheduler.engine = AsyncMock() + + scheduler.start() + # Verify the scheduled job was added + job = scheduler.scheduler.get_job("check_queries") + assert job is not None + assert job.name == "检查并执行到期的查询任务" + + await scheduler.shutdown() + # Verify engine.close was awaited + scheduler.engine.close.assert_awaited_once() + + +# --------------------------------------------------------------------------- +# 2. 查询任务筛选:只选择 status=active 且 next_query_at <= now 的查询 +# --------------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_scheduler_query_filtering(test_session): + # Create a user first + user = User( + email="sched@test.com", + password_hash=hash_password("pass"), + name="Scheduler", + ) + test_session.add(user) + await test_session.commit() + await test_session.refresh(user) + + now = datetime.now(timezone.utc) + + # q1: active and overdue -> should be picked + q1 = Query( + user_id=user.id, + keyword="overdue", + target_brand="B1", + status="active", + next_query_at=now - timedelta(hours=1), + ) + # q2: active but in the future -> should NOT be picked + q2 = Query( + user_id=user.id, + keyword="future", + target_brand="B2", + status="active", + next_query_at=now + timedelta(days=1), + ) + # q3: paused and overdue -> should NOT be picked + q3 = Query( + user_id=user.id, + keyword="paused", + target_brand="B3", + status="paused", + next_query_at=now - timedelta(hours=1), + ) + + test_session.add_all([q1, q2, q3]) + await test_session.commit() + + scheduler = QueryScheduler() + scheduler.engine = AsyncMock() + + # Mock AsyncSessionLocal so scheduler uses our test session + mock_local = MagicMock() + mock_local.return_value.__aenter__ = AsyncMock(return_value=test_session) + mock_local.return_value.__aexit__ = AsyncMock(return_value=False) + + with patch("app.workers.scheduler.AsyncSessionLocal", mock_local): + await scheduler.check_and_execute_queries() + + # execute_query should be called exactly once (for q1) + scheduler.engine.execute_query.assert_called_once() + called_query = scheduler.engine.execute_query.call_args[0][0] + assert called_query.keyword == "overdue" + + +# --------------------------------------------------------------------------- +# 3. 频率计算:daily 和 weekly 的 next_query_at 正确计算 +# --------------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_scheduler_frequency_calculation_daily(): + engine = CitationEngine() + now = datetime.utcnow() + result = engine._calculate_next_query_at("daily") + expected = now + timedelta(days=1) + assert abs((result - expected).total_seconds()) < 5 + + +@pytest.mark.asyncio +async def test_scheduler_frequency_calculation_weekly(): + engine = CitationEngine() + now = datetime.utcnow() + result = engine._calculate_next_query_at("weekly") + expected = now + timedelta(days=7) + assert abs((result - expected).total_seconds()) < 5 + + +@pytest.mark.asyncio +async def test_scheduler_frequency_calculation_default(): + engine = CitationEngine() + now = datetime.utcnow() + result = engine._calculate_next_query_at(None) + expected = now + timedelta(days=7) + assert abs((result - expected).total_seconds()) < 5