From 0841b1a1030d864db73190c8ea0999918931c9ea Mon Sep 17 00:00:00 2001 From: Zhang Jing Xuan <17660634225@163.com> Date: Wed, 29 Jul 2026 16:29:10 +0800 Subject: [PATCH] =?UTF-8?q?=E6=99=BA=E6=95=99=E5=8A=A9=E6=89=8B=E5=B9=B3?= =?UTF-8?q?=E5=8F=B0=EF=BC=9A=E5=AE=8C=E6=95=B4=E5=88=9D=E5=A7=8B=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 前端:Vue3 + TS + Element Plus,24 个页面路由(课件/组题/教案/动画/思维导图/作文批改/命题/课堂/资源/社区等) - 后端:FastAPI + SQLAlchemy + SQLite,17 个路由模块,AI 服务层含降级模板 - AI:glm-5.x 推理模型已禁用思维链,确保输出真实内容 - 修复:ai_service 两处请求体注入 enable_thinking/thinking disabled - 测试账号:13900999999 / test1234 --- .gitignore | 53 + README.md | 118 + backend/.env.example | 10 + backend/Dockerfile | 14 + backend/alembic.ini | 40 + backend/alembic/env.py | 54 + backend/alembic/script.py.mako | 24 + .../versions/97e0aa47c003_initial_schema.py | 28 + .../a1b2c3d4e5f6_add_audit_logs_table.py | 44 + .../b2c3d4e5f6a7_add_mind_maps_table.py | 39 + .../c3d4e5f6a7b8_add_reset_codes_table.py | 37 + .../d4e5f6a7b8c9_add_notifications_table.py | 41 + backend/config.py | 86 + backend/database.py | 40 + backend/jiaoyu.db-shm | Bin 0 -> 32768 bytes backend/jiaoyu.db-wal | Bin 0 -> 2982912 bytes backend/main.py | 91 + backend/models/__init__.py | 28 + backend/models/animation.py | 24 + backend/models/audit.py | 21 + backend/models/chat.py | 39 + backend/models/classroom.py | 20 + backend/models/community.py | 54 + backend/models/courseware.py | 46 + backend/models/credit.py | 33 + backend/models/essay.py | 21 + backend/models/exam.py | 22 + backend/models/exercise.py | 44 + backend/models/lesson_plan.py | 25 + backend/models/material.py | 26 + backend/models/mindmap.py | 18 + backend/models/notification.py | 21 + backend/models/reset_code.py | 16 + backend/models/resource.py | 46 + backend/models/user.py | 36 + backend/requirements.txt | 22 + backend/routers/__init__.py | 0 backend/routers/admin.py | 236 + backend/routers/ai.py | 297 ++ backend/routers/animation.py | 188 + backend/routers/auth.py | 435 ++ backend/routers/chat.py | 184 + backend/routers/classroom.py | 513 +++ backend/routers/community.py | 210 + backend/routers/courseware.py | 317 ++ backend/routers/essay.py | 83 + backend/routers/exam.py | 198 + backend/routers/exercise.py | 248 ++ backend/routers/lesson_plan.py | 245 ++ backend/routers/material.py | 98 + backend/routers/mindmap.py | 108 + backend/routers/notification.py | 101 + backend/routers/resource.py | 267 ++ backend/routers/search.py | 301 ++ backend/schemas/__init__.py | 9 + backend/schemas/admin.py | 53 + backend/schemas/ai.py | 38 + backend/schemas/animation.py | 40 + backend/schemas/chat.py | 41 + backend/schemas/classroom.py | 71 + backend/schemas/community.py | 67 + backend/schemas/courseware.py | 69 + backend/schemas/essay.py | 27 + backend/schemas/exam.py | 46 + backend/schemas/exercise.py | 65 + backend/schemas/lesson_plan.py | 57 + backend/schemas/material.py | 44 + backend/schemas/mindmap.py | 32 + backend/schemas/notification.py | 16 + backend/schemas/resource.py | 63 + backend/schemas/search.py | 28 + backend/schemas/user.py | 130 + backend/services/__init__.py | 7 + backend/services/ai_service.py | 3190 ++++++++++++++ backend/services/audit.py | 96 + backend/services/auth.py | 87 + backend/services/credits.py | 103 + backend/services/limiter.py | 4 + backend/services/password_policy.py | 27 + backend/services/upload_security.py | 130 + docker-compose.yml | 57 + docs/api.md | 109 + docs/architecture.md | 202 + docs/platform-parity-roadmap.md | 83 + frontend/Dockerfile | 12 + frontend/_audit.cjs | 76 + frontend/_audit.mjs | 58 + frontend/_audit2.cjs | 73 + frontend/_audit3.cjs | 74 + frontend/_audit4.cjs | 66 + frontend/_audit5.cjs | 67 + frontend/_audit6.cjs | 55 + frontend/_br_test.cjs | 35 + frontend/_dbg.cjs | 22 + frontend/_debug_prob.cjs | 29 + frontend/_diag.mjs | 46 + frontend/_diag2.mjs | 15 + frontend/_final_tpl.cjs | 43 + frontend/_find_bad_games.cjs | 30 + frontend/_find_bad_tpl.cjs | 32 + frontend/_iso_prob.cjs | 36 + frontend/_race_test.cjs | 43 + frontend/_screens.cjs | 67 + frontend/_smoke.mjs | 34 + frontend/_test_idiom.cjs | 30 + frontend/_verify_404.cjs | 55 + frontend/_verify_games.cjs | 38 + frontend/_verify_tpl.cjs | 56 + frontend/_walk.cjs | 49 + frontend/_walk_views.cjs | 39 + frontend/index.html | 40 + frontend/nginx.conf | 21 + frontend/package-lock.json | 3874 +++++++++++++++++ frontend/package.json | 34 + frontend/public/debug.html | 107 + frontend/public/favicon.svg | 10 + frontend/public/test-content.html | 32 + frontend/public/test-iframe.html | 51 + frontend/public/test-js-iframe.html | 46 + frontend/public/test-render.html | 40 + frontend/shot.cjs | 17 + frontend/src/App.vue | 6 + frontend/src/api/admin.ts | 22 + frontend/src/api/ai.ts | 21 + frontend/src/api/animation.ts | 27 + frontend/src/api/chat.ts | 44 + frontend/src/api/classroom.ts | 28 + frontend/src/api/community.ts | 37 + frontend/src/api/courseware.ts | 37 + frontend/src/api/essay.ts | 13 + frontend/src/api/exam.ts | 22 + frontend/src/api/exercise.ts | 40 + frontend/src/api/index.ts | 39 + frontend/src/api/lessonPlan.ts | 25 + frontend/src/api/material.ts | 16 + frontend/src/api/mindmap.ts | 30 + frontend/src/api/notification.ts | 28 + frontend/src/api/resource.ts | 34 + frontend/src/api/search.ts | 4 + frontend/src/auto-imports.d.ts | 88 + frontend/src/components.d.ts | 47 + .../src/components/common/HtmlPreview.vue | 649 +++ .../src/components/common/MarkdownContent.vue | 128 + .../src/components/common/MaterialPicker.vue | 282 ++ frontend/src/components/common/MindNode.vue | 158 + .../src/components/common/TemplateGallery.vue | 306 ++ frontend/src/components/layout/AppHeader.vue | 387 ++ frontend/src/components/layout/AppLayout.vue | 92 + frontend/src/components/layout/AppSidebar.vue | 356 ++ frontend/src/env.d.ts | 7 + frontend/src/main.ts | 165 + frontend/src/router/index.ts | 99 + frontend/src/stores/user.ts | 71 + frontend/src/styles/global.css | 403 ++ frontend/src/utils/animationTemplates.ts | 3010 +++++++++++++ frontend/src/utils/coursewareThemes.ts | 131 + frontend/src/utils/demoDrafts.ts | 387 ++ frontend/src/utils/download.ts | 10 + frontend/src/utils/gameTemplates.ts | 1538 +++++++ frontend/src/utils/materialContext.ts | 59 + frontend/src/utils/request.ts | 97 + frontend/src/utils/speech.ts | 51 + frontend/src/views/Admin.vue | 447 ++ frontend/src/views/Animation.vue | 733 ++++ frontend/src/views/Assistant.vue | 585 +++ frontend/src/views/Classroom.vue | 2176 +++++++++ frontend/src/views/ClassroomJoin.vue | 529 +++ frontend/src/views/Community.vue | 493 +++ frontend/src/views/CommunityDetail.vue | 461 ++ frontend/src/views/CoursewareCreate.vue | 1130 +++++ frontend/src/views/CoursewareDetail.vue | 621 +++ frontend/src/views/CoursewareList.vue | 812 ++++ frontend/src/views/CoursewarePreview.vue | 1234 ++++++ frontend/src/views/DemoResource.vue | 886 ++++ frontend/src/views/EssayGrade.vue | 518 +++ frontend/src/views/Exam.vue | 647 +++ frontend/src/views/Exercise.vue | 873 ++++ frontend/src/views/Favorites.vue | 266 ++ frontend/src/views/Help.vue | 179 + frontend/src/views/Home.vue | 1601 +++++++ frontend/src/views/LessonPlan.vue | 650 +++ frontend/src/views/Login.vue | 631 +++ frontend/src/views/Materials.vue | 392 ++ frontend/src/views/MindMap.vue | 366 ++ frontend/src/views/NotFound.vue | 204 + frontend/src/views/Profile.vue | 1286 ++++++ frontend/src/views/ResourceDetail.vue | 1161 +++++ frontend/src/views/Resources.vue | 778 ++++ frontend/src/views/Search.vue | 428 ++ frontend/tsconfig.json | 21 + frontend/vite.config.ts | 46 + start.bat | 32 + start.sh | 62 + 193 files changed, 44995 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 backend/.env.example create mode 100644 backend/Dockerfile create mode 100644 backend/alembic.ini create mode 100644 backend/alembic/env.py create mode 100644 backend/alembic/script.py.mako create mode 100644 backend/alembic/versions/97e0aa47c003_initial_schema.py create mode 100644 backend/alembic/versions/a1b2c3d4e5f6_add_audit_logs_table.py create mode 100644 backend/alembic/versions/b2c3d4e5f6a7_add_mind_maps_table.py create mode 100644 backend/alembic/versions/c3d4e5f6a7b8_add_reset_codes_table.py create mode 100644 backend/alembic/versions/d4e5f6a7b8c9_add_notifications_table.py create mode 100644 backend/config.py create mode 100644 backend/database.py create mode 100644 backend/jiaoyu.db-shm create mode 100644 backend/jiaoyu.db-wal create mode 100644 backend/main.py create mode 100644 backend/models/__init__.py create mode 100644 backend/models/animation.py create mode 100644 backend/models/audit.py create mode 100644 backend/models/chat.py create mode 100644 backend/models/classroom.py create mode 100644 backend/models/community.py create mode 100644 backend/models/courseware.py create mode 100644 backend/models/credit.py create mode 100644 backend/models/essay.py create mode 100644 backend/models/exam.py create mode 100644 backend/models/exercise.py create mode 100644 backend/models/lesson_plan.py create mode 100644 backend/models/material.py create mode 100644 backend/models/mindmap.py create mode 100644 backend/models/notification.py create mode 100644 backend/models/reset_code.py create mode 100644 backend/models/resource.py create mode 100644 backend/models/user.py create mode 100644 backend/requirements.txt create mode 100644 backend/routers/__init__.py create mode 100644 backend/routers/admin.py create mode 100644 backend/routers/ai.py create mode 100644 backend/routers/animation.py create mode 100644 backend/routers/auth.py create mode 100644 backend/routers/chat.py create mode 100644 backend/routers/classroom.py create mode 100644 backend/routers/community.py create mode 100644 backend/routers/courseware.py create mode 100644 backend/routers/essay.py create mode 100644 backend/routers/exam.py create mode 100644 backend/routers/exercise.py create mode 100644 backend/routers/lesson_plan.py create mode 100644 backend/routers/material.py create mode 100644 backend/routers/mindmap.py create mode 100644 backend/routers/notification.py create mode 100644 backend/routers/resource.py create mode 100644 backend/routers/search.py create mode 100644 backend/schemas/__init__.py create mode 100644 backend/schemas/admin.py create mode 100644 backend/schemas/ai.py create mode 100644 backend/schemas/animation.py create mode 100644 backend/schemas/chat.py create mode 100644 backend/schemas/classroom.py create mode 100644 backend/schemas/community.py create mode 100644 backend/schemas/courseware.py create mode 100644 backend/schemas/essay.py create mode 100644 backend/schemas/exam.py create mode 100644 backend/schemas/exercise.py create mode 100644 backend/schemas/lesson_plan.py create mode 100644 backend/schemas/material.py create mode 100644 backend/schemas/mindmap.py create mode 100644 backend/schemas/notification.py create mode 100644 backend/schemas/resource.py create mode 100644 backend/schemas/search.py create mode 100644 backend/schemas/user.py create mode 100644 backend/services/__init__.py create mode 100644 backend/services/ai_service.py create mode 100644 backend/services/audit.py create mode 100644 backend/services/auth.py create mode 100644 backend/services/credits.py create mode 100644 backend/services/limiter.py create mode 100644 backend/services/password_policy.py create mode 100644 backend/services/upload_security.py create mode 100644 docker-compose.yml create mode 100644 docs/api.md create mode 100644 docs/architecture.md create mode 100644 docs/platform-parity-roadmap.md create mode 100644 frontend/Dockerfile create mode 100644 frontend/_audit.cjs create mode 100644 frontend/_audit.mjs create mode 100644 frontend/_audit2.cjs create mode 100644 frontend/_audit3.cjs create mode 100644 frontend/_audit4.cjs create mode 100644 frontend/_audit5.cjs create mode 100644 frontend/_audit6.cjs create mode 100644 frontend/_br_test.cjs create mode 100644 frontend/_dbg.cjs create mode 100644 frontend/_debug_prob.cjs create mode 100644 frontend/_diag.mjs create mode 100644 frontend/_diag2.mjs create mode 100644 frontend/_final_tpl.cjs create mode 100644 frontend/_find_bad_games.cjs create mode 100644 frontend/_find_bad_tpl.cjs create mode 100644 frontend/_iso_prob.cjs create mode 100644 frontend/_race_test.cjs create mode 100644 frontend/_screens.cjs create mode 100644 frontend/_smoke.mjs create mode 100644 frontend/_test_idiom.cjs create mode 100644 frontend/_verify_404.cjs create mode 100644 frontend/_verify_games.cjs create mode 100644 frontend/_verify_tpl.cjs create mode 100644 frontend/_walk.cjs create mode 100644 frontend/_walk_views.cjs create mode 100644 frontend/index.html create mode 100644 frontend/nginx.conf create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/public/debug.html create mode 100644 frontend/public/favicon.svg create mode 100644 frontend/public/test-content.html create mode 100644 frontend/public/test-iframe.html create mode 100644 frontend/public/test-js-iframe.html create mode 100644 frontend/public/test-render.html create mode 100644 frontend/shot.cjs create mode 100644 frontend/src/App.vue create mode 100644 frontend/src/api/admin.ts create mode 100644 frontend/src/api/ai.ts create mode 100644 frontend/src/api/animation.ts create mode 100644 frontend/src/api/chat.ts create mode 100644 frontend/src/api/classroom.ts create mode 100644 frontend/src/api/community.ts create mode 100644 frontend/src/api/courseware.ts create mode 100644 frontend/src/api/essay.ts create mode 100644 frontend/src/api/exam.ts create mode 100644 frontend/src/api/exercise.ts create mode 100644 frontend/src/api/index.ts create mode 100644 frontend/src/api/lessonPlan.ts create mode 100644 frontend/src/api/material.ts create mode 100644 frontend/src/api/mindmap.ts create mode 100644 frontend/src/api/notification.ts create mode 100644 frontend/src/api/resource.ts create mode 100644 frontend/src/api/search.ts create mode 100644 frontend/src/auto-imports.d.ts create mode 100644 frontend/src/components.d.ts create mode 100644 frontend/src/components/common/HtmlPreview.vue create mode 100644 frontend/src/components/common/MarkdownContent.vue create mode 100644 frontend/src/components/common/MaterialPicker.vue create mode 100644 frontend/src/components/common/MindNode.vue create mode 100644 frontend/src/components/common/TemplateGallery.vue create mode 100644 frontend/src/components/layout/AppHeader.vue create mode 100644 frontend/src/components/layout/AppLayout.vue create mode 100644 frontend/src/components/layout/AppSidebar.vue create mode 100644 frontend/src/env.d.ts create mode 100644 frontend/src/main.ts create mode 100644 frontend/src/router/index.ts create mode 100644 frontend/src/stores/user.ts create mode 100644 frontend/src/styles/global.css create mode 100644 frontend/src/utils/animationTemplates.ts create mode 100644 frontend/src/utils/coursewareThemes.ts create mode 100644 frontend/src/utils/demoDrafts.ts create mode 100644 frontend/src/utils/download.ts create mode 100644 frontend/src/utils/gameTemplates.ts create mode 100644 frontend/src/utils/materialContext.ts create mode 100644 frontend/src/utils/request.ts create mode 100644 frontend/src/utils/speech.ts create mode 100644 frontend/src/views/Admin.vue create mode 100644 frontend/src/views/Animation.vue create mode 100644 frontend/src/views/Assistant.vue create mode 100644 frontend/src/views/Classroom.vue create mode 100644 frontend/src/views/ClassroomJoin.vue create mode 100644 frontend/src/views/Community.vue create mode 100644 frontend/src/views/CommunityDetail.vue create mode 100644 frontend/src/views/CoursewareCreate.vue create mode 100644 frontend/src/views/CoursewareDetail.vue create mode 100644 frontend/src/views/CoursewareList.vue create mode 100644 frontend/src/views/CoursewarePreview.vue create mode 100644 frontend/src/views/DemoResource.vue create mode 100644 frontend/src/views/EssayGrade.vue create mode 100644 frontend/src/views/Exam.vue create mode 100644 frontend/src/views/Exercise.vue create mode 100644 frontend/src/views/Favorites.vue create mode 100644 frontend/src/views/Help.vue create mode 100644 frontend/src/views/Home.vue create mode 100644 frontend/src/views/LessonPlan.vue create mode 100644 frontend/src/views/Login.vue create mode 100644 frontend/src/views/Materials.vue create mode 100644 frontend/src/views/MindMap.vue create mode 100644 frontend/src/views/NotFound.vue create mode 100644 frontend/src/views/Profile.vue create mode 100644 frontend/src/views/ResourceDetail.vue create mode 100644 frontend/src/views/Resources.vue create mode 100644 frontend/src/views/Search.vue create mode 100644 frontend/tsconfig.json create mode 100644 frontend/vite.config.ts create mode 100644 start.bat create mode 100644 start.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0acdd24 --- /dev/null +++ b/.gitignore @@ -0,0 +1,53 @@ +# Dependencies +node_modules/ +frontend/node_modules/ + +# Build output +dist/ +build/ +frontend/dist/ + +# Python +__pycache__/ +*.pyc +*.pyo +*.egg-info/ +.venv/ +venv/ + +# Secrets & env (contains API keys) +.env +*.env.local + +# Database +*.db +*.sqlite +*.sqlite3 + +# Logs +*.log +backend_stdout.log +backend_stderr.log + +# Temp / debug / scratch files +_*.png +_*.txt +_*.json +_shots/ +.claude/ +debug_json_fail.txt +raw_ai.txt +_gen_dump.json +_cw_id.txt +test_frontend_api.py +test_output.json +*.bak + +# IDE +.idea/ +.vscode/ +*.swp + +# OS +.DS_Store +Thumbs.db diff --git a/README.md b/README.md new file mode 100644 index 0000000..39be815 --- /dev/null +++ b/README.md @@ -0,0 +1,118 @@ +# 智教助手 + +面向教师 AI 教学创作场景设计的智能体系统,帮助教师将教学想法快速转化为互动课件、教学动画、课堂工具和教案成果。 + +## 核心功能 + +| 模块 | 功能 | 说明 | +|------|------|------| +| AI课件生成 | 一句话生成互动课件 | 输入教学想法,AI自动生成交互式课件 | +| 教学动画 | 交互式教学动画生成 | 数学公式可视化、物理实验模拟等 | +| 互动练习 | 游戏化练习生成 | 贪吃蛇、消消乐等趣味单词/知识点练习 | +| 教案生成 | 智能教案辅助 | AI根据课程内容生成结构化教案 | +| AI命题 | 智能出题 | 根据知识点自动生成各类题型 | +| 作文批改 | AI作文评分与批注 | 多维度作文分析与改进建议 | +| 课堂工具 | 实时课堂互动 | 白板、倒计时、答题器、连线等 | +| 资源库 | 教学资源共享 | 课件、动画、教案等资源管理 | +| 教师社区 | 同行交流 | 资源分享、经验交流社区 | + +## 技术栈 + +### 后端 +- **框架**: Python 3.11 + FastAPI +- **数据库**: SQLite (开发) / PostgreSQL (生产) +- **ORM**: SQLAlchemy 2.0 + Alembic +- **缓存**: Redis +- **AI**: OpenAI API 兼容接口 +- **任务队列**: Celery + Redis + +### 前端 +- **框架**: Vue 3 + TypeScript + Vite +- **UI库**: Element Plus +- **状态管理**: Pinia +- **图表**: ECharts +- **动画**: Lottie + Canvas +- **编辑器**: TinyMCE / WangEditor + +### 部署 +- **容器化**: Docker + Docker Compose +- **Web服务器**: Nginx +- **CI/CD**: GitHub Actions + +## 项目结构 + +``` +jiaoyu/ +├── README.md +├── docs/ # 项目文档 +│ ├── architecture.md # 架构设计 +│ └── api.md # API文档 +├── backend/ # 后端服务 +│ ├── requirements.txt +│ ├── main.py # 应用入口 +│ ├── config.py # 配置管理 +│ ├── database.py # 数据库连接 +│ ├── models/ # SQLAlchemy模型 +│ ├── schemas/ # Pydantic模型 +│ ├── routers/ # API路由 +│ ├── services/ # 业务逻辑 +│ └── utils/ # 工具函数 +├── frontend/ # 前端应用 +│ ├── package.json +│ ├── vite.config.ts +│ ├── index.html +│ └── src/ +│ ├── main.ts +│ ├── App.vue +│ ├── router/ +│ ├── stores/ +│ ├── api/ +│ ├── views/ +│ ├── components/ +│ ├── styles/ +│ └── utils/ +└── docker-compose.yml +``` + +## 快速开始 + +### 后端启动 + +```bash +cd backend +python -m venv venv +source venv/bin/activate # Windows: venv\Scripts\activate +pip install -r requirements.txt +cp .env.example .env # 编辑配置 +python main.py +``` + +后端运行在 http://localhost:8000,API文档 http://localhost:8000/docs + +### 前端启动 + +```bash +cd frontend +npm install +npm run dev +``` + +前端运行在 http://localhost:5173 + +### Docker 部署 + +```bash +docker-compose up -d +``` + +## 环境变量 + +| 变量名 | 说明 | 默认值 | +|--------|------|--------| +| DATABASE_URL | 数据库连接字符串 | sqlite:///./jiaoyu.db | +| REDIS_URL | Redis连接 | redis://localhost:6379 | +| AI_API_KEY | AI服务API密钥 | - | +| AI_API_BASE | AI服务基础URL | https://api.openai.com/v1 | +| AI_MODEL | 默认AI模型 | gpt-4o | +| SECRET_KEY | JWT密钥 | - | +| CORS_ORIGINS | 允许的跨域来源 | http://localhost:5173 | diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..86a38e3 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,10 @@ +DATABASE_URL=sqlite:///./jiaoyu.db +REDIS_URL=redis://localhost:6379/0 + +AI_API_KEY=your-api-key-here +AI_API_BASE=https://api.openai.com/v1 +AI_MODEL=gpt-4o + +# 必填:JWT 签名密钥,生产环境请用 `python -c "import secrets;print(secrets.token_urlsafe(48))"` 生成 +SECRET_KEY=please-generate-a-random-secret-key-of-at-least-32-chars +CORS_ORIGINS=["http://localhost:5173"] diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..a00a059 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.11-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +RUN mkdir -p uploads + +EXPOSE 8000 + +CMD ["uvicorn", "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..89effd8 --- /dev/null +++ b/backend/alembic.ini @@ -0,0 +1,40 @@ +# Alembic migration configuration +[alembic] +script_location = alembic +prepend_sys_path = . +version_path_separator = os +sqlalchemy.url = sqlite:///./jiaoyu.db + +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/backend/alembic/env.py b/backend/alembic/env.py new file mode 100644 index 0000000..ca94963 --- /dev/null +++ b/backend/alembic/env.py @@ -0,0 +1,54 @@ +from logging.config import fileConfig + +from alembic import context +from sqlalchemy import engine_from_config, pool + +from config import get_settings +from database import Base +import models # noqa: F401 ensures all models are loaded + +config = context.config + +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +settings = get_settings() +config.set_main_option("sqlalchemy.url", settings.DATABASE_URL) + +target_metadata = Base.metadata + + +def run_migrations_offline() -> None: + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + compare_type=True, + ) + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + connectable = engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=target_metadata, + compare_type=True, + render_as_batch=True, + ) + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/backend/alembic/script.py.mako b/backend/alembic/script.py.mako new file mode 100644 index 0000000..590f5b3 --- /dev/null +++ b/backend/alembic/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/backend/alembic/versions/97e0aa47c003_initial_schema.py b/backend/alembic/versions/97e0aa47c003_initial_schema.py new file mode 100644 index 0000000..612e4a5 --- /dev/null +++ b/backend/alembic/versions/97e0aa47c003_initial_schema.py @@ -0,0 +1,28 @@ +"""initial schema + +Revision ID: 97e0aa47c003 +Revises: +Create Date: 2026-07-21 11:38:09.510081 +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +revision: str = '97e0aa47c003' +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/backend/alembic/versions/a1b2c3d4e5f6_add_audit_logs_table.py b/backend/alembic/versions/a1b2c3d4e5f6_add_audit_logs_table.py new file mode 100644 index 0000000..ac4d456 --- /dev/null +++ b/backend/alembic/versions/a1b2c3d4e5f6_add_audit_logs_table.py @@ -0,0 +1,44 @@ +"""add audit logs table + +Revision ID: a1b2c3d4e5f6 +Revises: 97e0aa47c003 +Create Date: 2026-07-21 12:00:00.000000 +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +revision: str = 'a1b2c3d4e5f6' +down_revision: Union[str, None] = '97e0aa47c003' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + 'audit_logs', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('username', sa.String(length=100), nullable=False, server_default=''), + sa.Column('action', sa.String(length=64), nullable=False), + sa.Column('target_type', sa.String(length=32), nullable=False, server_default=''), + sa.Column('target_id', sa.String(length=64), nullable=False, server_default=''), + sa.Column('detail', sa.Text(), nullable=False, server_default=''), + sa.Column('ip', sa.String(length=64), nullable=False, server_default=''), + sa.Column('user_agent', sa.String(length=255), nullable=False, server_default=''), + sa.Column('status', sa.String(length=16), nullable=False, server_default='success'), + sa.Column('created_at', sa.DateTime(), server_default=sa.func.now(), nullable=True), + sa.PrimaryKeyConstraint('id'), + ) + op.create_index('ix_audit_logs_id', 'audit_logs', ['id']) + op.create_index('ix_audit_logs_user_id', 'audit_logs', ['user_id']) + op.create_index('ix_audit_logs_action', 'audit_logs', ['action']) + + +def downgrade() -> None: + op.drop_index('ix_audit_logs_action', table_name='audit_logs') + op.drop_index('ix_audit_logs_user_id', table_name='audit_logs') + op.drop_index('ix_audit_logs_id', table_name='audit_logs') + op.drop_table('audit_logs') diff --git a/backend/alembic/versions/b2c3d4e5f6a7_add_mind_maps_table.py b/backend/alembic/versions/b2c3d4e5f6a7_add_mind_maps_table.py new file mode 100644 index 0000000..19a2dd8 --- /dev/null +++ b/backend/alembic/versions/b2c3d4e5f6a7_add_mind_maps_table.py @@ -0,0 +1,39 @@ +"""add mind maps table + +Revision ID: b2c3d4e5f6a7 +Revises: a1b2c3d4e5f6 +Create Date: 2026-07-22 09:00:00.000000 +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +revision: str = 'b2c3d4e5f6a7' +down_revision: Union[str, None] = 'a1b2c3d4e5f6' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + 'mind_maps', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('title', sa.String(length=200), nullable=False), + sa.Column('subject', sa.String(length=50), nullable=False, server_default='综合'), + sa.Column('nodes', sa.JSON(), nullable=True), + sa.Column('created_at', sa.DateTime(), server_default=sa.func.now(), nullable=True), + sa.Column('updated_at', sa.DateTime(), server_default=sa.func.now(), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['users.id']), + sa.PrimaryKeyConstraint('id'), + ) + op.create_index('ix_mind_maps_id', 'mind_maps', ['id']) + op.create_index('ix_mind_maps_user_id', 'mind_maps', ['user_id']) + + +def downgrade() -> None: + op.drop_index('ix_mind_maps_user_id', table_name='mind_maps') + op.drop_index('ix_mind_maps_id', table_name='mind_maps') + op.drop_table('mind_maps') diff --git a/backend/alembic/versions/c3d4e5f6a7b8_add_reset_codes_table.py b/backend/alembic/versions/c3d4e5f6a7b8_add_reset_codes_table.py new file mode 100644 index 0000000..99a7805 --- /dev/null +++ b/backend/alembic/versions/c3d4e5f6a7b8_add_reset_codes_table.py @@ -0,0 +1,37 @@ +"""add reset codes table + +Revision ID: c3d4e5f6a7b8 +Revises: b2c3d4e5f6a7 +Create Date: 2026-07-22 10:00:00.000000 +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +revision: str = 'c3d4e5f6a7b8' +down_revision: Union[str, None] = 'b2c3d4e5f6a7' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + 'reset_codes', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('phone', sa.String(length=20), nullable=False), + sa.Column('code', sa.String(length=6), nullable=False), + sa.Column('consumed', sa.Integer(), nullable=False, server_default='0'), + sa.Column('expires_at', sa.DateTime(), nullable=False), + sa.Column('created_at', sa.DateTime(), server_default=sa.func.now(), nullable=True), + sa.PrimaryKeyConstraint('id'), + ) + op.create_index('ix_reset_codes_id', 'reset_codes', ['id']) + op.create_index('ix_reset_codes_phone', 'reset_codes', ['phone']) + + +def downgrade() -> None: + op.drop_index('ix_reset_codes_phone', table_name='reset_codes') + op.drop_index('ix_reset_codes_id', table_name='reset_codes') + op.drop_table('reset_codes') diff --git a/backend/alembic/versions/d4e5f6a7b8c9_add_notifications_table.py b/backend/alembic/versions/d4e5f6a7b8c9_add_notifications_table.py new file mode 100644 index 0000000..3407632 --- /dev/null +++ b/backend/alembic/versions/d4e5f6a7b8c9_add_notifications_table.py @@ -0,0 +1,41 @@ +"""add notifications table + +Revision ID: d4e5f6a7b8c9 +Revises: c3d4e5f6a7b8 +Create Date: 2026-07-22 11:00:00.000000 +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +revision: str = 'd4e5f6a7b8c9' +down_revision: Union[str, None] = 'c3d4e5f6a7b8' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + 'notifications', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('actor_id', sa.Integer(), nullable=True), + sa.Column('ntype', sa.String(length=32), nullable=False, server_default='comment'), + sa.Column('title', sa.String(length=200), nullable=False), + sa.Column('content', sa.String(length=500), nullable=False, server_default=''), + sa.Column('link', sa.String(length=255), nullable=False, server_default=''), + sa.Column('is_read', sa.Boolean(), nullable=False, server_default=sa.text('0')), + sa.Column('created_at', sa.DateTime(), server_default=sa.func.now(), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['users.id']), + sa.PrimaryKeyConstraint('id'), + ) + op.create_index('ix_notifications_id', 'notifications', ['id']) + op.create_index('ix_notifications_user_id', 'notifications', ['user_id']) + + +def downgrade() -> None: + op.drop_index('ix_notifications_user_id', table_name='notifications') + op.drop_index('ix_notifications_id', table_name='notifications') + op.drop_table('notifications') diff --git a/backend/config.py b/backend/config.py new file mode 100644 index 0000000..9d40d8c --- /dev/null +++ b/backend/config.py @@ -0,0 +1,86 @@ +import os +import logging +import secrets as _secrets +from pydantic_settings import BaseSettings +from pydantic import field_validator +from functools import lru_cache + + +logger = logging.getLogger(__name__) + +# 已知的弱/默认密钥,实例化时会拒绝(防止源码泄露后被伪造 JWT) +_INSECURE_DEFAULTS = { + "", + "change-me-in-production", + "changeme", + "secret", + "your-secret-key", +} + + +class Settings(BaseSettings): + APP_NAME: str = "智教助手" + APP_VERSION: str = "1.0.0" + DEBUG: bool = True + + DATABASE_URL: str = "sqlite:///./jiaoyu.db" + REDIS_URL: str = "redis://localhost:6379/0" + + SECRET_KEY: str = "change-me-in-production" + ALGORITHM: str = "HS256" + ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 + REFRESH_TOKEN_EXPIRE_DAYS: int = 7 + + AI_API_KEY: str = "" + AI_API_BASE: str = "https://api.openai.com/v1" + AI_MODEL: str = "gpt-4o" + + CORS_ORIGINS: list[str] = ["http://localhost:5173"] + + UPLOAD_DIR: str = "uploads" + MAX_UPLOAD_SIZE: int = 50 * 1024 * 1024 + MAX_REQUEST_BODY_SIZE: int = 60 * 1024 * 1024 # global request body limit + + @field_validator("SECRET_KEY") + @classmethod + def _validate_secret_key(cls, v: str) -> str: + """在 .env 加载后执行:弱默认值在开发模式生成临时密钥,生产模式拒绝启动。""" + raw = (v or "").strip() + debug_env = str(os.getenv("DEBUG", "")).lower() + # DEBUG 字段可能已被 pydantic 解析;优先看环境变量原始值,回退看已解析的 DEBUG + is_debug = debug_env in ("", "1", "true", "yes", "on") if debug_env else True + # 若环境变量显式设了 DEBUG,用它;否则用 pydantic 解析后的值(默认 True) + if debug_env: + is_debug = debug_env in ("1", "true", "yes", "on") + + if raw in _INSECURE_DEFAULTS: + if is_debug: + generated = _secrets.token_urlsafe(48) + logger.warning( + "SECRET_KEY 未配置或为弱默认值,已生成临时开发密钥(仅本次运行有效)。" + "生产环境请在 .env 中设置 SECRET_KEY 为至少 32 字符的随机字符串。" + ) + return generated + raise RuntimeError( + "SECRET_KEY 未配置或为弱默认值,生产环境拒绝启动。" + "请在 .env 中设置 SECRET_KEY 为至少 32 字符的随机字符串。" + ) + if len(raw) < 32: + if is_debug: + logger.warning( + "SECRET_KEY 长度不足 32 字符(当前 %d),建议使用更长的随机字符串。", len(raw) + ) + return raw + raise RuntimeError( + f"SECRET_KEY 长度不足 32 字符(当前 {len(raw)}),生产环境拒绝启动。" + ) + return raw + + class Config: + env_file = ".env" + extra = "ignore" + + +@lru_cache() +def get_settings() -> Settings: + return Settings() diff --git a/backend/database.py b/backend/database.py new file mode 100644 index 0000000..580223a --- /dev/null +++ b/backend/database.py @@ -0,0 +1,40 @@ +from sqlalchemy import create_engine, event +from sqlalchemy.orm import sessionmaker, DeclarativeBase +from config import get_settings + +settings = get_settings() + +_is_sqlite = "sqlite" in settings.DATABASE_URL + +engine = create_engine( + settings.DATABASE_URL, + connect_args={"check_same_thread": False} if _is_sqlite else {}, + echo=settings.DEBUG, +) + +# SQLite concurrency hardening: enable WAL mode + busy_timeout on every connection. +# WAL allows concurrent readers alongside a single writer, eliminating most +# "database is locked" errors under FastAPI's threaded request handling. +if _is_sqlite: + @event.listens_for(engine, "connect") + def _set_sqlite_pragma(dbapi_conn, conn_record): + cur = dbapi_conn.cursor() + cur.execute("PRAGMA journal_mode=WAL") + cur.execute("PRAGMA synchronous=NORMAL") + cur.execute("PRAGMA busy_timeout=10000") + cur.execute("PRAGMA foreign_keys=ON") + cur.close() + +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + + +class Base(DeclarativeBase): + pass + + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/backend/jiaoyu.db-shm b/backend/jiaoyu.db-shm new file mode 100644 index 0000000000000000000000000000000000000000..269cd3f406c5f1e226bf772dc51f43f014e35d47 GIT binary patch literal 32768 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 literal 0 HcmV?d00001 diff --git a/backend/jiaoyu.db-wal b/backend/jiaoyu.db-wal new file mode 100644 index 0000000000000000000000000000000000000000..dffc61f8e801f4d37d0191c37cd74cd1b620c3ec GIT binary patch literal 2982912 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࢞Obf#WnWdugs1 zrV;nl}bYRdMtdc7AT5YaOuGyR&y~7@TkE<2Wl=JcuC3VA)_=o14fS+F>u(Z zQdB!~RLRgQ@ga4!?qmU@FB_qabE+04^&kuAJms~Mo;NVM#p}L&5!Ohi-7ZlfcIACI@pYkJvu7JN9L$!nzI_sPfWSkxfy9i^Aht|%FN+RX2*^l6>}7L zPT~e!u(G+>cEBmk!(W(c$qfADwyg*iT~)AN9DThon2EX>SKpnoFU$*{n{Rb;)5&EjDt6s$M1DkNzyBQ)t;$7|0MCSKgU!Wp_0D_J<~jX zmp6c4gCehvokU_w+YA!7h3V5xiC=@WJG4D=+C++bsbmvJtp06gO+L++M zo$&b(|9lE~ftybHd11D>f)_?UzdjoQkH9r~!Ywy}?@5e9!;T4u){JruL2 z;=zwDS?(oE)or&iJ`Y0AB#mT;R6HW{GJPyQuuS9 zia%3f8ep0hJ5?gB=<5RWczTRT$>^BdQ4xqtJz=lfN?(l!v#D)jMh_c2e00>JD{8+e zf%v%GnHmNDyq(<@PdU6RE>dLE3|F0Gf?;9NUx~8CQmr!(fOosQw9*-2h^#TWee*LMH95p@0A6I5)^69Up z9p>+aJcYzw0Qn^TvL|@t7jt4SK>P@#->MSlb7;SRh90?x3o#N4X~ku4>H6fRHlXvd#^4X{5nG&Kxm!EH0j0 z=e=IoYDnvPYvTAx2y8ia5qzc{b8Hlqc-40L9zSu4)#MCTt6dw%$_JjzP#SDv5v5fDED zPtb|Jilb(dv)0@0X^j0SEJor-K>P?IFV+T`;Uua>PpoKKj8dG%hfIrmRYs0U@U(63 z9Q~ilM*vIy9comd#ec+JU~td-?|SUJ*Rqfwfhwgjkskrto%|65eq#hko0f&#O#z+#p3ZbD0Oaf2-La->#RI7EA4 zMbtlIiUc*xm?Gz|(=LHgqo>^(miDeWbN~g3F941VL@$u9C3=C#?M(Cn<~;518eahU z1Uwee3k;oi%fCCA@9V@urGh<=g8#`ML4Y7Y5FiK;1PB5I0fGQQfFM8+_#Z`JmXwWb zZU+)uP40~!`Sps}O6wpOTXbx-L}pwQ>#S~z2`q?pX1845II+$Qhfl04Fqe6x&{GkM zhZ7q}#^A3z<*4s)bK{kNL@)3nefwRFGq?Xu%|Az}_$Mjw`v00g#y9bY`9u7h{44yE zP)Ys>0t5kq06~BtKoB4Z5CjMU1Ob8oL4Y7Y5coqNke;eg&@}%Om7!K6UZp~%Qi;mU zOjKrMpfWujm1$|HOie{4#1RzY1tLEJ(O%$#T_gYRdi8_rkRO3E?J)5p_(P2H|6(VI z9|7?rh=~mw(S{N~0+Np)IeU4Mk3jY?5#ufpKLX-MAoB`86+a3RUx36HAn^qvKjxld zhy{a9t0jH}5ibJdEEMe$gz8Cr0e!S<0Zv12FpXBxl%gPu?a-m4!C-IQx<9<3v3bXW z)~(A+Tj$&tp1&o$a0PP3YJeP8ZojiUD5g6!YqSQN!Dut+6I8*9Q*;*f zzuS?I0O=EuZh_<@Ao&PT(0-DS0J2+0Y)5UW0N{g`WIC7>?gjM-^$xDt1Jd<86>>-6c}TDmrUhW>%=-snNObhn`@B||AjW)+1dBZo#?ch*fF)}U$A6>r}IO5$2yRaJP_mhi(fgx!R;@REH^J0FyX?SdI#(VCGz zTe%KOqs|30!wo8ANibAEJS1?2EASYAcWPl;nLJK2nl{l&eVAb3>h($IyO9!$4yTbKans(mTw0k%7Jp9DWmOblRAKG~I z_FDzD_|f|oB4xOsAQ#%2AD+`ZW6y7x8vri8%nWc?OuF(C`K^Vs4@B;ys2`4!j0Q-o z8)T0@V%=~$vj>nUi6lzmY4~;DXtbCM%qM&QJA7sGr(+}zWP%4|(m@(c1txgso$T}j zAhTY5YzI!52PEkZkSK}7y5X;F-5`rE@NCgF53S1^U84rq1SS6pi7x>95%Nb6AP5iy z2m%BFf&f8)AV3fx2oMAa0tA6SDgr1{z-fptAm%BYc;-Df^fH*%wOudrF?cN6Uf_?4 z1L+q*fFM8+_+1eo_5#FSK(f3c_5wA5sUAN#@OM8=b`&HU0*Qt|q9J%(!BW9d58Yb$ zNBBf}+)GGSMPe^7a0<5Hi$x#2oMAa0t5kq06~BtKoB4Z5Cnc-1W2C3hF|tIC3y<}{CNs5NB3DI zPvMPUT>7sJ{jjZSsQbHWF~$EBm#OeM$W^$T-_Ae8Z{aubYxx!YVtzhuph8*v#+r)um{+^>|^XU_CEG5_6~LxyOdqX&Sqz@ z*Rw&khOK1F*oo{I_Dc3L_F}d_Yi0FpANC?v!=A^U&2p@grI;U?ubEcn6XpZ5 zlG({@W416Gm{rVTW-c>>na0#ImCUuwIOa-b5MyVojF!n^G)yPv3FybSL@@T1ivt zAJt#0Th*Va52+8TUr;}(-l^WE-lE>1UZq~Fo~xdro~Eu#$euO|4M9{CHxM^KCQTC>Ep1Iw0TFFLN?8lY zzJqKkwCt3Y!i?k0=!`mxGj22EjM}8Bv#9fZvweR5Z~o_;o4l8#MPNo|^!sih=iJ=) z-d%q8+~u9zbB|tgrKYi%q z;0SO8I0762j=)zMf!r+cAN2>}55#{j{GRylgx?YWt?*mo9}6E7?+`kO$AlR1cA=ek zREQFf2od6KLL2dqgpY`SD11o#1K|VWzY%^z{MW*-iN7zrPyAQHuZaIr_$BdQ2)`iy zp70*=cZGL}zazXu{B7ZF;%^CW5&yaHbK*Y}en$LF;Z5Q{6@E(mC&EvN|5*4j@gE64 zBL0T(2JzR0*NML-yhi+o!Vii6K==XiSA|!JzaqRs{QJW9iGNS{9`WxA-zEOC@G|l5 z2;U+8lJFAoZwuch{w?8K#Lo(6iJuY95N{P)iJulu6K@e(h&Kz(#7_yQh&KsM#7_z* ziJuTo5I-&)Cw@#gM*OI7l=uSJ+E@kFbaMZecg^=Y;2o?-F(q-zn@QzC+kSe7mrn_%>l1 z@vXvE;#-6*#5W6@iEk1%5#K0mB)&n|KzzNhp7=Up9r3loTHA4o+Z9oSWSGD zu!{IfVI}c;p`Q2(VFmH!!gAuvgk{8+3QLJE5ta~NEG#BoC)5#NBrGCcE7THSC@duY zjPMNc1;PU2^M(1u!$O#NjZlO8v7de#=~GW3oi`8ZlTRX@I~VB_PavH$2kGqDNN3GL z`uO8WtE-WQLP&!_q=5j^#~wpkRfV*&5@|&R(wQ@nmX{-a^iiabJc88kM_N{fbjA#% zJ|9xA7inoJQjZ5|NeNQ78|gQ{iFEpOqz^xgblNneQ>P-GG6m^F44XVLZ@(SsZMPvEKOX6= zw<5je7Nj@djC9;Mq&MA!bnIB9E*H`~1V5EZv zAssjn>Gjtm9WVfC|Ncn(^+Q@vfV6L4q6KR^?bQqEH@<in_s?#CWYLQ;NpTexH z{Ct=UC*Xvb&;^og8W;tS@$b=C;kp-2u_PX5s#CeAR_--eph}| zeqH{){B8M!d`Nyl-YsvFH^{5yWpb@tBR?UBSvT0b(Dq zw|KcIiaEN!gKpq6-N(9*bZ_Zi)t%NI*EQ(&fo5Q%ZnbWy?it-Y-Q&7S&<(hC59uc9 zCg{fLM(gaL9T==D&|R&&LMQ5Sw0{Tvz-QWzwI6AJsr|Y34ej?qLvTuaMEjz4w|1*` zowgoy1YzwH+Mu>v>(xH2y&tp$9cJthV~vb8Fm{l!1B|`M*b9t3 z&)9y(_A$1Xu|16KX6!k}b}_b-u^o(UXKWi|TN&HJ*k;ByF}9Jh4UDa4Y#n238C%2H zvy81~Y!zcG8LMY(1!Kz@TgKSZ>@0AXwuF6IOke8QmqqlYmVH^s*fWeRU~E2PVa7-~ z;3DOKiaG-NIBpl<$#Nn11?eyxJWtRBISUKlmjkO4!B4; z;3DOKiPSOlGNi*Oi&480M15VNmI7u_$ zOwtTENi*Oi&480M15VNmI7u_$B+Y=6Gy_i33^++M;3Um}lQaWP(hN9BGvFl6fRi)> zPSOlGNi*Oi&480M15VNmIEP~S$B<@V3~2_&kY->EX$HoSW?&3y2F8$PU<_#n#*k)U z3~2_&kY>P1ngJ(i2ArfBaFS-gNtyvCX$G968E}$jz)6|`Cus(pq#1CMX240B0Vin& zoTM3Wl4ih3nt@`{3>1sl)T9^~t;OPx))37nnnyI3Xb#bA6qs%p9F;{hGp-l711Ft? zUSPuhmFp(ld-#u}7s##=6Fwc?U6Q1tE5HJJSiyorRmaS>2_(1WS53Z z{iNQKEM<$Ii=T<@;;+Ov#aH2t|G4;~xD(#@mx~MFUB6QF!h8Nb;w|uwZx#o``+ZMQ zD`x2aqWe9(*T1HF34ZZ!(CyZ3g5UdV;eFn#o1(i%cZ+V6uCK0_PK3AjcI~gUZ)#uF zp4A?Q_x7FI_1fjy1==~OHkuZHDGAn%`@FuK9uHtmc^J zdCfL>H?P**uPM@ufVc1~HKHan|1bH!%l{z%t^6Ot@Ab#?U&vpX|9Jk?{Co24`Pb%W z<^479_jw=Yy`A@3-na8k&mFr#7cNXKc<@IdV?7?7wCIA^W53cd}p4ekuE8_JQo3+3T{G zX4l9;-B9i4u(zN@bCdkB{3`6|xC?eb%=vdMHh8`q0gk{IBcRFZm04an!(So%K@I+1 z4gO9I{#FfstOh&OU`!3RtHG!mjHtmjHTaPl{7?;kpay>ervC$-J3*<)^7%f0Fw3$Eja` zl=}6J)UU6netk{N^M|Qdevtb0)zq)Aq<;N=>euh3e*JFh*Oybjen&0BOKR}jYVccX z@T?jX9Wt5=>&y|PQavNQF{4)w}*HMlMHrmgCgEvZ*F zt5-IqUfHN#*`Nm3tHE_@aIG3#qXwT4y~8eFCZmtK}NFf-uw z1-!ws>7mL;y%oX|_3GlzSL@WPi#lJeRj)2ogU_hJ1!{1<8Vsw!n*6L@88oOqEoSw} zP)4q&l=yi{=t(6sR|!3#gytxr*-B`Z5_((-RV$&85(+AzfD(F42~{bfN+ndGgk~zC zawYVr5_&`l`IS(a5}Kied`ifxgi4i=r$^SnjFe_8QLed_&^MLP^iKCbtX!L>gr+K? zDN5)eCG?;YdO(+TE#yAK9}IZ|_bbu&DWSNeN92bLNc$23YPKjn@*B|cXP(#TzfIdC1HoJ`GB8%B#F&ZsJ^m
j?gyWgm9v8SJMYySp>c1}Lxjz8gMPNnGFU&Rs;b<3zqjOee`uuDY#(8^=?C3@ z>%DhO7^W}xKkC)r;`KaQIaq(I*Iil;!Z5ZhP&v~((r85~fq(i*Zl62gk3ZU_#28Hu zqX}wL_P?Swh9Za20DV@*ew)w>*iO#+#f^_mTZ4Lm-r$Wu4!|$?2C*LY($9c>@FC5A zX_kS_`l!5x@OyR7oY~p2Y-_idx)o&|$b32T{)`joz~FzwU)bFHav`Jd__1J0a7XVx z`5Afn;7Z%&sjLnJy|df_Z=|j^y7o|HdvkQlf#}}(k+a8{6YbdXXQIt3BMT2kP91A+ z-iP-b+{+^V_lN_G27;<<{C03lA zcPe3W0h1k(Ta*GqnavI}jEeS0dqMen4d@N!8`$G=C|};SX0xR-ROSsN4)+|M2oz5# zC@w~PAfjLz#kvEYG618LrOYxjJFI4h#n3s*81{aH+R#0)pXh})WM~*wG+0~38uvyQ zENp998f$F@f9TOQC&1f#Tg!>a!UK`hHSzYWZ*1RuEV^P3yG?(8WuR0aYdF$&YVk)k z^VJk#d>C03@ILPM&KhYnA#jByB_?#|ZYg3-U07#p*r7)ypdG z3D}cFkb!P+z$9mmw{S{SIOhs1%wX#PQS74>W`LT}q>wul)KByVp)0|pOy4|ZFgEMq zhb4+r?{ySSDWE&fRS+1%tc4C^Q4%VNsK+kHa`f&C<>;-HqraNA3IYqMR~&_eKwRX~ z>yGK$K$O$6#Hg_0GRU)^z3)i7I1;;GULJJ60-S9sE2dYKyDQpHuZx{o638a`p4bn{!(|yUs;j3+hLnhpj=OgbQgUV%VTs{Y-#0z zD0}#-?o^(;pgcoac>uybi5%LOoQT$hqbv4D8c(zGw6EFU*1Q7xZLBfe*19F~Y&dfA zNThyMq-g=WIq|5&%4DfJI6MSmrxdS?_B9tdOlj>)QQ=`S6t3skaWH<3qxQw&^%`s( zrQ^0WEs4}Fi*DK8)^w1~2zXgfpm{*VXZmNSAm^UxFhhe@hbo7wfMV0qt%*`9$#)tT z080}LfoW|^QQ^@NisPtB15=8dBxI2-we6dpi8btwHNtF$vs0|$OsrvN(ktAppAYi$7Cm_`VPa${4ENAt(XYj#4^@WEtCWlrqoxvZ{-T0-(^G6J|ALVm zrW9k^m_SkG8YoiF!9AhZUE8xqe2D6e-D-m0=k?6-lza8#;2Xx^2RjtPF22Vm)XNi%E3C{-g_u4T@Y?7?KZ0gkzlteMe1URH&`8p)9ip zP2u0573$pq|KpIY{%&tzrau^jiBDO^RIy9x(=|>supXqxrWC{jUsxQWYPGGJ8r zx^7UczGJKd`cf;wS{A11p6Z(AX7}Tj0Y4P(W`8-ZCdMi+D!#-OAG?BOQlE2Jl=Uhg z$0o*e_nJanZKcjNEXp`93(ME97nF}yTlrYNE7W`v<3?h2uU>JOl@icMkr*I4FZJOq z$7*#LlbbCbRTy?=V*cjw#RJ7+)@xyAzFf^EzGxX<>MgCV@(wTYhTKeg*!Ts@nV=WP z33mw$CU{kS)PblBH`)wlOOe4|q?8t;rxd97;rLd~1hZfo4D@9)Y@H{&LI=DGrA`|0 zsIWEzt8n$zBZrEEm67){HGKx5D~_ZZk1Iuk!KjprePx60dB$AG+6b%JU$j30c;tV>U)Yl3 zjv(XT=Z@fF{1KSAKLR{+1J0I37X{oOLBe+iJHk|TIFL9TwwU`P=n4w%kAV9l;Qk2W z9yHE%-hA2_?B_cipZg;)U!$Dy&ixS-cRr~b{AzH21l%71dU)af2$K9ZaDM~}7~CI0 zaq+*-A3+a%kx$qW44-k^D|fu!N_v4@%?E;Z*&eB1-m$kX za;7e_rxn~Am->UAYVvo74vj6|LJv4FE-@Lwb+OS`n(W8gYBIuii`Nv7P588)GOg1Q zN+;E@F=)FvN_SDKGIo<~EZCDj+Q5S_yoQ-Zu@oCNca9hH6 zWJ3!ThX?e1Fy2B=rG=))7QF!YMwYE(9%^G}H%2!!wl&p6x9(If#qX}$9BFBWqOyjK z)GmxPoEnzw-j=-Du8S<$9$V14S!|`267)7*YDJ%K5Hfn*P!p585UAN`E0~tl2;`94 z2wr$iO1r30;f!w32)B&tH9TG&2e^riZa*AdxuyNsE`!xnXfm0=8=gM8e`j>-*~miG zYk2$aGm&M@5%L-yECVm;>8`^QWneC&Ee?~-VMy^J9ghkZX91?8!r8t2APFe-_O_-M z+M1iet1ifQd&_q4Qp@sBMkt;?dH)6BMpxeJ>jCh{%Du6(n95Wu`OTAX0@Pn0#Vxk5{EOF97`S2a1>}8n%;mCF4uzNt zj)<9C?&<#00*BsY1W=Ty(3~nSM5cK`1h1SSZ$)T&!0SuBnB-&}>ls3i*LVjji(1-( zdDF6zG40yjxXod*!k~Tye3bnHk1hQ}L+#an_Msg?uKZ5?+(G$c@K^XgeC2-}0geDi zfFr;W;0SO8I0762jsQo1Bft^h2z(6?K!btYT%T?cBkX{Q}FM{OsiX z*8jX2?FjgOfv-VB^CEHtI0762jsQo1Bft^h2wWHfsrv=+*Fe5sAlAV53#4!3;QIw) zjbXlD;Q!!$fnGR}ChQ2NJQ00+{`LR)5b6c84T0OT@`Q|x8A6^p*VVCgTcoKe(y$H8GSaM#z`Q8>%=Xx!UCcD; zlha|v_@q#8D0G<34rAfnnV41((#ml~>Kofy_CP}LKy35U_M>Yd#kMm`!R#q=dM#LU zV2W$&qT9AGQ>Ek-Rg0be8F@l}#aoF9y8)=p?lWRirPTp<$X{6jiCN&Q zx7<4uj&+QuojCdtj*WyfAmMUVpt1_D%`FI4mn5!w=9K#@O1%NR!BbgY8NlNn`&q^m z8OBj*1MX6PH9lp68_K-?8D$~NZ6176+!Ns=L5~}biiGI71&O3X-r1qleB=0u&!0ED9 zj|JkbUKBl$9ms_}m7ESMgSFHL&)V&DZfZ#(rb+2{-6)uv*&Rq!0AXWMWpS~lc4x5uHcXf2^CF57U2UeD9)f*vxJw#~{;e6ivu+ z(&$Pu4FxMwW$IiKWbxvRYqR;N2l;UPwirPA*oe!<+PKtGAGDfrK*V0aq!uYkHXO8` zCSXi*)5VPX07^ggahtsea*X%EbBr`BFPjyY8tja8I1gKyVQeNUv(X2+Gd$v>JM}Re zT*fX?3in$}d?4!a{#1w~gVl)ZjLs!Y>s7ekNLsX%_z-%N9XdNp5+)LW5RT-=sm0?f zN$W}f=$L=OQe6gBI_jMj6nB=E-2bAwpJM;;x*@cPgg=DTGN7*JJ4_Q%; zw6afnX)xiSrHWzvC8tyWiH{6jNTJn6uXeO-=qzV!G)RV>tyW+igTrDPEGVy5m^D~) zC*~_$A;H@Kz6W#~K$262XK@Ut<@JT9Z#Cm^3~wGZ&KJc;erU+#gpz=aiyJ)B>4hdS z{n0B9bue76;hjcE92cM9BLLtIRfgQvNbCKOm&<}i=0ut&vs+e1ekMGL@9&CUV06KM*s4#xF%S0( zbdy&K;D_K9`2Ym?A4h;Az!BgGa0EC490861M}Q;15#R`L1ULeh9D%F4Wy3iGW>;<( zXAoSK#jfQj*VNMluF9lq_7wMoahH!*WzaQSrlM3R)F(DeK!x!4e?>3w%!U~U-@5+g zY_?zE13~_q{F&S#e{jhw&NJW$a0EC490861M}Q;15#R`L1ULd50geDi;LArKw_8R= zhAX!l<2hNx?OB;Q5T9?u{Q_topy&nO8U6I@M$NL{cVS2H<(G}8$PwTOa0EC490861 zM}Q;15#R`L1ULd50gk}G1_87kaFHE>i|h#OWJh3w%<-f6BbfQuVa89_ANZW<1wM}Z zBalD-*A$ZH!V%yIa0EC490861M}Q;15#R`L1ULd50gk|>K>!s4-AFNzMT&vU%&e^Z ze8{T@=9f5k@Zn$TX5LidS%~Kgmp>ux(+#dn=N02bF4?mG13mL{qy@rRM z&dAHpGn)q$$N!9Wdr^_iY`2;dF<-#p+~a_EN&()1r&$u828Zbfyur%qfX6$X9Yl`D z>MK!+GwD5WN6 z_X}job%Ok!Tqoc7Rj(~CA4h;Az!BgGa0EC490861M}Q;15#R`L1pf61+?JIGz5`|m zdFEVK$JT9;rlv^4w)QpqVMh+zniE}jDtdJ7C#RQ1pV=NevKofy_CP}LKy35U_M>Yd#kMm`BL`PTPOptD+=D5ut&48k z5;;>B+0&YwqUxcbe@32=pK&dusBC{o+b;lH2ynl^m55wIFYv-2*Y0o+`S0K0et}H+ z&++{NcnAOE2yg^A0vrL307rl$z!BgGa0EC490861N8sy@0PY${J73^htW!cS@bRva zS7=Rn$lepIc;r{R$ z0H5pOGYCG{Os%#Wy@sjP7N6BRwc2Jh_#o`H6iux*8w@5ik_dXd72Y6NNq{K^Szw?6 zMumGO8BW0CJ|jG6HQIn$yaw;o>cSGY1*yjd@pc1Ho84!`q)Mv;?vTH-0ur;pS8us@ zrne##Pdj1C0cIdoWW`YxsI0+~0Ua^9b%223sCMM*otO!l=KY>6Q3pgt*{T>U%TfHcH zAUlu?dn!2{Rt9US51zH#y_91irb+2{-6)uv*&Rq!0AXWMWpS~lc4y{9ghoP`?IkGr zRuc-XS_lL@Np!97^-_p#&^sg19gMb4;t~qDD}uhtz|4!17^{b>k|esxRYa7G)aJg@ zbg2qeR;9~u=Rzf9xGR||Dufn`4=WxEgsmlNyCuto)q-8h=QDyVB+Hl8;6dpy89-hN zZ61&pyQh>%3dowR&kYXW67^1`VKD}qv3O=gI-HqZI)6pT8z^^sXe3P?U}si7o;JRI@xUH{Jlz#Ds>|Jh=~V%L zkVeq1a<8m%d;Fm}FsK?vbRJdzv2JocO#hkjy^FqKGrO4_gG{qgG$F@HqbtcY6s$;< zsdGt?#fvkp&E}&XSH#zj9s7q-CUn18`i zT?UhQd3?1J3RHVLjsF+FVCjrW9M$ADf|*%AN)I*v;=vPd8#J}rVD=aSzzij(!H7_D zzvybhNEtwGg8AR*!s#n@q)1F67qJ#QSJh~N)OB^}(x9?V#qM(TgafBK_METaUG?xz z>!iXW56yw4Aj{aR6TPi-RbVE>*(_-vpTE(tuc-478v}|A*z;JgN@`H- z)J9yCC1spgIa#65R!bocES=v`lDAI>R`BB!#jZY*ElzoicC2(sh@0-P)GihMv`iTCh7jsQo1 zBft^h2yg^A0vrL307rl$z!BgGa0D(b0#|j*273atE4K?`&Aiq&6cSs6$bo`1`-27g)ElpW~N{KW}FH1v&)zZ}Mkyhs-?@ zT-s*msdEH40vrL307rl$z!BgGa0EC490861N8r*RklQUIBg2*3jq#i;;`Xe}9Ei`i z;eG+M4^Z_2Z~T1EO{4yPV;6P=m!^C?RgM5hfFr;W;0SO8I0762jsQo1Bft^h2yg^0 zJpyP);37K$7ugZm$&SDV8RO?f;(UP*zV-3!rz?-d&lmVq@l$x|3&?Ze2yg^A0vrL3 z07rl$z!BgGa0EC490861N8r*Sa85sk_hX(3y+GsS$%9_J`<*sCU!c2Ypdhc72THF< zJ}F0hK`a+X=swjwsms%@(T>#oK;wlQ_#a1rBft^h2yg^A0vrL3!2cx#ZtR{fWDF3+ zJhRyqJ-#&7xHi&wD%QAFv2;y%+_G4@i1Ntr^yJ=s@af3j`E9LhV+}{5O$)&fS9I;6 zwo@Cxn-{p^YHwKIzOwd{)7v33a&T{3^P0%1W9`lRl2f#!$*j?y7G-t7p>@)inTgQ> zSr|;V;RgF~6I2fJH9HI@kEmwSUVpHyX-Qkt?&!8K)FiTOU0c(hjz!C(H7nXq)kT(U zJ6}x<7Kg=@nu);*_P_xhWD<IQ)GXJBRC9&m;V|Aw@HQU=8 z4t`X#6rhVVo(BJA?JFALhA%|VU~pK9((*JpOqPK5QsoH;ra|+Vlkj)Na_akG(=e~ zH|Ac@(}0dD73^s{h~7gv@bqL>^0t;55V7cn#>mn$vH3eA4VzfU0SSrJ?}2{#QB7P7 zQtM>`*lnGnOty!!A#Hb0Lgl<^M>cP3Kez^RfK%4X(h{>kV|Nkt&2E_f7neQOc(T2r zrhWTCCH<~i#$sYiT87Y=OGICpLq z7`@VRgD4aF4R`=2vV>k>^j-2B9ksn~Lpy@*@-Ah+0GvPgzWk2-Q~5Rdd-AvBX8D-h zAn%78_#a1rBft^h2yg^A0vrL307rl$z!BgGa0EC4mkxx1WH|aroe~H^HYH zd=h_@UgAsF9MTx^_|j~+m?eLPx7;RwF8^8nEdTfUYjZ!#-I{$Qdt%m8Sq|Z2VV$r- z5OTWbP(l;soL)owWcd9RrQX^8+0z5wU}bf{;|)&tmnz}zV<(OqbMH9)_&aYJ_kcd_ zF8y71>Xke7gZ!m~pSeTI?bWAGMx8(8E-Cja7nOhMgfb$Kn3?DWu_DzE;&F%!q$I%eV^qj4}k81je8)ku@U zFgPXMQg6@`@K>Q1Z~eXF9=I1D@l=8@?}`vW;Zstx!h?9t=P&n8uMU)Vy5IpP-%!Gt z?ip%siHAb&8A1JRlkU0`QwUaIaRTJaOWickMHb@^{6lhh9#OlPhJUQu8$_8@#l~F`oP~%a#Hcjx zm^HbH$z$vEprMx7HDEx-qRUBUlCLL+ zwJ8#md__O#-z`X$G}wT*UbSr>_0CCeZbd@;N%zHv!7r3Jn8~~Wzq?#by{vMkcUEQK zQRT6(hbT?VPn`z6v%LY2KSIT*E0X59uSdJzmz&dTc;Aefi6O*Y0aG}QywIfT zm%Q|rS1-kzUX5UFe_c*)ufBaVYICXW)vIbKH>LH}_)F9Ln0{*07qKNq*9#z@e}>Y7 zLuJ)7ODf#{&WoP3H3duY>(THJW#{x7*)QXHU0S~TffPI}WPU5mGU zQVeVS2fF3<>enx$b`~{$(&eOJc1rUnUAQ#ue~upB^|G^TnZQM{A{1Y=-tAH>V0j$G zRd#%Jk88k`b@A6jY+PBXOHo>;f*26*mM$uZtGK$LFp0;+%$#0>du0R@qQ~y4DyyvU zQZOr?%;Y=A-!nPsr7P*aqy*`?!3*>mw7jkDLn24F63I*vGm82G0i(o{SBWVvOI7Y* z5a#yM>1FO%v?z(HjxH0qs_Ae7Oc)vEH^}ywNAeTu%hQLSB3-o_$-@?D- z)YRem0{N0&fc*jMWVbv_`m6Mkv|PF$qWB+2fFr;W;0SO8I0762jsQo1Bft^h2>c&H zz?z>YWMpP$g4uq?imZtlFDpj!Ha#AOVR0DLV;s~&Ml5;o7(8b-J4Xng3NI;7fgOFJ z!w82z6e>}a5S-v(vYT`1sSLBZ_%>m_dIvm&CoDLO>bVOPWwyd`29_KuAv`o%s1fF8 zD(RBRJ{Zv_ov1)jXxMKvW#OvW$=r={f+GwH zg=IqgWCAN5KZ0kT6e-7^q(qss=%IYGd591e=ChpG9R`P4Jxd}v%52U|JT$nAhu{#0 zc=42nFv^?(4^fo{&JpJ4r{)BwqnP7Iza$>QDD+V{0>wG8U%(f>XYFf+IsMR%pjCV2 zdCwP+w}9SYrMyIbMt(}3EeGUs*(ZNfen`GozEi$ccF8x&HrXH#k^9Tn$ydsk$vQbl z7Nq}@{!{vc6q7!bekr{r{aE^e^s;nDYLbpf2c*5y4r!CLMp_}&N%N&ArN^Zz=@F?^ zdRTfunkY?>ZkC*qL$XLCq(M@FbdB^4sk@|+x{3cI{#E>=_&YHw{ziOH{F(TM_=@U1@_*}5uSneLmq2XuGq zZq+$;g*t<7kWR0=QrBIVrxUb))qbjtX@8@ANBd*#E81^qo3w|t`?Wi?8?-C6b=n&3 zY;BdcO#4mk1KPW_w`!f*LajkNNUI0v`5#AsBft^h2yg^A0vrL3z*hi)+$`8P+bXmY zKP{Xl-XgRRZx)(~pAt?HZxWh_pA=3KKOvkTeq1(;O&lBG->?giY*hhS?u$TBAVGr@$!fxWv z3C|JVCF~-;Q`kv-hp>bAc40g5ZNfI2otXnYH+*u(@!IP>M5l2 z<{^FZNu+bn1R&iL+bS+EiFar@gOZJLF#rR{pL53PM?nS;fIk< zn}&4iRHRd;Absc|qz^ub^nnME-hV&R`|d+Jc{0*_??pOk64Hqik=}C;(!1|Qde>b@ z@4OS~9d{s|Fahc9wD5;wz3M8Yy?Z0Q@=Bz=dLjMBH<0%1iS&vqkY0W{(#tMG+M@^3 zOM1S*NE~hxdI9ZF(@S$!+`NtH1s)XOw83}fpUFRheg1!vKao4&oPk&6@5rt4N%^q+ zqP$1mE^m~dm6ywl< zPl-pxgW^7Mr?^>ME7psP#RcL#ahCX)_^9X=r;87Ylf*m3Tf{Nq4Wd;XDGnCR!`*S9eBt5_AL4>vrij>(=O&>uPmRgLdFC-6J}W zZmMpw?oQn;pdTpGnRG*S{dL#szM+#rLy)2UFYSM5f2WOT-`BnkI)YcVFKJt}$Fv8v zd$rp^OR!42M7u!yq_$dHp`8JG0zCDB|8WF30vrL307rl$z!BgGeEku~$;!xd39XEs zW~_y=X2wo2*2LIJ#!fJHoUvn!9cAnYV}}_##8@L^4U8RR>;PjgGWG&v&oj24v3-o~ zWo!>)yBT|qv0aSqWNZgx+Zo%&*jC22Ft(YoO^j`1Yy)HK8C%ELTE^Be_AFzo8C%8J zO2+CLTfx|J#+EU*l(8j@EoQ8au|+8gP+8gPfir0W6z!BgGa0EC49088N7b0*wIwl;;91}9vg3+}bBYRrGvsd)M&d8!xaES$e z_>>K}>K5WoQC180Q`n`)Mee5%WbDiL#|X~rxSvAqr;z(8jC)y4*i)R#PvJv25hV5t zy!M}id%becQ-8-Z0k!h0cFB7z4FX?O1&bj z&qfw6SF01dXpXUph(GvzF@O=$5(z>gy=^&)s*0ejibtk(NJ97&2J#{p??o?#; zGPv4)aB*Anis-s6$(KN)Kdjk0WQayHeE4vUX2_6Fx3Au*k8W(nH~A&cM;DWW`$P4y zW5*)Tz4*!LI*dQKH`3A^YiRi7^kOI$J{;TF3LdbrHX9n1+OSGsxms(X$dQI^9UB+M z8qY*_%#SeF+3QchRhAZ@(ybiXq^f=$rH`E6t9*r4>F<8n1UHoc9i{g?Z0Zk9 zGh_&BTn$5q$s$89y5ZT5H5=RKpNiJ6?5JJMVA24HdgBO0GIDl9q;@0BtZ z7~X`dfb}O}0*y2^!JXI+r<-Gk7EsHjfl6%$a?#fGLS*;8M2dBhvkN!B`DguhIAS^w&oZHO0{;F<$IyG0$M%M?ics~12O_8Ip_U*flqz%~eSh~beMx^kmb7(w zdus#USHB$A4_HPGEZ3sjwtyIe7`8Pn1$pfN&+vQ0Q2E%B`5^Z29Lngf_UBJ9_w=!b zGqDC(fz?GD*WlAjPlLS28pF{IE2#NE(qJ(bt62yQQdb)}JRed>mY4RH1#PExKx+&c z0=IT-!tyGP>gnNC*lU^^aKu?u8`-@G2Bzfk%!YRNR;x1`hsL{LG{d10hrl)a(>if- zww+Qi!AyYG^E4A=DkI`oYi*V|9l&IqkcHKqk!Q}t>LNUKt5{P+4%Oo*(X=>n>R5a8 zJ^&pQn32ZQk%fC$iz!u4q-L~*2ZK3(K2`mzTyr=o!DPHMBUi}pIWy0U6;Eztcp*75 z7D(eLAO{P9ynu%X?8h?(_Y4QJYcP;)1AuJp17u5YAe%1-G6g=0Uf{;fj;r5uKmRV; z5!@s_isuXbMgC0wtsIem4HgA&%CF1cm%lBy$j9YI`FZ&{d8@o$UL`M;7s^k|bL5~r zQ=S3m6i$&R%Xi7+<+1W8*)AL9q4EH^kK9|nTo&bAIYatf`jhmD)FFK&{YrXU`ib;I z>ATWd>6CO-IwlV(YeK|%N*M}Q;15#R`L1ULd50geDifFr;W z;0S!(5y0K+-N}leJ6RFPbija2Rs=Fx5y)gkAdwY;L{qI zBG8f*ftIWYv}8q~B`X3gSrKT+iapec<$hnD4Pkr!0prWbf-Km zGM+CmJTolFyXEoVIbfr76P)O`M!ZLqbU%c1_{ZrowP)d!yduq?HAgj7n&J5$=kLoe z&F`D{e%|K12l9I6{xr8fcS6qRIlFR(XMZpIo7q=(`&qY@-EPc!BP*13P3Fs)VSfd9 z5q21EC;=an-oWsHyVPGDbXW}oZYXhk9-R@Wtga|^l=~~Z?!a(#0qF;aq=SkKrQR9C z0y9e7gG^TIu*7GC#WZ+WC;*k-qiOKq8x*g*j$oC)LT?S~ zS@L>+h0kB%4|#8x=LCP*z5uwi4C?WoxxPT<%(;}XuQD*x5vUA-59~o$h`~3^3srW$ zWVXdG&bx7BFf^y!JNm|;C*ZFNjUMEyt^oIxl@)^q&rJmN0KpjYSvd$RTr~``A3WFZ z8`PJ+4i0!j)q#o|=*tLj!yK41$y@IAgen7L%F720oKgzzHHQah`9q$vX#)q3fHIDA zd&<kwDTkdf6JX={h*0H>8RgzV1DUJLfy40X3@Ak?6!4c+L*)i0B|32M zs8OS^O!Eexrw~={3U7IrWuPmA=Mo!1kc=qx2f=OooKY;Ui`?OYGdv3RFJ%n0W}01p z_msIq(`R@qya9lJ%=q|*N7@U?wy|fdQ1$_-7LAj`H)0aT$&kcR7aflZ*B zaCai?xrFUjxB;ssvTy-@J+KpYueR@B+P<%)W9O-iG)q#W&0#X!6E38B-1U#uBV%7` z4Gac{!7?Un$214pA=(97|=-P#Cr*>u> zPfN$>Fcx~k7Q!g_k70zna2M1^Yxc#C#f2f`WEZ)VhRv9Z5p%iF9uQf$7>mASL*&pi zk%mo?GfShV*R=0$j(3QbF0u)PO<)n4VGe5{p9_{RWv6tcb|G$=UcC&mYhMZ5hnIEK z%!h5qZA}Z_M_A!z}Jz*>VRD$P z&hVg%N#d=vBmhom)PWb1#M@~}7#x-&XZZSyN#dPONtlez@PLa+;@#vVz__T$VTMNS ze=$kCmzD%{I>4ge#U$~IPDvO*Itwl)iC?BAVRsmzQTtv@62D4I!UjISoMHXNB=LS) z5>|%=lIU|WN&H$#B4Kf81}v_-m?VDFISIhx+KWlzgU(4nMXtG+BtGmiiK{OriH|xb z0it!)#U#O0?(2C%W_unANa_K*oP`ndlW4pXioaSki#~ zSNp!kjK6hG6p+3A%Ru(;osz~zL)+hfw$U=P(~^cYr0v0Q4@}yO@^JC1E@!`V}E=6m!G3vASbh@03P+8%ITN=e&#*!h5!BEM!+!^ z&3DG;nY)wu&ffWLt!weU`a+H!+L!49A@1|~EqP{HvDQhm++@aTabx!-&qmIy@7THx zE!`HOofnvgwKc~N>H#aSNbNEZZDuIcc4`CI*hiMHg_l=&tpKaT_J;NFiUB5Y(eNSs z59V%UJqeG%)s7eTM|K~Bt8gA9e5wGk_81g~Oc){O)N3pex&~&qXx?~uKI$vB?ujm~1Pg-9JCzl^)7CP+pk|gefxm}s0cYcAKv=Z5>tt= z(kl-}cQryQ1M*-L84It0mrSTJn(#7ES+(F{z^$!mMUIz==J2P4EkPG&Dan0#q*9tQjhV(9Pcu8MHM$8#%m9>DNrF!^+;V^9&3H zPR7aljYnvIyRfOPX$_v! zu{TWBsDUDWShEKHqw(*O_J$g;^kpivk7|}uXWAb*xE@9T*5FW6>Lq~fr`tDx@W44C zkR_CWjS5(&mYTLxb!<4KQm*P)&>FWjrBcR)dl6Uw0~o8@53U0%Q?<|=8nME4TVp%d zfMF~O)54=rXBc=eHPWA9*KGt%OdX7Za5F@LgmrA(4Fk_dU{$|+Q|!zs7!lZ#3S$x@ zQ)E#+yC=D#VCD{Wf{da|T5)iy*$4Kt(G6!JyS9OuGIq-*z_b?HgOwZ%h}+I=220`g zBWGxg598S(n~y)S76yy=qCUaWTqPlF2WONMPX$4M!C{ zi5egEQK0EkAzMNrq-Wse|8<(T9aUbgfSabymJ$X5Kbd2wlIcT(QyRZK_26If_49pAs{jxo6dm!HzW>Y zRU*&$gtKKo#4)L1LwmG_Otn{T!>MTz92l?xKnDA01evsgaeMc8DUL)pAM2>8kF>&o zl$7&|My&0@z0m~=aV$ufpeHhivc}GWAuiQ6AqCXDSi^YH>ejcvxGrv)O@)FwfI&Eu z8VDm6TNX2!ggl!cW7)SW1c(Dzru zu?L5s2PNP@w9)W?a+*nB9ufC*E=G8`lhh2+c`SPv-oL88$ei|7CV zHurTr599BW$myEMzNN{tQDWjz&BEaq4K}oz^u;(n%klAfwz6(d++noupFA1|-yefT zv2?EG6zdK+vnaD_(x47h81jAaSZV{Yn!Rp<^sBa}V{9mooP;GBu4AIhRz>Sj<3D6bTgwWZN|L5i$UJsp35;1l;dV$Ks)dE! z+zi4P+c!UQ7PE?-+>bYOT5I9JNArW4$f0^Qk%jx=L?u=_s5Bk6(bXENecuLH`~jF$ z$mq$PxJ2609Nh)avU4()MQXOgD26k9>sngE!C0g&udsv-N1)%v+o}Xr(OpMjDa_``$e#7ImI8G_ z^x&S@aa>_x3nVRzU~v~a3G1d6iE})XPEDbq85*D64^7?HdKOkClrNKZ7@et5I6*?f zFz&+7HPF@jPlcabJ#&ro=9=c|r-uCa%c~M)p4T5%O4u07k46vA*Q+}Q_qHf6!C?p=olIV;q5FgiQ znT(^!@uh9eb*YJg000K8Y>6cX+yQZr6OA-%R!a=1tl?l$8CwQ#LwHCG9xpPw(_t*D z55v+Nr&AaN8`kUlo8t`wE5yU_IgT&8iTwihcZzpi^QQYcjEn!B(Duyxd2VaY-0Wr9 zcw_b7_1@e9*m)Ghol6A{eZkaftKDFmT5YrY zj6f|$&(vyrDHKCZ7$PiqOJR}M3}LsS1RnCbZ3V;h1(j8pP!Lj@0!dkXMmNSn$ZIH# zf1d`mWxJDD-K@h+g?i8|nBZ(AYZp=Ti*rRGV|-6z_}ck>puTokxWZ{-P;z{!Z`lq9 zI>E~^oNxwbK6bpY3;|`QKeB7l!;9c$YHoq2G7x~RRnuV;*(`4Wt7^icCD+|J)Htd* zy%t7{pj*uaQ1rP4!BBN6>_MAe0o%yph?Up?4!hOp8-q8eIS_U}bf{Lyy1# zp`qaP;LOTLy#@2&iC}ffOn)fkEuHQTQOOK=Mlb?%F91W>jIf#Qrb6RT+%01$nAatO zbYm)l6g9tR4j|aKXKz4I3kceZl|Mv_eYu1P6&GI`TuiWWXoRiMY_QqB7#F-T4Ht}h zW>hxdV(fWs`2JcEa2b|>#b~z|71^vNOOc9CadB!#N!(l9Nn-9BHK`2c;J#5~!g(8232M$Lb9AC5eC;CZ!PuSQVJg)00qE(s~j;8y=7a zOPXtYjtmdPCC&ATZh`BTL|EZnjl=r;MimE&%k=S^5a#5i2#dy83?+CmPUv0sc$j6a z@ue4Ap5UyDCoMmo3FYkDv+sj6k#>(~2Q0Oub35gp$TPty@6Kawr>Qbg-hV`F*4vJid+NB0!%i=Q9Kwi}Lw^kc2Z&`AW_9SA?r@2dq3 zIsQHjyOa_?pF&aKQMMH)x_@V68|+nq?MKVNXHa{~Mz|WOLthV2k>+Jk5!@i(a0Dor zPr?Sr=(cAx!}Sd7?j1%E&>Bc+zwE?S`UvwE^RGm;rDFz zWTburY=?m-qiasFJqzHs0!%8g0@3B@p(FkryHR-#HqDR;V_p0Q<%La9c8H8MHsZPc z^~>?cd)hgnCc(DsU=q+S;FtlnCOsd21mOuM8f8mOv}SE2yeRhELfE+q)ra570VUY| z2gZHaoEuvqjC*#D!*Awc*!me+au$De2X_mbj&*D~!fFl=vQm;erx(y|!@)JQO%gUL z#G4zyDuCbop~(0!?LUGIG4P`_wlVx#zIiib5NTe6jkIe6l)7WV{^;f-npk5?y!u<} z*oL&Wvpb_<*gV1TqCKZ0BX#RxpB#d_ZV_yogZ-tj$p&s`TP+b-xP!ru4c@T+|FicU zfKip%{x=C}Gjs37f{2ZgA`n_CfhZ6LQBed%P^=ij0FjVj5}GwbNKZ&YOCX_ymVl(t zQ%K?#cXeHN-F0`>-PLt9cV^-i_5ZfLx9`3E|IYbl?#v`ucu(KzdpF8C-`w(*^PR7q zdkS7hxxHOmJplyD>xC94YXOw?y-*Wy9Z^!7j);1Cb;&#`KrP*#+8y0ILhzbUhZ`}X zpB|XtJn%U&9SA{8S9`|QNhSzR{@5av!nhQCwzrC)3>YD~D#2)RHa zeXogz9RXte#AAB9i>5fQT%hfvy)QO?f8`&^@8Cy~MPD1ggUWK{F=dIeP?@LPrX(v# zO1v^j8LNy?u2Zg31}i3Iko=$W-{k)%|6X>=@5#TEeFS;)0)w5+wPvOI2CV!6k1hb7rE)e>zPYq46c zwhXr5GoSfy=8w&8^LysE%&(hYHos_YGq;)>VVzN9t~3{!bIlvgYt5_7kDHg6?=jzD zPBu?9N1Ml*t>&xEgUu0U$@DkV$0oPwJ=0sJ*G(^*UNp6tT1}0n1Ev~NrK!l2YuX6! z4y#O$o0gdFG2LNGHcd4}o5q@~rmIbZO%Wz3@^6tJN4g{5i+n5c^~jeaUyN*vY>jM; zJP=tES&8m7{tOBj6fh`YP{5#oL4mI!1;RsugQbt8j~M?^`Xl2%NPl4bd+GO#e<%Hp z@rTlfjQgZM#%{^Y*d@6bJ0&M$P0|?mO1+FfkUn7izVtrh-%7t_{GRk4>JfKWF?i>1T}JkltYY zQ|YISeGq7{4gJ$hb%9VcacsGwzbQ7+;VsFz%E( z8Fxq>jN7Gl#%)p?pVcaY= zGj5Wa7@v?%Fn&RLf$?$aIOAi|F~*HjBjX0Cf$>r4DB~m25ypq5!;B9}hZrA}4l=Hn z>KPx94lv#??Pt7C+Q)dWw3l(6RL8hhs%5-K+QWFaw3~5_RKvJhs%E@P+QoRMw3G1; zX$Rx&(sst%q-~6=q$J3V4N%El2^RvpC|a-a|EA#mf$nb5PbS+f=@j~ zaMLD&8#fZ%uz}$E^#s?gBbbvzFgu%IRu;i0pCq_;Ey2u8f*BbE*Q_C!o=$M}YJyKZ zK`1COB^% z!MpAvc;}r2=guW~#~lQ3zn$Q1w-KB(hoIqi@N4LIa2_p(Ub%qtJFhrS*sr~cG?iaQ2oZ6}$hlj#_YK^*0EmI5BT=gk6N6k=Isms(y z)ce(Y)H~H+<=>URC?6{yD!t0P%3I1$l^-hKQ(ja$lru`R(x@C%>XcndrBb47QJzya zDp^XpvO;-Wc}Q8L%vbJEY|1U_t?CSQsv4_KP;XML>NV<>s-i|HQ_*w93zjHN6W+IVe%ESEQeYC0olMOmJcl-Sbkx7#d5)N*3xWgfMj5|Wt*kc zvc>YOWxXX6vVj!KLzeq2^Wc+lh9wcwfpL}*mTN6nT4YO@`5%xEd}994{DJw`=AWB? zWd1%R1nuTi<`>L|&3nze%oUIkILuF(v(4#dyZJHm1CSETHQUTLo8!$B%{Q5cLr!p+ z*<=ne{k!SUraziokQBUa`l;zv(|1i>rgNqi$O`IBdraF+Wu|=7bEXZD7NnV$nI1MR zGTm*u%`_A8f*4bjX{6~o(-4zl3O7mezsZ048g7J)Dj5_oC}2>)pnyREg986F3WS9O z1>2;L1p1>ue-P;R0{u>)4+ZKI$SsgdAg4f@K)nKeAkg~){Z^p&1p19Y?+WyeK))8~ zR|5S~ptl8jOQ1Ie`h`G07wBgKy&=#~1^S6VuM6~Jfqo>=YXbdHpjQR@fk3Ya^s+$T z7wCHey(G|g1^SLa-xlaw0=+0uk3ii5bqRDqpiY501Zo$kO`!7vofGJ+KxYIxEzl`} zP72g2P>Vp#0yPPALZBA}Ixf&Lff@yB5a_5tM+7=7&>?{i3REx90fF`lv`?VD0@Vpr zE6^T+b_-M^P_;n21llRk4uQ4{v`wHYfhq;65NNAF5YU}NEcjfDd?77o~0IACMpfQ^L%HWm)pSU6x~;ed^W z12z^8*jPAVW8r{}g#$Jg4%k>YU}NEcjfDd?77o~0IACMpfQ^L%HWm)pSU6x~;ed^W z1GeQ9WiEA4h!n)40UL`3Y%ChEEv0)b8L+Wrz{ZjR8%qXkEE%w|WMDQ+24=Hlz{ZjR z8%qXkEE%w|WWdIf0UJvOY%CeDv1Gu;k^vh_29jAakj#>SWR?shvt%HdB?HMU8AxWy zKr%}Pk~vUeG6yP5=0JtX9H=lkndAn^Ga1fcIGy1w3~y$b#BdtJsSFbtPGOk9FrHx? z!&rtf45JxNW;luAM1~U>jwd)Pis3khV;PQNcoW0X3^`EYEDlsSivtzT;y{J7taLYd zIKvwlUeEA4hSxH@hT$-VS2G;S@G6Ew7+%Tn3Wk?6yo}*shAP3C3PYK?+Cs@0W`-t) zkqjdkhBFLf7)lJoVG5^*(1q#VIE8o9;^UPIT=(mf|DM-+IG^PLp-)O`gPL#pO=NAP zBXVV=Eplw65`H%PXn1vaQTV3tC&C{NpBEk*J~I5u@X)Zogt@}r3hNGQ2|Ey06}BPl z(Xjbp@nNGwpH%;R%6sL>hVUsA0nIOAj@AZf56H9hn8>Sn}4(Auw@Uv_!rLWxC}?%TSAq^SjIZj` zaprM2tq(Dq%|WKWn*Lz=x#6I#LkG4i0316 zBGMuri?}c1j)>_Iu@Pe;u8X)VA}svx;U9-<;lB+3ark$`XNA2MRuGmRHaqN^Fg0vY z=)Z>kG4zAbw?bbF{dVa2&=aBcp}Rs$Lvz(^%XstOpu@3P5-oQ zhf%OW0fPem6i`Bj2XoksY#Z(C4c8je&|c~`I2s5 z(&bA!eM!xi?DZu-@Fm~(C4cKnz86^M-vs`A*Z1H%fp>l#`14o32Y(rO=WXAew*v3H z8Tj)Tfj@s9`15DJY~BdG^V7heKMDN#df?9=2mbs~;Lq0rfBw){=Bt5seh~QcmB61b z2mbti;Lq;`{(LF$=XV2te#cjZZ~Kzp@+DvNC3}3yZeOy?m%QLhcKVVXzGS;E+2%{0 z_a)Eyl4pI%Grr_$U-FbMdD53`^(9+;$!1@&$(KCgOTOSs9`_}W`I3#kWP>kx)R#OG zNZ7-^JBI@A9Q56(54>~0cW1vZxi9dcy}mnjfp=2ldnvOUU&oeI-I? z*|Q$dFW;}H7VD`+dg?wswNOvptEcYKQw#LeeE(wZ)-TV~Q+Mg9JN49DJ#~kkx?NA* zrl;oUsay5bY&~VuQ?v9`vYwi$r)KD>>3ZrGJ$195O43u)^wd;6m8hqt=&1xf6|bk_ z^i-^#iqTWidTO$snxv;D>Zu8OYP_C`(o^H~)L1<=Mo-~mo*JR2Zq!p& zJvCfU-Jqv%#PW>$>-5yMdg>ZIHB3)kt*3_SsjKwV5IuFJp1ML$U9P7t(^G@}*-h0i zEB=?|0prLLFiV;B*GzgUGT?~_{d%~b3iB@_RKGk(PlfnD5F8OQG*WCkCFu;dr@DWE z><>Tuw~33@9pqmi!V)8?vC!Z*%ctZAp;O<03&x*80fPbt1q=!p6fh`YP{5#oK>>pT z1_iPmnefmya`@!SrVk%-ZJaG8Zd!a?e0)qy{4`wv!->?)jP>@c>=g1>8#86rT=?lH zv-p;FxKW4w`7~Ic!xB3tHgOU>+f9ne$XssEUYDARc(E5TgTCL9L8T5Ia@pwqrSkEH z!tG3%;@XWM6+6kGy=4Cv$~Zdae#e?GJ>%F|_=CIOk^bdpOtGl%cdY)>Ge+3y=qdL* zp7`=JPDn~faNOxggC9@Pdyc0AndbAl@9xZ}($dpYCMQgawvK%OJ|;6aWLxJivc`aG zZnomWl=z#i8>hsNv(8$(Hr@V!ec4@UIg=CO5+}t?v5vj#jzxFRn}9g=tL@g??WwCX z$64>Nr!1$a#Isi+HobjvOaieV{x-n zh$c0!zQ89(;{Net*W(B7yLVn%j@`N+wCJqkcj25b4L&pz?u}#Ku`^L15OCgG8pU>G#)0XS$!Lt|6nYCz+b?*FI=Pa@I zd&;_CzEywHIyP;1D9&c0*T!m%+iahD^;$V-MigMRS%cqE|4@=8(5+NO<+!67VA3TalJ-U%DOiay7)7GS{ z^p)m)DJNxRw)M{Y7R;v{;5hpUIQ90+fHUTh?foxeWM-sirYz6)wwp4bkr!}beVQEu z*h4jXA}xFA+I7p|JlFpsIq+VWt(P#BOHIwPrwAY3)?3lRi{{=v$9n6W+h#4Ex5zqn z_Tq&L=geQUl&;>lXx82L@GI-qF2B@Q?pZkZ?pX^TwB9x6LFNyt<82ET&Y64re7ZK) z)4AiU3+LQ62Sv`FbDx#2W%JXfaW*+@=*Xc#4|;jjzAP5?^wfbIH5c z5$Mv`QNdQ z{bw^$CQmkL>oH?^C!p2#P5rxD=a4kN=RDJ({@%qn4l9{GD=j76m-{O005)W1t=3=r zX0X!2$iQXLzR{kQnwD)B(~Ku=>d!>}Pl@Tq^Q2&(8zXobEV#@w<#=xDZ$$<$j(+*8 z;TmuD{Z`3%p42~OZu->uVc`6fZO_R`%kW$0z8U>~e|Xr?i8ltV@lGKr8Cb$O9tb7{ z-;ap?J@32anLKK7C!XG8-ohMx$k{e^&9{rL}?6%pMDa!-kF<8kFs2J@M4qRhb!f zPKJ20Sv-I4y^H-0U4GB`WytT1%d-aY_O@U+vm8IlAUJ@{_|j)E7-#0cOAI(Ht%Wa6 ztnJH}u1d*X<$Ee2A@EE^QHJ!p3H=USDeF^mQnLJRi=F7FuxEDas?1E^yc6R&g87R;M7iw|J`V~mFLbqsy=zrqS|xdQM2{^@doy{CSYI%o9# z<4IpYHQ6Qj3mBuk3_JfY`0h)WC&3HfGnNSR4)bK_&D%{RBQX993K$eHC}2>)pnyRE zg8~Kx{ufi=uECK~&}>NqqTQWe<8yVpA zI#oqxi-!&OWd^6{&bIsjAN6~-2tW47Rd~+t?bsoFk^2{tNYOoG68l|BNQ$4Ds(hJ+ z#3Z2zjw&izf^rdeEjCs56#{?Z6xqG)uydaSErEmZ-nJtYhn{`BYbE~_)Vw@Bo=TA&rqJGRWeI;ckTJa?` z7XQcSWqL2g#oiK$GKURgz7|n$1-bkmMff*FJM~csZ}X*?X}3gtk+v1oHZ?4X}5cGz1F?2BA_d=TaT*>#6g zeOGg?Yk!lT|3zC6mlWT>1@Nms<>pT1_cZX7!)ulU{JuIfI$I+ z0tN*P3K$glswhA{0td6#L6yA@D(rPomg%~M-42@B?VyR>4o0%u!3cIc7|w17!`SU$ zXed2Di2V+Ru;0O8uiwE3spej}K<&>m9JzmbVx5o+G)U0z|Fim$+NXY?zN5aW{zQFM zeF+`}+tt%*6MPcZe^sqE@---6P{5#oK>>pT1_cZX7!)ulU{JuIfI$I+0tN*@0Xojx zSS(;;v4D-m0yY*4*jOxJW3hmZ#R4`K3)omJU}Ld>jl}{s77N%|EMQ}?z>MHQAwiKd zP{Kn*XRln~idumyrsUO{z!c#bXMp!gVqeX zEo5iNJ@lCIXHdYPz^74Q_7&k$(2&IuaVD~w5*nDFtu(1?7kQQ4Vq+bXE{~9chHr-A zWhAVp;IlU+Bga{Nzk+M})jDgg)mc}rmDGQ9p$wW+&O^D*{YP9~Rj$rD zXVETKW4Ttj6$dKdn`?xH=nLolU}H5c2Y`ut&dc^kvj)C~>jN4WXYm;IZgwj;O)huAvce z5qi6{uI*ZJxvx3SJ)51?=X%@9spK7vqILb6BU<3tARJ8PV9>E0V?%C};a9-D1)hercJ9pu+f$FOtUD#tX&9FvA zy`NjB)it_m&$t`5K?fC3>pIxm)`Xl%Eq!mjxaIC{r*hj*J9o8f+sbg))l}TuvDLY& z&i@uT`u*I!QBfw-#EBD4rl_co_iwAWI(K)F=07`oqg+}m+-J^chhBioH@e<*RO{?; zH#fue8>&SwyLWd%6O~$1)1tRVG=l2YRe&mM&HMUx=et|FwF8?qp~6~y4tGUfph{mp zmXIRR!mR~b#i{XHZ8sXn6?E=BqLmzQo=py5Fw85W}TEz?R=hi@*`2F17 zs4*Vv+u8GJk3lzEJO&M2@tB~wCzA_DaTjEF7UzNo{56z&+xKg&CqY53HtwJV(Sg?S zR^;mYIo+yVII90duZ&80EEW$f1DtM6eJpkqx+W@0bgqXwf)@q#oHg6~cI)|g2|Nv)?wE7*;d75BwW!NAmujy<#ss6K}URBLO)lhh9vI^3;!+;jbi z%6$jE=xsZ$9d7XESfusj_tkfzsm{7$@di`V!_Cg(a(7-AuiwRJ16M4TE9NZjM#Tj^ zMa4u5#Hg{3N*!f&wzj!@>Uuj5x$7JI^#HMNaW2Ls3d6WT@uFS)4t&AG1*Yz#K+Z7T)8_CZ7Y zr~{3MeFoSb?-7q4bR9b<)Wn5>2UV(N2fbZ-0sQW6aX4$Xa`%DLuo-jb=A%PkYI1Tj za`5LDS7%;t`vLSuR1_ZV+e7u$6~_7HDjGG=T*r)4SfCv)#Kh!3pLz7$lS650T!7h( zrpC`zq#uw&ELMU+-jyI&pAo-ado!P|{k#xr?#hU`U~29qDfa74_vRKe#W5~8LW&%| z8QapYbIV!pe|}K76ggx~L>x8T-^>69l2fw*TBZXuO$9if0MJO50!JnS92^I*Z#2N( z;Q)2R0BWxQco_e5xxk9~xz&%wOnQLy1#HR&vLpDb`ic5GRa1YXz72`MYwGvaZ>gP# zIoP5eQx75XV6|GMma6&c^XeuwTV12BR8!Q4)y3)pb*?&FovtP#CgFH>v^rcJhPZ^X z8mkCZ;;1LYmWC;W->s`8T3gBXRUl_sS@sYjf`ol1pLtmGk9;Ra>3vYP5> z{23H5C}2>)pnyREg8~Kx3gNQaSUS_#t<||Gn~wD62plMComk(FpA+ghGQ9y zVR#e6(F{j197)hLg5iw}tqg}Vyn*5M46kE&EyHUV4r6#V!=VhXVmO51l?<;S73_!6Ab} zB7?kk1dk9`dG!UJ>dpSuM@9eoppXl!|6Kn9YMr`EtyD|YE$VaXMm0-KS68Tys}HG* z)cNWis!hE`ouWpoQR*o52K8$7a@C@Ssgm+<%Ab@!DsJU{<=4tDl-HFXDBo4Ol{V#+ zazZ((98mTsJCv^sp*-=8=6s{_1_cZX7!)ulU{JuIfI$I+0tN*P3K$eH zC;$r3@%C003(RJ*fQ`ihvsf&U%wmC=EEbr-Vu9%_7Py7Q0ync*Ac@5S(^xDpmBj*y zEEbr;Vu1t}3&gWnAdbZXu`CvdVX;6oiv=dLSYQ&11tzjsU;>K;# z#R6kkEN~Nx1xB-2U=)i5MzUC71d9c3WU+vi#R9`wEN}yh1+HhYz;!GZxR%8N*RWV% z7>fn2X0gCf77JX(Vu2wn7Pykd0#~qD;BpoVT*hL7uhhRl3$dqHE^z*;^^XVjU4JFT zBM2Q7BdMK{H%AT|v>r+0&!B)o0fPb;r@-v#q{0P5358X-*j4oxG?ZYE%P}#e#%sNW z)p$dYjjp=YPmLG00;Ht4xsg=w4i`d;&so{ztUm;mQhyy;XgNX$&{bCo+X_;)tEeKY zg0@^}J>toYxJ~MKq#9dJ&SQJ>oMpv$6c6?7Xm-`s;I6x;hDypi4KoWZ|1>mpg*s(( zE|fK)!|Q4+i{K4 zQL+Yj?_oB%}_Aq%AN1+ z+79(g=Sg_Z-YnEBp)P8Ib}LjcK?o>$9xl|jA4eadPeCYBp)7(DCZ0kMp~=1NHGR7e z!)ONO!$q;)3HYfU-UAhCC?=yQ=y|&uT5!>MxZHW5O4!Ii(;8i3g34-CRG*_z^t8vg zK&VgGwmS2w`W4H1&CXoXiUps*w=?vosau+gp^A#4oVg9|GmRj#zprsuLuX%oyLd}0 z>>%Y>_C)ULIDmGd1gc0JSA?}u$**|#c04JnqcN`pR)lC$vRP{`bM8Dt#qQkgtS>?f z1wTV!*M05~5t+S@3r$wsjfw7aZ13AvgwA#DuLNJZTMD3HOc}7UG1OE&WHITTCsbCU zVhgqhr7>)X1ByVHGSO#H&n2a9WQl6GoFR>0P{e=a(W5j*w`zxvk+N;`DY#0<5J6ya zXG^(foa}6c!*o39uj?sV1EzB~Lp7gO%ApZl$U3%ue$^@e>T^)zPJjw--?sf=C-gv+ z)nr1k9+g{GtpyWPy9%KQjaguut7#Xw8@jaM2s}vxqO$~b#86|lJ1!`%UC7lMO2t^! zl}mk7l=Wts=Ty%;w{gG|vC${iCDA1I_QZ%d`Ew>QKb?NGqTl#L#a&xl{r%cg%b_5R z;R?O&!+S6!#N61I*VTJr5BR>fjo5=Yw7M2O4YD9xt@Z#9XO9nw?}Y_n1Qr>8n+l&?vqfV-|Ox=AtoVbNf)m4<#xKeF(*;rd5l!bW+zPw z&Mg(a?a&igmnQpA8o);>g~*T=jQbtFLO87W0IqW!FqzP^wq#1p;L@C;vN`BDinf_d!m2}f{O?aI(aF#u2Yuj?z3MckEMnG@?b`WfYE5pL zR&X9u87&PLYGA`awQGm_c6tR*_V(<>b@3!IW#v|PPc0tPx;nLvozAVreKo{>Vz%^5 zmEK8`8tpO3z;sL_BJZ@8Uj*`jDQR+TIHVP#XO5#cT%83N-=uc#okRs6yX%i=`T0yW zvgyEFDS{n{X_cD7RX_!)G&X2q`gUU6p`W9o1bNXIt)w<8irUpvh;;%FiNy^SL$~#u z737|*b?m@EW%YIUc_IVW2+Tw@ZK7tPRA)+-LA!83t3*%kCkCrL zib<)rZF^reJ?ZYw$J9iFwi*)#RzOVlG>3NQQ+sRo!BCOvUrFPdSONBo`9)&%IuD*k z-Kh7DR0}f{R=XF=YH!=t-i~cpeq7+9$^wufDvFBZwF$N@&h6!3K%Oc$ixDrZe8AeU z^MFMFMjSc-&(NB2;EZ-MmzE@9cQbP2NGr+(3tG`e7|4)WLp2O+_E65dh&_Y}0BiIVlBVXxrLT~HfW&9uoA<7HX~wr0>mTeT*`yy%wv>& zWgedIJ93iRgjw55SS}EJuVg-8elBce=)1x91`naF&n2~*S%Y@^-MYqv+lsh_r65>ttoDQWTUuI}5rk1XBjD6$!giQl|dD$X=w zD}pgbo7i#$=g7m@!}KI#8)2|H9j){~&Ud{1#~8!izT-HA(4mT^9yT5m&(nx)!#YeB zi?sZ=p2U6&b6h|vUE67@YU+gXovZ$YJCBZ5TE!uE3l^0kS}I^|+Kzde5|kA-OD*N% z3KBfqlwpR&0ZtrHXa(R6m1qTRzxgNGT%u8}6{sgD<7$9qeK}jr6{AFK8L?9m3#q7Q zZ$|^_Ou>;3H{bzx^IzsgYyHQgvE8tg>X=SQu!4w)PFhFS_}LeN{wi)#(Iv6 zc+aaT4^~I0SA7TqjP@Ds_^NqDzi#pEU<;_5>mwN+RJySumSan_+RTb+A*dfShZ*(2t!)3axJmJZc7kS|Gy zgGgF&B}O$-Rp2=`B4J?*zpcYo;* zTKVz*s#25b^CUh+Rjx8J3B^v3_9=b>AlJdxegYt2>WOFQc|KCQn;YHD5RG`xQe3UR z!ZI-*k^OBxb~-!APsdRQ`jj?oEih5uf&XbkRuU+;z@ChRNBvI5JW8;I_;`N}mp^rJHslxKUJ>598W4vLm@|NjY zaOxUy4fTe|(Q`&(p_H?7A1zIV5P;A&Fi!V?UrjhfJLZ3fTdGSo`{pVg4-<(kB1ah)zuPSY>@?;X?RCK?Gqjeq5gLqf?$tXkYNQf_Jh40$QTR0K|>Dw!+Ve=`BTTk^`u+3|u zrM{*G(*=2T+6zfG{B6)?5*rWwE=lS;(FFJ{ft-T22~)EVWc@!i1cdl+}f>4V>u zSX=>Jurz6{kXA$Z)PX`!13z(&*uSBH=n%|6^f95jfw!e)LICE^24YDQbP!uJRD^OB zGg&)Wk0tpgeTt@ulsAK7HzsB#WI0N-YZUSlJ}^9Tzrq8wsVg~vN|HMk?}kz&Jo6fh zFljb93t?4JU}j(d&_LA9yT!m0xQ3E=rt!LA5&a@M4qvIfv!54C@5R^<0~(>44B;omv)kcgj$cZEG2S{6z! z3yeR50tN*P3K$gVPl3geW+`ae5c#sphQ`GudluWXrEu}M6kAq^v$0`#a$VX}Pe1$c zqsy~WR^;e{+9?#dHZDFX&KIHBcgc|+VUdFF8#3MTVHHn@#8C` z(9D@%4P6jDJbu!Ysd3R$rhGPC=)rz;p-Wq)hk`CKL#}r`P#}XY6TD=JNlctJZAwCH z{4^hZX3h*8DBfph4&a#kr{A}V_~8EOvpqOvnI{o5IS%(8#m#~2$gf?D!TJ%;oBc)j z%D!=JW>(J991Q2rrEs&|b&H9qckC`%cO0>?` z7b|~u|MCTSzohap9Ae`Wz3soqxMD)ZdcxF;naM-Dg4+i%|BQ1yP%;GkGv3P+f`8(t zB~JC{AMZHxpK`o!{10$ZdKxC5fa&QoI4Naq+81V{*rbGMlj7qOr%cm_;l;YqmbvV! zD?VXb0@gflFJ7$p*=ZTeH}$W87@!y7q)To;mc&Hg^n9@j+`1_}C8K`@#Dsue`7-J- zby~vIY4M*$ggNVZJ@&0oUlI!->OE!>>C*#B_m=6!T=u}ZcED2Ut?z$>KceCFds&m(T#YZo-#uJZaRX#Y3Gu@6uyCv5b7z2UoZmSKMwXWaRJeIs?^0Q8s;`ZzzC-oW2ixZBN9PSG+1zah#3y+CJ#2kktud z29bpZG$)HF)&m;SQHF+4ypHBmfNfS1%&kpX`we# zuJ0FYv*I25&ldz^ION77(|hxAPn|z*$ZaIy~=*2le-$G=ns? zMkv(IJ}(#ySv<-nxoKtC?A@u zXg8m>>T8jSQLltDW{)YGdFGss_=;jgP3%#&K95JGw9+==f@HsIZ2tiC& zdj=Lh^sSyhwg{y#E(M?Mts*D`Mu_WZr>mn!sNNChG7}*zSZ_y_>ji}6IF4XpFqaTH zvnFc^-hy2N#gGwN%$*hZ!hwSD7>yr~{ZTE(2TU)85eE!3M3Z1`0uvSLZ9jD!yh{w5 z+d#T19!;b&*vvt*$1kLp!xLLf#NiTZS7?ur3nbF_n&_!X`0|txkGbsun%}%~fx2P0 z-&&Ga+(UAK!KND}b(=ayd0APZgvrO{ba|5HW6Lv^2=fl}WYZ5!c06GG85A%mU{JuI zfI$I+0tN*P3jD98z+HnQrJ&ie9ATqG3=(m1Hu4jI7;NW?a2$ZXth=kL-%c|jDZ%qm zO5c3arBoGpEFOliyKy#bfuUoK!wC%ebX8lN2C?B4=SisZiW97EDC1v9B5B#gB=)!`1?8#-VZ#7=@OV@e|AgQz&Egv=!B%$ z#4HO6$@Y*7A?o1Z19oA}^<>0U0u9{^!>+es(XmOf@mc1ts!RmFMfb${w-cABAG1tfNtt@Q=NP?A@1?ldTOv{Buwl&3 zLiu&;5_DsJ14(C_O}?uPE=O*qHPn6tzYa z1=)e#rMgj_`wUGho>@i&i-mny;c-Nn{q)jEd!t`z9QP25*b-XEymEnml>Ucp_v+Vz zC?3IJ_2B2mBcOMUKZ61W1q=!p6fh`YP{5#oK>>pT1_cZX7!)ulU{K(zq5#El_)6mu zJcyd>b_79hzxMK~$U%<^xj?<7{$2gE`jOhFexSaizN!91eN}x)?NQs+(`u92pw@p? ztv2#CC}2>)pnyREg8~Kx3V((}QO(oHJ|D9P8Zqx6WB&?e~;*!F;R!q;+iC@^M@4R>Fr4A0AYcmXoq9-LBu% z6RIb_MYC-4=J>yCicDLMQWnj*ea=Gb`~{1w^B2#XH^FL(Bq!gf(M9ihvt63AX|3IQ z|Ez_x@0hi4Y)s5JdNC(0C*5~3HadD-K)%cE*{NA+?CHz8XwH&F^h#=GMhjlQZv_64cDZs^p)m)DJNxRw)M{Y7R;v{;2`S>IPdbyfHPj1 zm9pIbO}@B1b3;aYX3FwxZ@Vdj^t9FRJnA75U09!H-w<#iEqm$Ob<5x+$NwpE)U_^K zFLo*ykeX#r5x%;tx1xg=&Aoe$_0~DJ&00Kfk#+3s#S0hCnZIZ$UA=G7th?{wSJtgv zeyOkAvvBU+vlc#Ry=%^c%pX+g+ZHUGGxzrSbZxAsbH`a1&be(4ikv;?J}X_z=BG{L zY;xGpkwb$X^zx{EW6B!hPsU1r-t^ujzT`CLl6S8qeCW`jL4`rglYE1L9Keg77ye6p z_&Kb0DIM?itK?%F;pfKpd-=Mo6!K5!-^DqZIVtH&v*C}+?tgiC+KLrvsq4~nHtD?| zE4X=eM&^ce`|_3crE4?OGID&me>3>vF>~0^n9)J&ynK;v&(6-wSh^O@iHJS?e_Hx; ziT_QSSimg)mjutOGKCKvJvylH3g#LA`~GQj00;Tsv5x&`Gg2l`HgHhr>)X}#P5rxD z=a4kN=RDJ(K5<=)PkK8(6&_8q?Hf|CnM9BJ{R->f^?vs}eeZWgbpDb-;X_A`3@X^b zo$q(sFBuxp{eCyTn*RTk5q|M)=f!z~HpMzqeAa&VqHKZfad!XxivPa&o541=kie}d zZ&Serj0g2+74KbI8#vAEu`oDn=(wRl*iGB)uW~Z*o%=FDWF`iS{MY%RB)9;{{$delmLc%?>)wW>4 zyg9S@0QNt|XgFWTkhj4rtN@oQ08#j-%LPXM=DNORX{+BQ{{oSURf2xNE;U7+p!`kw zwo>pT1_cZX7!)ulU{JuIfI$I+0tN*P3j80UKtd#ZUIqsT!)!mOJY-?e zOS+Lf;wZrxMtoAV&y9m{XGb1I;v?`H-b1*iNz%vCxAnJRM?Wy5@7ItWLAQDE=lT~=>mWC% zR7=z?>T~KwHA_ubSE!Gx52=gP`RX02O}#~(qDHGx>L~RF^=kEU)uM)}lJalLpOil; zZsmRD*UB%H*OebA-&MMmHszFZLOH4&Q1&Q0l&wmUvRQdXS+A^Bo=}!6k10!(g~~kT zHYHg}QsR|K%2;KDa-DLOGFUMwgXI5||0e%G`S-F@eoy|T{4@DS^2_qK6n-oFT80m&uRF_sjRlcgnZQGvujqtUN)!Nw&(@$XCjW93e}V zzroX@+wz{}Ez9ecmn|<^+AOV>M#}+9jiu63WXZK`w5+wPvOI2CV!6k1hb7rE)e>zP zYq46cwhXo)yqNiK=8w&8^LysE%&(hYHos_YGq;)>%?Hdi=1OysIoG_=yw<$R{J43E z`5yBf=4A6!bF_J^*=oKT#5ev73K$eHC}2>)pnyREg8~Kx{%I5l4}rdHx75wJOX^~L zLAt=WQ|e^gA$2frm)aS(No|bJOXnG%lg=?dE1hM0Mmodzv~-&BDd`mBlhR4Xtx_xF z7O91Cv((JENorzzLOQ|t1?dIG$ED+pk4eWEH%g6+8>9xtN2Q~Tk4Q%tAC?X?J|rDt zd{8>bxL&Gfd_X$Dc)zrt@jhuEe#&uF1<65bf@g8Xp zqrv(FNI<{5%d zKTYtdrwDG^L~!Fqf*UpvT)&>+x^)C|atLN;6U@pY_~erW*RCa)nMp7sgW#Gq1k=+A zu3k;>i6;oAr4d}Uir~tX1XrveXtxtwzMNocD#2yT2&SYEeEe~OOP3OS>@k9mK1%SB zM+iRrFu{i&BKY8g1eYu!_`m}M@4uhm;>83PEh2c|eFPUSBzW(=1n;?r;DQAN=g%j2 z_uT~N%_Df%T?FsEli=LB1n;CFgcmv%$Wpd z%pf>@I>B3RA$ar61e1~oPMbz>>QsV>i3F!iA()UrFg~7OTpYpJSb{Mz1f!z~PM%C~ z(j{x);K-2#M~onNKjD8Z|)A~<9S!7HyMc*PY2FTb4NWtS2B zO8pB=rpeYT7npipnsd$O`%ehDz(PrVSAARknfhZtKLf9*-%-2N^Xf_U1@(x!U)`;4 zSIgBx)uBGEu2VDBG&NOyRDD3bSG`M}qs~;Psd4H=b&PtWdaXJ{Rnjq0qpVa?l!ukY$^vDs zGFzFhBq}k=cxALQTp6ZZp~yMvlkb*qmuJa0%L(#id7L~_zFrF*h<~JcDc*Xo}bEo-?xygLgydP46DszcB z&-{#eojJq25^{nE%?tl&i=a`kK>>pT1_cZX7!)ulU{JuIz?VRQu#li&o762(mp~T; z>J+F$pmu@U1UfI!If2d!bVi`l0-X}*q(H3#wFuNKP?JC>1bRWB;{qKMs8OH>fsP7v zM4-b09TMoEK=lG05NN+Z`vlr6P@O=v0__oKw?H)lRSUFBpq&Ek5NNwV+XSi-s8XN` zfwl@%E>M|3r2>@*R4h=DK!pMo2$V0-7J>2v+ANSmAQlbSSTtZ`(SVIb12z^7*jO}R zW6^+(MFTb#4cJ&TU}MpMjYR`C77f@~G+<-VfQ>~1HWm%oSTtZ`(SVIb12z^7*jO}R zW6^+(MFTb#4cJ&TU}MpMjYR`C77f@~G+<-VfQ>~1HWm%oSTtZ`(SVIb12z^7*jO}R zW6^+(MFTb#4cJ&TU}MpMjYR`C77f@~G+<-VfQ>~1HWm%oSTtZ`(SVIb12z^7*jO}R zW6^+(MFTb#4cJ&TU}MpMjYR`C77f_^L<2S!4cJ&TU}MpMjYR{=EE-5=(Lgeb29jAc zkj$ciWEKr1vuGfhMFYt!8c1f*Kr)L4l36s6%%Xv077Zk`XdszI1Ia8JNM_MMGK&V1 zSu~K$qJd-<4J5N@Aelu2$t)U3X3;=0iw2TeG?2`qfn*j9B(rEBnMDK1EE-5=(Lgeb z29jAckj$cinJgNZ$)bUoEE<@}qJfz#8kotVftf5Cn8~7nnS)6{FjHlyFq9cu2+lAw zGz|)g42mQWK_Hw!7~Po~N-$~=!Eqr3$FP5aF_`cI;uL=T%#gNg=lmdF#3{V?^Wqd< z`z1`6MivGI3xnj z#QiQI(3cUXuw#daW9CmAXhz2;#rrg)K|x$H;uQY3<1u=J62|%z+`X5KIEDUN^&EfE zh*KClWnwg9c7huJH{%pOM9Yv@U*Nl!O>~bLJ?TO6320Vdp8s z-%x*~z7jes^qN6y2Hh62Gvpq6%=j}XU{K)GC@}kqa4BfW;)pmCc~ki!!J75Gf_L$X zjde`A9Hv6UHzVM4B)qDm!DUWHj&(~{YMuNRr_jkIQ*YM+ggNf3Ztgo+gMi4|`BU!ZR;_C% zUKY7H9S60;o_CzPTD6L*zFdb^ep;(&adkGhI-9hXb`coYzrvolzN0UrRwH;UyW$ja zfd@PmJ=|)k)TYmkzKp>T$eA3|*%4*(wm9@_-C_z5JvWkCoDvZi`h{Ao z$D8igZpz9LMTVfmH)BK&`dYV}VpPu!_q2P^XSI7^9PEDWrU=-@cav4>@NJJq#D(bX zc4NY8D{^jb)Q)t0bivWL_aNpx5#^df#5Wz&nksucPtf#&XDV8HJC9(3a`)_Z9ysp{ zGhJEL+qnymxf>4k6_)rPs6V=}$7Gsejf#3dw@#~Tbk&}5H*7=fYe22*U~gL!a_((A z?A%)~Zn?YLsoeI{&Ry-=wldsxH5K=EY<2Fc^S=d-em{3_RFugyapFXiDJtsY{oCrT z&fOi(ox8P?W6oj@aNkDCTx)T|d&@&Z-*^09;zi56}x&?-)i*J``bIIf^` z?-8x!fb;xGl~+?)aP6!_vC1+E70&e6 zDh~H{bT~^&2GY>oo#)!$gxApqe@bcx#u2Oid3rJB!QRd0o7I7o!bau~@E{v$z`-7xWYr6D<&<#yTo>l-1eV=I*KM z?L6eJZ|v6t#JiOj=8Z~XrG~*N&Xom|iG5ODDe3VT{NDif`aRFvCni^>e^ox8y za3&g%Lo8MTvYzLaAXuLfzg~MYuXljhn0X=8+?5e=!PHzLPf?ZV3azz*W{I|9A&3Lf zojY5#mJ3?`5z%9M)4jRHOmU2ZIe#QK{p;LvxJrnPnI9A`MGjdL5l0R8?_?ZEPR#~r znGVo272r5I4`?JGgGVL;92^I*Z#2N(;Q)2R0BWxQco_e5xj?XXZQI-hOWq(mf?Ji< zL=>&R89R2EDGLGUsJ!YeoO6C&#EozG4+tTSFKj7)KWEHeO}$9W~*z|mGD#e zu)0`Xpw3litJBp)HAWq;j#h`O!_+HOSq)c%l)o!~Rz6btln<15lsA>1D6c9nDLqQN za$0Fp8kBmaR@tdkD8)*i@~pB!c@h;d{tOBj6fh`YP{5#oK>>pT1_cZX7!)ulU{K(j zP65(hAIw$+gV~BeWd{Q)TM?*iMWC`3fx=b<3R@8$+Fk2D?hme6ma7ajGB$( z^BeUyC}2>)pnyREg8~Kx3=VylJMtm`zbL?ZO z660=uHYao7t+*+ko6p`dIXh=lx_!ni*{NA+Yjb9dU9m1Bm4XeA9rv_1X$1*p;o^Gx zSZeUv2`GNt(`hTl-pD`4W!ZDqWo6vVKPDjzbJnK&?CJK@oXo6Q>FHy~JiHv?H790o zNXtoG_2`&!lTgPwDXFV`B`wRz822>5q||h1!pwu>`J|l8l`GTjW5Z+8Sr7P_j zcEqr^&zkGeR%As+p@SY&t}F5Q%n_3v(=R29t^KlqQfT~Q$1Rja%wp8y-}KJcs{)lp zXHliMI*qO!olswMHXVW>lu1M-cT_JAXa@}2xJ z>ih3{1aB|pM0I;Rwo%Pk%?#D;?I{qC+eZS>S`=?HFWmXw+HpJB#oJp6dQf7 zV=A{}!GEnCK@EW|h>nKE?>sq?}P*WnJ&fattPFdViIm{^M3-gz*DXkUclgjkA zG8E^kg!*tvX59bAQk)ff>h7O-e|d?}(<16R3;-o{m1w%vicMJtd=m z<~w&g>x+=ByZpRXU*oJRau=Tp`oVw<;}eq|lPSY5Ivf5tFpC&i8^==D1 zS-kC^1&oWPCB>mrM_o!5zwDO<1|4WI@=~(+)qpIb!JQ*6C5vD8%OWu;2Az82rDX9= zzbvL8=2x=AdMR1F+b@d*>|2r@!!IR^-{@I*Ee_*AiyJN_i}wa*0a{#tDOvn>U>0b| zb(fOG`xnjP+DpmegMnFqwXV68EP5}R#js1sLK~PxY_j9(OUc4HAdA@8C61w#MGUQY zUvxDexX1>%0<)$_b#V(FS8>*~W&5I8W1~QebXOhJ)`R}gFVk4gbjYP=TG|b>8nF=w z`j?AldgY~OT33KA4eY;M4J|?cIxtgE_KGiqvi}&6HFX-<{{DA6EjYAa*4RT%dC+kg zWgSQSaOt})P{iGQ*xgbPZ1&H4YEm4cMUs`lDSx@ZkbexW-SyDFG?82&Ecj{(5%r!6 z8yWg;@V)q9{QcjiK+^2+#MxxQGd&`1FkA5WjpEO7JJb+t0}~H_?e>T`RkzmZXSpei z)jY=TCEK;`>b|}E$kMHl?7Uze*6VfD11m4BpbV@n4262zYhYuqmG8vi6~_u#9lDyU zam0WLoYT=t|HIskttasc?)DvT)DEA)T|6v|$HeonLvyy}(?zy)gh^m8&Uhsy0j1z* z0dYc8Cyei0^(WkUT#brD?v`BGlaaL;tWDc%M1rz{DN{?ixPpW*c!Q0wtp-{A3$CE))l3n0^YQTdlRg zzoFC!?j>Bro5J{1C&bP(s0^DhqU6APA``v`vs*H6Jh_?V6?>03OABB$hk{|=Nrr%A z5!*o~csSn{og<>K39LsJ)V$BRy&O^walG@G;f@?R(&{>Kf&?KaH{;ajt1;Jz8eQ4s zJlKL>2IXNC>2~bEArmbo6J9}7(JZ_Sy7jhgb@H2{cZu*@Dtg;tdf5icSY!cPNu0Wg z2?`IOE#f-a+d|R<{@w;|-b>tHbd1c#1fv%fkil+usk^7vU0>$h+K96+lbei)s}FtD zR$&!wbZ^gdl#n_>~t*9bD5^cF49tzs%$sK~3GBODT>p^V9oKQ|ihCO+% zgROWI@AU0xcGW_b1K+x)iPwjJcBD=4#pnD^p*kYg(wmMSI=#J75?~%#?o`ZN|hy zxv~6=hHeWaF-4dK@h~oe!}@j~#>Ddhtm+T%ad)?4Mi5&ny5gOgw89GUjDJI6=8iU@ zAZJMzO-{KDu%~s_bZZCq!AzNkWgBQ(fc_9QhXHYKcP%W1U8lNvj(3pnklHh-t-G=d z2IW-ewq_W^qrw9VXM&_8Syk&Y8oVrF_6I>&vc4sb|YFF-~rKu1O z18P8I*hdqQSt{tUchB7lR-qwWd!{e9LhHhW`Z)bZ_CPtGfq= zxZGMV7jXAd3q4gUs&>7w%VU|%l|ma}5H7d|X`)44vEW&ZY6y*8#~rQ%XQ(@l?sk^e z2G-~>ttMhNh9P`)v$rmy77%w5FJkOh!tFsTMv#{r)J`%yuWlBm>g3JM z&#GsB-l*@}y-&m>HZGx)L`=mVsa&&=j&;URM+7=h*=c&Z1dV^sjq3lbU|L7_F|(*0&_rYJoLLXb#|f&@LSS_hI=ZB`3saJ?&Hmm^WbT0 zg~b}F9jWHM6vPG2rX%jNw8Nqv@Y@z)bLT$)|JnNzz$mJ0?VE%o-L-hRuDA~FjH3=}x;t^j`QMvy z-n{?Kf6lqxRozKk=KVV3yjM}qcluVc88S`R>sN` zo@0wF`ps|!ojwGF*dPSt0#lQ_b`4Y&PAL~wKn_~1-=Kg{Aus~re75NTElbNm5-`FB z_6goOxE%P>3^IbT((DVh03!*c170ThI{`SUfPpli4f2r*qU_Gg)g9vEJ}~%-4k#|_ z$|@m~=#ItT89Cz{*JzoHqsgfijrFC$kwF6h8SuRMml$vdgh5I)()iu5@Ot4D2aC$4 za(Ej;Co$+=WKb(-ENhR!(j2E#7zC>~S-RVO6$2~8WAN`3zU=z#1uicL|G!OtdiyGd z`TkCe9Zl~kGQ(7Zj#}4cd%sFeCFxEF~7i5f@`k zAA&cxNMW|CzzO%kgE@tHsq_dq2rbB;kv}_UmaFSRcp|?rWp-vkfh%o>vw(6Ih0c#r zFcUy9B%A)R_BdO-wJ%!BM0H)*rUdE6UB_zbg*rF43 z`3xQ$+)@1Z4sO+ACMAu}z;-YxX{ZmKDL%$Jzz)}@!^PIkksfQSfwd5xKl(P>m1|yZ zPF}%`0_e^^*sj35AP&Cm_m8s1$Hm9^<##8x(-sZf5!|T##rlcTx5mcDMrrN3t@MYs zOrMMQCrTf>gDpDFU;Z}w6$#3g5ZHcvvMaf#9krj{#gofALi_3KZxg)zL?^_>2eu!7 zKMNdk{7?J?T2WRe3_d}_)AzI_XKvhChv&93Fl;^;nYGLS9}u8ceu_NAC&a`jM1P+U zhR>n-Sf8POq9$O6NvVGntO{&D(37yOwCG9rY;lh;Skm0uaX@iTT+-a(?-saj@$(wC zH4a-QB@NCSoMG|Zgkla{itu0zMb3|daYApi$HOce<6C<15BW}p*mjbe=HRF2rKO(o z<=g}SzCz-b&SP-e!g_A-0A@NE6?|u|D*_6i9R&ecu%v)sG?X~7$6ay)KVy>FA&&;_ z59li^d}0;2Cw_R)XYO?z?1%76G+guSszA#)vQ+|O1mB0)3b3m{n;5u(tFgWHhruQe z%pDGU29sXh%yLV)d!=lHJXBCtf*1?MI;o&`I*Qtzr|jpxDf|1QXL zeL3X0d2J~_s(_agjPSs6#=RT9rtYH8zz5c#E!5eQ-ueT;cvl|<#=HD1S;3Uz2^VOx zQjBMUfHfw3vWIih26jwE9`V znoF};oC=xL8pm{$NRPz#l}%|0Bu!#Ca~pj{sb6z1RUvA>^obG zpTot?uAs8GUI5Ny&_lUzk}DzMZXxogG!q6urfs% zr;JkWR+5!OC0gmP^isMiw!$*1L7 z`LMiK-YIXE*UIH`iCiQaL zlfIR%NlnsK>56n&x+Fa<)k(F|L8(&OELDJ?g(7L5lq+ROk4RIbvC;@BS&EmUq+XIm zx>0H`!Dl}4Tk)FMBwiJ-h?m7n;?rWCSSucc-5Hz33b9ly66cAzVutvLI7J*Qju4Z@ zcri-sC0fKA#rC2}WWu+?HK9qkDqImR3zvkag*u^DI4D#Kn}rIYR45YWfp>=t;Sphq zFjg2LBn$CEl+a7C2saAt1(U$c-&&(0gXT)}W^;wP6e`&G zGbCV0z>t6;0Yd_Y1b#6Rhztu2W!Km>!mruagkQ0*2)|@s5`MwHApD$tPWT!7jIfzC z6E?9XLND_YdYFgM&D?~Itda0j_9@{f>=VL|*~f%e*;T?nus;xf#6BYYkbOw_0sDaP z_w4tC@3Z#_f5(1Dc!gade2=|H_%3^w@E!IJ;oIzO!nfF4gm1Dp3EyCE5ME}N34hCe zOZYl_o$xjG8sV$#Rl--;D}*nzmkD2DFA-j1mk3{EFA~1MULbs)Jx}-?dyeo~_AKEu z>>0w}u-_0q&7LN_$Sx9IU>69_v-5=K*g3)m)<9U#>Iu)XvxIf5j_?dSLwK5niO#10W2WCsZk zumgnq*?z)(Y#-rXwwG`Z+e5gU?Ix^ZRfLtSl5iK>MYxmgB;3Jv5N>DN3AeFrgj?BG z!YynI;byj(a1+}^xRGrn+`u*vu4n5B*RgekYuQ@DHEa!G1*;%j%~lhxVyg&OvXzA8 ztekKKTR~XH$_SUU<%FfIlyDhaMp(j12$!;@giF{G!o_Sc;Ucz(u$UDS7O^7qiudG` zNS}BD>B5CbfBkEu3l<=K{Bfl7=Odjr59!>wNaxH!T3Cp*pa5xpKGM8Aq>nv@G&dJ% zP7c!SY^1YiBhAV}I%^ivM;}F+nTa$b1L@3}NYm4ix?D)p(vYU6B27s_>U1J~C~x6A9@JsgAXE|G6m`6$w((nLOO9G(g_ogjvtS7+&H8UJb-lU zSfuygk95o!q@zb89W@H+efJ@~_gXAieu;q{D|JHT(|#0{sq-!pYFD z7kF-o<*gZ+b^n*^1^&X+&*ggQLwF~C7EUZYC%++|Qval0YjJAfX|+~83?2%1s+-ld zYPnjX7O9V`1!}gMp{A(Q)Jf`C^L4{QiCiMnY zQB6v$5~cJ}x+}LSH!9aDk`kdX`760e{zU#jeph})eqO#P*T{$DJ@O8Dle|V=AupAm zl;_L&@@#pg?3Aa<6XpBmk@8SENsg1Pa$mWJ+(qszUoXpYg!DJi4SXYgCVeWsE4?J0 zlTJz1(gDy6Y?0PVE2JgTLTQea1G)jH^pG@O8U;QX2T5_D9q27}m2Q@9kYp)B{2S;8 zz7anYKNUX^-w|IIUjz-oS@F2|l(<*iA#M~`gN~qBd|b>IvqYEpus9jC1S3Uh#>((0)_+(2^bPE zB=FCZKtxzbsDoYO@HL08IDE4m{hb9hQ4jv9}4vic>>NdgbpekLI)KNp@RyCSnz6c7oxWj zy_M)KL~kZ~6Vc8@ZzS4@=&y)&BzgnU>xo`Rv;)!hL{+4N6{0d$wS?>-k*Gk_Ow>d) zl4u0caBMJiOkq+O1|<2@`5n0i26D*VB`&0K7rX?B=< znU%;>k^3VzMV3a+k9;(8YUHR$TV%J$8zRFa{uJSjxDs(8q9&p;Vok){i0KhyBBCRD zgg+LZqW)REs=ljUR8Ofp)ODaK7_JUft!hv8R`q(-to%*+cja^CW92>NHRW05tWu-w zQ?@B^_)d`L-U-TftY} z4F35>@XyP^KYtti^Y!4LuLb{nRZsJk;43c&|9mO<=cVAEF9!d7A^7L>!9Sl1{`st) zg=cj3H#+;Y&R*2n3p#sVXV2+ugU;6L>{*?y)7djRds=5t>Fi0JJ)yJ5b@rIf*6M7H z&Q|N}QJp=avrp;lVVymsvj=tdfX?pM*?qx+-K$^O6MSX2ex)k-N~L~fm(K1CzG;Vk zWqa_IZTgk1!B@8ES2pYHCY{}=vm11Fz0R)F*|j>mMrSK@cD2r~(%F?dTduP!t_$lK zn&(Q-bLD5uC;-zy1ez(QvK?()>ljPt4nouiOw$8*+n{Atg}VtuudT~s6Hu& zbqUc1-zT*2g&Oy3ja#5`k89j~jhm-&b2V;`#uaK@fyU))T%N`~rg6C%m!omn8aG?x zvNUd%#yzTWnHra&aWge8UE^FDm!@&49m0Bs1XWv#cFn1Ak7(SCR`)-wU7N0P(==|X z#yzBQ4{F>LDeP8AeI^{8<;t6^1y9nri5fRS;IE{Ni@F@xRDw+LgVhyxVtrOxW*0BxS<;7(6}KQm#lGvHExi`C28DU8aGhm5;ZPC`HO{VaHjT4tT$IKQ(765@*H7c_)VRJH*GJ=eYg{jl>#1>fXj~7C>#lL# zG_I@0-L7#Kjq9Ruw`m-_VEOv}EgE;T#@(cGoi*-8jq9XwztXsl8h3-nU9WN1XtyE3PR35c z>Rj_LuyGbCV0z>t6;0Yd_Y1Plon z5-=oSNZ^+vf&AiJ@RioB%YY6^T{_-ucUbKS(e~(Qt2H`7Q-u(dnv*@pm6z{C&#~6n zAtS+KKAcU&&yI#2((!P73T&Xa+T!|ykGcNV?3^@Leqm}VoZQ;RhUEO>oS!7sgdd%% z9gZHJRGf{ecIw!nN6WI(>kS^a6C3N@0*6#=fKwyfWxIYvalkR2byD%{pFCol4gA4P zD$e@pBgV6+Cl$~7$s>kir=wyg6+imZM;wzF6H|O|aVGfjK z%Q+yXf0U)yWbiSWGdJHdW`e~EZFQgp0%D^FTIR(@_qGhl&CPO6cBR~xSuh~R9@pO< zYw300hzX-d-3jN^&vIGraiz}6>1`R|a;D*_#6vUSY@Sz$-OAdC#7|+&un)QJDoUOL|V;Y-fSR&e}8B z@0M(_FcyaA6{4>~JjWmu{tbhF?cksPkK*(HXd5Ap{&QA@!^JT58@%OS^{=o$;T!Xp z<_(eGMD7Sb9zHJYiLgZW8QaKK!HJ3OBPgPAYDA~LU0}0KcA9Hm=DZo8iYA}W`7<)p zG`{`Nal?m97;YIkX4vp4mKJwe?jK{(?zHsEOzXX5v=Z5=OP7$+%mQahmP@;+F{&@V z2}2yCh6g?@m^0HLl?lV|89vT3=KcwmF%w5YV-w6c8bOZQyylN~M#21Cmu1qBaYIK8 z8Q05d?Trr>WEN!Ufwriq-a+xgxSX1oNglv06NXQjfRDi7UVz@RLBp%X76T9Bwe-v^ z*NnovtX3CNb6`T4kvZErQ%}wRP=Rx1zUAKW_m9CCz;X7Y;M6-H0*aW9Y;PF|Ju^Ei z$C;MzFE>VjJufI=PNoYwuup1uM`r$v+`<%ao*Nj54!jHVwG`qh)zmzfll$f~D8ciQ~o%A2VSFhK`>wWb{~iq%b$_?|o$KxRIlWjC;^>-|z>i zePAB%zJJ{Ck@t+juwK5(?QI!1{O;k9cEf|(hcMH88azv+YokAY;w^7$T=WJ}B zgi6b{>A#BYl6Z<`@dHw1r%s(hmW5E8qzgpkptk6H;J?)lf1s)TJr(cwx2!iC?*CT* zomQAftB=4cF32fxX3fY4Ki#gt;Iz#2^vu-4tb+Mk?b~>3o|T<5H_Mea(={VECo{W1 zkNsbxT|6vCbh7pcDfG7soP%<*XXJv@Vr(9PKc$wfB=8~*EYMg2gLs?C5F$JE=n=B) zdTKL)*8_Pms09UHvGn?9H>4aIY%=G-h~XcAX1V6KtZuD^WCq;l8wR!K+G=qylezLT zomqP98L$GFo0B(7d+cAMDUCA+PlK*`uDsODd>0>Pe7w-I5e44Gha2CWy!qU2qG_A%f5+JLo#R96@hRU`P>`7&FwgyK)cd5!h)(@( z51H*BLY&z!h0}Q;FevChOf73(zvLS{^Z;J>w?#yDy8ZT$l1Qq1{i@DI2GzbE{_|8n z#vfeu2~_jnu>eZXoT>HTf{eo1DcR1<){CANYYHCYzedGB6duuOK(~;`{1u-HtKNLq zTqi6hp+*CKMYOE?fNQ?G4+!CvKc!t{r*7RsO6F4K2V4$d!-J|n;KI*S|KHKW+g^5l z_o7&96yLYrZL?XRz67)-feVwMb@9JOvvGt4FGXpY3eAA9UCXB8zlw7M@zgxVg+_Gh z-6cQnuZ z)Qp@QecZA7-on~!wRt%KFM4q_xWS7MKeox@xc~l9!-vod*gveH!TYrpPa(M80^~Xc z$Yl7Z=>^9B^_k8${%zwJv=>kX7XyC*J(Wwa&p!fu_hrfb!3*E7C6l;b8~|4HF5zZ; z*7!3dU`W7_fFS`x0)_+(2^bRipDTg;+M7qTH?xo=SvJ}24)=-+O^d4FV0!T6&Aobq zlWg$V-Qt5Xd>!;1h85)WI#q?3CU)*ZF$Fjz*N*u1oY(-4w&8GX?kF7G!h`ef#`^W# zb$nnVad^6?HLgWaOk#9=s`67NVoii3=u}Z&5=a+5FWXXOJ(1Q9(Hrab;W_k0s~XRi za<}N;Tau{g#AyAzTJRAb2M4+)+T!vgNF?7UuDWV;3txQ!PHp90+CMESYg)Oysr0P7 zXqUJ8=%+<1Ky=($yLVL$xN-mfG~p;l-+^-ef<@U9ZP9t+&y{8@d@)Xpwg(Cmg0LR* zgr6nNI179UwZZ>ZElq!r-FBB5((K%s8Z|%lmWwO!Ryh6*PCM151W!|tHQ_GPk9pc2 zpBU{sJ{4q4PeV{l^j(oZ>S>UU-YWRh&@gNdMZnV&dCvK15OC~3V84X!2rb0DdLNuu z0M6&zDwhq!9vfU#2(raK91d|iJCam>FmCYP?>)L6QUG0LMvKUzp|aZudZZo3|NVIf zr~2ONBJZxFTKsL6z@8Z0vIJmI5<5K%Qnd_&CALHCB-9lm+pX5 zECO>2Ruj+{f(k%EF)^8;5dBPweh?htp>;`j?P?sU>X!47iVtYKGxCEI^rY&JYDq=o zY1J`ce4r=g53*UaLLjNxJgLB9xYxkJ3d=F&le-{R-zal#gL~;d&-NlXAKSa+^mnHg z4Wn0!)F8-)8iFY}i}~#Zc4g<-{^*W7j{XHy_23VlM}W^7e})7M2^bPEBw$Fukbofp zLjr~b3<($#FeG3|z>vVtB?0si*q*!&s^oQ0A+Lk7jNuZw9Tds!pg?X1&E$5_L~aKo z$?aeSxg87-$NSrn-@!2QI~eNsJNO{x+^-k7@3XSGMH~NgmFoo#FtG3cclB$vS^ZS~ zP<>B*Lw!Yk9{dNMRZpl#!6#wW&sAz8UPA(g1Plon5-=oSNWhSQApt`Ih6D@=7!oif zU`PNYfUoloQVTdpE#M%vfP>Tm4pIv^NG;$XwSa@v0uE9OI7ltvAhm#l)B+At3phwE zFetQLScrKLr0@{R*{>JyTzcj4$}b{M~?pDZ3r8Sw;6wi1Plp$mjs4hAIU;GPBhsC+%@I?5k`}mb&=oL&1Ng^f1Qbi zbXf$3muA>K1wMP7*#(|Wm0)h!baAI=)joG=t!2nai)Z^vcUjff=gPry%Co1)vumIC z{2Fh=c2DU>@4=PsidA6M*i^FtOumH1^Oaz3*u1H_dG}_pMRK1x-c(%+PI};B9*d`V zw|lSe8PCRA_v$sxMaAxwC)}%RybTAu4M(}3B8V$6!#?}E9v!gMV8lf(H@JOXtJ|Uy ziu<&ua&2zb=N=$kEL0-BR#o;CJHmv4_Xsj;@EMMy~ zxl2}hj#T~VGLh9^57k#D6V_x~Wg>gQ;y#MMOp(9nGNCPE57}3y$nPu@S}_*)k^E(f z_$8N#Y#@hud>|DsEbhMFVISYoHi9{!_Btl4D-_edK? zRoutqFL%3Nbh*)1bY!Hj-0i-v+^y|VTa+6uQJ0U7z;dshZnB4IC(Z$IeWcCzWfTChO{>OQ}_vF<3u+*r5Qv!jY%YPxV1(>;5_ zv+=BZZ8==^9$ns8zsj?5d*CH#(VrCU=+j3K`t|E42z~lo+qJgJ;@MIUrvBW;9L7?+ zy6NOe_nxP~(Hw>!-S2LwZ>p{ahjWlEe7I@Ld9Xyq(rm8LO2Z3*`8r<$S$0?NY~Hf8 zspf*aa*>;xux>gHS9x3@mFA@|g_QCVt}1b_KHk^8?E(~zGU(Z{&s|pOIdcqB_paXs zxx(T5VhPEmkHrF#!MSaFt*3T9UaZ;S*BdYvNCeU;{)s+}6C|;&ZYA)O~Sjb5#Qr)w6v$ ze}bgxYt5eJE1MRdr}=w1lz}p4ql|f$Ux18rdGd_$67XJQ>7Lr%;;F4`y12cuVNX-l z!4@@u&3Abb^h-z#`VAz{)24O|y=P_9{$ea56n*It_qo+jN@yk&D|8`Ccjuj!lG_r3^VN9Y>Xop5z&r}DT=VSQ4$T;vVPoA2Xs^v+p}oHt3J?1;pxMK7*hY7I51r;_ z;(UV#W~ydAKE2``wD+c(V$bGPRDICWU@_KIv=l0&w8VXE5yTMKUc3#98_!lkZS?5_ zw>EFZ{Avc{^za(&HDI|8BhIoC_ugeNFa?fhK0Ej1a2y)%htUj&#ve4KZxw?LW`cJ9 znIKdf5r0{0Gu_sHR2UX_rpX?P#r?Ys`?aF`W3$E<_YO5NbC*T1Ed3>yob3L`ghVoP z$Jr)37Cdm90lbhL9}1)<3CPiSActds9K@Xh`}zUd-5bcx9zb?<0kXX_kZsoknF{|j zy};IXuZ@1Dd;gcwUcjNu#eD>SR=-g{SKaC#)Zc+d;5GF{^*3q*oH$sc9#Z$fxr3Y3 zHR=j=srsZkU(Hu%t20%nI#r#h-mi{ShpI_x9GpnlSM8y8Q9Hw_gt8i`hA4l9lL^08 znw3wL58-seHR1ND65s_%3?UFaIP{-nT2^Y{tO8i5-=oS zNWhSQApt`Ih6D@=7!oifU`XI!S^{JckIw5O+7Crubr2mwG@0mNq{<+oNks1=IuNOx zNHhVd6i+mcXe`kfqR~X{L~TT^NX00k1Bmt~+K=d+MEer$L$o*1UPOBmy@O~EqTPvh zLn?G7dOJ}I(Jn-9BYG>*TZrCF^d_R6iQY)G6VYE0?MU2Exf=pdDEZv?GgwFtQj3Ba4AhvKR;rYZqn?@$Vy;hONqP zFYx?r-9knmxo#fU3(WaJ{{rfEb)#CLmZ?kBC)9aro|>hmtBIl`L-lfK> zQEDHxyLy{?qk5exsSzqu{zLgUt6;0Yd^H0ern3Mrwhf zq!w_HT3`sN1(HcEFqqT=gGeoqL~4P%NG&js)B=g57DynqKs>1h;z%tJOKO1_QVT?r zTEI?f0UN0WtfUr*BDKH(QVaAawLm{o3*1R+fxe^`=tF9O-lP`jMQVYbq!ze?)B-(7 zEzq6R0^LY0(3R8zx071HLTZ67q!zf1)B?AXTHqE^3*1a<1h z+-?rA#-AYpLjr~b+Lpl3BsAfIjS>o(aQ!_SCCKBl)rw}kmb=J|Hw>cDOt)H?@xopK zG*Vo25KZs)E(0q*PsLGB)gCY@4YZL3D@U*a^lo1Pdn?e`ZuJ`6RZv$1R*!J!Jh+VJ zcxW2C5}n6hTl(lS6U-6Oc(}2y9wdPlpAB2k zesXn1Pb?7Y1PY9P?FccrIt4c{XJmex=u0+_7!>C;@ijMubp za{)J>-d5{byrxC6WY_E|LaSJ4C*Twv?5D9>jxGmNR7lEGbfD?vK@fAGuW|2zhUTiX z{3-XcdNhtDPvqYEN+>6!fElqlijez zQ&kEj8y;nmtGwx(&8~QV>izuQM>uMWb$r60%)$5-ol~B7r>*Z^b^k%Dwjx z8rxPM2UqFPMc`m^Pt8hSKiNx{0 zG?;P6fC+B%+Fj6`pa%L_1ObxuncR|TEi_^**D^4KhEZUx_vl7w-C#=#EduVu4$)8s zd4#S;&F(Oly!%{{`@jm`S2g2OeTcFQ#BombjdSx_-C>J5QkaN?*zfw8?D7u`VgWY& zP>L4)6Ef~yRf`)4b{3yI0mCu&h{Y$|OG`m~ z&?s?mJ+Q~U3~J^u)P}dA1o}6co%;t--j189_PLiXC8^<_4j3!>VF!Fz#iCFaAcL41 zZD=9+HbB3FdhXMQixཚ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
  • 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

    jTq+M&F_Wj8+7cI;IBSn|F=w0H`I-V-46 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 z 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

    D}5^t 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 max_size: + return JSONResponse( + status_code=413, + content={"detail": f"请求体过大,最大允许 {max_size // (1024*1024)}MB"}, + ) + return await call_next(request) + + +app.add_middleware(BodySizeLimitMiddleware) + +app.include_router(auth.router) +app.include_router(courseware.router) +app.include_router(animation.router) +app.include_router(exercise.router) +app.include_router(lesson_plan.router) +app.include_router(essay.router) +app.include_router(exam.router) +app.include_router(ai.router) +app.include_router(resource.router) +app.include_router(material.router) +app.include_router(community.router) +app.include_router(classroom.router) +app.include_router(search.router) +app.include_router(admin.router) +app.include_router(mindmap.router) +app.include_router(notification.router) +app.include_router(chat.router) + +if os.path.exists(settings.UPLOAD_DIR): + app.mount("/uploads", StaticFiles(directory=settings.UPLOAD_DIR), name="uploads") + + +@app.get("/api/health") +def health_check(): + return {"status": "ok", "version": settings.APP_VERSION} + + +if __name__ == "__main__": + import uvicorn + uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=settings.DEBUG) diff --git a/backend/models/__init__.py b/backend/models/__init__.py new file mode 100644 index 0000000..702ab8e --- /dev/null +++ b/backend/models/__init__.py @@ -0,0 +1,28 @@ +from .user import User +from .courseware import Courseware, CoursewareShare +from .animation import Animation +from .exercise import Exercise, ExerciseAttempt +from .lesson_plan import LessonPlan +from .essay import EssayGrade +from .exam import Exam +from .resource import Resource, ResourceFavorite +from .material import Material +from .community import Post, Comment, PostFavorite +from .classroom import ClassroomActivity +from .credit import CreditAccount, CreditTransaction +from .audit import AuditLog +from .mindmap import MindMap +from .reset_code import ResetCode +from .notification import Notification +from .chat import ChatConversation, ChatMessage + +__all__ = [ + "User", "Courseware", "CoursewareShare", "Animation", "Exercise", "ExerciseAttempt", + "LessonPlan", "EssayGrade", "Exam", "Resource", "ResourceFavorite", "Material", "Post", "Comment", "PostFavorite", "ClassroomActivity", + "CreditAccount", "CreditTransaction", + "AuditLog", + "MindMap", + "ResetCode", + "Notification", + "ChatConversation", "ChatMessage", +] diff --git a/backend/models/animation.py b/backend/models/animation.py new file mode 100644 index 0000000..09b1418 --- /dev/null +++ b/backend/models/animation.py @@ -0,0 +1,24 @@ +from sqlalchemy import Column, Integer, String, Text, DateTime, JSON, ForeignKey, Enum as SAEnum +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func +from database import Base + + +class Animation(Base): + __tablename__ = "animations" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False) + title = Column(String(200), nullable=False) + anim_type = Column( + SAEnum("math", "physics", "chemistry", "biology", "geography", "general", name="anim_type"), + default="general", + ) + description = Column(Text, default="") + config = Column(JSON, default=dict) + thumbnail = Column(String(500), default="") + status = Column(SAEnum("draft", "published", name="anim_status"), default="draft") + created_at = Column(DateTime, server_default=func.now()) + updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) + + author = relationship("User", back_populates="animations") diff --git a/backend/models/audit.py b/backend/models/audit.py new file mode 100644 index 0000000..364e546 --- /dev/null +++ b/backend/models/audit.py @@ -0,0 +1,21 @@ +from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, Text +from sqlalchemy.sql import func + +from database import Base + + +class AuditLog(Base): + """安全审计日志:记录登录、权限变更、内容发布、敏感操作等。""" + __tablename__ = "audit_logs" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, nullable=True, index=True) + username = Column(String(100), default="") + action = Column(String(64), nullable=False, index=True) + target_type = Column(String(32), default="") + target_id = Column(String(64), default="") + detail = Column(Text, default="") + ip = Column(String(64), default="") + user_agent = Column(String(255), default="") + status = Column(String(16), default="success") # success / failed / denied + created_at = Column(DateTime, server_default=func.now()) diff --git a/backend/models/chat.py b/backend/models/chat.py new file mode 100644 index 0000000..f95873c --- /dev/null +++ b/backend/models/chat.py @@ -0,0 +1,39 @@ +from sqlalchemy import Column, Integer, String, DateTime, JSON, ForeignKey, Text, Boolean +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func +from database import Base + + +class ChatConversation(Base): + __tablename__ = "chat_conversations" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True) + title = Column(String(200), nullable=False, default="新对话") + subject = Column(String(50), default="") + grade = Column(String(50), default="") + pinned = Column(Boolean, default=False) + created_at = Column(DateTime, server_default=func.now()) + updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) + + author = relationship("User", back_populates="chat_conversations") + messages = relationship( + "ChatMessage", + back_populates="conversation", + order_by="ChatMessage.id", + lazy="selectin", + cascade="all, delete-orphan", + ) + + +class ChatMessage(Base): + __tablename__ = "chat_messages" + + id = Column(Integer, primary_key=True, index=True) + conversation_id = Column(Integer, ForeignKey("chat_conversations.id"), nullable=False, index=True) + role = Column(String(20), nullable=False) # user / assistant / system + content = Column(Text, nullable=False) + tokens = Column(Integer, default=0) + created_at = Column(DateTime, server_default=func.now()) + + conversation = relationship("ChatConversation", back_populates="messages") diff --git a/backend/models/classroom.py b/backend/models/classroom.py new file mode 100644 index 0000000..84e6546 --- /dev/null +++ b/backend/models/classroom.py @@ -0,0 +1,20 @@ +from sqlalchemy import Column, DateTime, ForeignKey, Integer, JSON, String, Text, func +from sqlalchemy.orm import relationship +from database import Base + + +class ClassroomActivity(Base): + __tablename__ = "classroom_activities" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False) + title = Column(String(200), nullable=False) + activity_type = Column(String(50), default="poll") + description = Column(Text, default="") + config = Column(JSON, default=dict) + responses = Column(JSON, default=list) + status = Column(String(20), default="draft") + created_at = Column(DateTime, server_default=func.now()) + updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) + + author = relationship("User") diff --git a/backend/models/community.py b/backend/models/community.py new file mode 100644 index 0000000..858ad1b --- /dev/null +++ b/backend/models/community.py @@ -0,0 +1,54 @@ +from sqlalchemy import Column, Integer, String, Text, DateTime, JSON, ForeignKey, UniqueConstraint +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func +from database import Base + + +class Post(Base): + __tablename__ = "posts" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False) + title = Column(String(200), nullable=False) + content = Column(Text, default="") + post_type = Column(String(50), default="discussion") + tags = Column(JSON, default=list) + attachments = Column(JSON, default=list) + views = Column(Integer, default=0) + likes = Column(Integer, default=0) + comments_count = Column(Integer, default=0) + is_pinned = Column(Integer, default=0) + created_at = Column(DateTime, server_default=func.now()) + updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) + + author = relationship("User", back_populates="posts") + comments = relationship("Comment", back_populates="post", lazy="selectin", cascade="all, delete-orphan") + favorites = relationship("PostFavorite", back_populates="post", lazy="selectin", cascade="all, delete-orphan") + + +class Comment(Base): + __tablename__ = "comments" + + id = Column(Integer, primary_key=True, index=True) + post_id = Column(Integer, ForeignKey("posts.id"), nullable=False) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False) + content = Column(Text, nullable=False) + parent_id = Column(Integer, ForeignKey("comments.id"), nullable=True) + likes = Column(Integer, default=0) + created_at = Column(DateTime, server_default=func.now()) + + post = relationship("Post", back_populates="comments") + author = relationship("User") + + +class PostFavorite(Base): + __tablename__ = "post_favorites" + __table_args__ = (UniqueConstraint("user_id", "post_id", name="uq_post_favorite_user_post"),) + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True) + post_id = Column(Integer, ForeignKey("posts.id"), nullable=False, index=True) + created_at = Column(DateTime, server_default=func.now()) + + user = relationship("User") + post = relationship("Post", back_populates="favorites") diff --git a/backend/models/courseware.py b/backend/models/courseware.py new file mode 100644 index 0000000..60dc167 --- /dev/null +++ b/backend/models/courseware.py @@ -0,0 +1,46 @@ +from sqlalchemy import Column, Integer, String, Text, DateTime, JSON, ForeignKey, Enum as SAEnum +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func +from database import Base + + +class Courseware(Base): + __tablename__ = "coursewares" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False) + title = Column(String(200), nullable=False) + subject = Column(String(50), default="") + grade = Column(String(50), default="") + description = Column(Text, default="") + content = Column(JSON, default=list) + cover_image = Column(String(500), default="") + status = Column(SAEnum("draft", "published", "archived", name="cw_status"), default="draft") + version = Column(Integer, default=1) + tags = Column(JSON, default=list) + created_at = Column(DateTime, server_default=func.now()) + updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) + + author = relationship("User", back_populates="coursewares") + + +class CoursewareShare(Base): + __tablename__ = "courseware_shares" + + id = Column(Integer, primary_key=True, index=True) + courseware_id = Column(Integer, ForeignKey("coursewares.id"), nullable=False, index=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True) + token = Column(String(64), unique=True, nullable=False, index=True) + title = Column(String(200), nullable=False) + subject = Column(String(50), default="") + grade = Column(String(50), default="") + description = Column(Text, default="") + content = Column(JSON, default=list) + tags = Column(JSON, default=list) + views = Column(Integer, default=0) + status = Column(String(20), default="active") + created_at = Column(DateTime, server_default=func.now()) + updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) + + courseware = relationship("Courseware") + author = relationship("User") diff --git a/backend/models/credit.py b/backend/models/credit.py new file mode 100644 index 0000000..3492749 --- /dev/null +++ b/backend/models/credit.py @@ -0,0 +1,33 @@ +from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, Text +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func + +from database import Base + + +class CreditAccount(Base): + __tablename__ = "credit_accounts" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("users.id"), unique=True, nullable=False, index=True) + balance = Column(Integer, default=130) + total_granted = Column(Integer, default=130) + total_used = Column(Integer, default=0) + created_at = Column(DateTime, server_default=func.now()) + updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) + + user = relationship("User", back_populates="credit_account") + + +class CreditTransaction(Base): + __tablename__ = "credit_transactions" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True) + amount = Column(Integer, nullable=False) + action = Column(String(50), default="") + description = Column(Text, default="") + balance_after = Column(Integer, default=0) + created_at = Column(DateTime, server_default=func.now()) + + user = relationship("User", back_populates="credit_transactions") diff --git a/backend/models/essay.py b/backend/models/essay.py new file mode 100644 index 0000000..7db0f03 --- /dev/null +++ b/backend/models/essay.py @@ -0,0 +1,21 @@ +from sqlalchemy import Column, Integer, String, Text, DateTime, JSON, ForeignKey +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func +from database import Base + + +class EssayGrade(Base): + __tablename__ = "essay_grades" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True) + title = Column(String(200), nullable=False) + essay_text = Column(Text, default="") + grade_level = Column(String(50), default="初中") + essay_type = Column(String(50), default="记叙文") + total_score = Column(Integer, default=50) + result = Column(JSON, default=dict) + created_at = Column(DateTime, server_default=func.now()) + updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) + + author = relationship("User", back_populates="essay_grades") diff --git a/backend/models/exam.py b/backend/models/exam.py new file mode 100644 index 0000000..62ece23 --- /dev/null +++ b/backend/models/exam.py @@ -0,0 +1,22 @@ +from sqlalchemy import Column, Integer, String, Text, DateTime, JSON, ForeignKey +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func +from database import Base + + +class Exam(Base): + __tablename__ = "exams" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False) + title = Column(String(200), nullable=False) + subject = Column(String(50), default="") + grade = Column(String(50), default="") + questions = Column(JSON, default=list) + answers = Column(JSON, default=list) + duration = Column(Integer, default=90) + total_score = Column(Integer, default=100) + difficulty = Column(String(20), default="medium") + knowledge_points = Column(JSON, default=list) + created_at = Column(DateTime, server_default=func.now()) + updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) diff --git a/backend/models/exercise.py b/backend/models/exercise.py new file mode 100644 index 0000000..0640dd6 --- /dev/null +++ b/backend/models/exercise.py @@ -0,0 +1,44 @@ +from sqlalchemy import Column, Integer, String, Text, DateTime, JSON, ForeignKey, Enum as SAEnum +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func +from database import Base + + +class Exercise(Base): + __tablename__ = "exercises" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False) + title = Column(String(200), nullable=False) + exercise_type = Column( + SAEnum( + "game_snake", "game_match", "game_adventure", + "choice", "fill_blank", "true_false", "drag_sort", "matching", + name="exercise_type", + ), + default="choice", + ) + subject = Column(String(50), default="") + knowledge_points = Column(JSON, default=list) + questions = Column(JSON, default=list) + settings = Column(JSON, default=dict) + created_at = Column(DateTime, server_default=func.now()) + updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) + + author = relationship("User", back_populates="exercises") + attempts = relationship("ExerciseAttempt", back_populates="exercise", lazy="selectin") + + +class ExerciseAttempt(Base): + __tablename__ = "exercise_attempts" + + id = Column(Integer, primary_key=True, index=True) + exercise_id = Column(Integer, ForeignKey("exercises.id"), nullable=False) + student_name = Column(String(50), default="") + answers = Column(JSON, default=list) + score = Column(Integer, default=0) + total = Column(Integer, default=0) + duration = Column(Integer, default=0) + created_at = Column(DateTime, server_default=func.now()) + + exercise = relationship("Exercise", back_populates="attempts") diff --git a/backend/models/lesson_plan.py b/backend/models/lesson_plan.py new file mode 100644 index 0000000..1a26b1b --- /dev/null +++ b/backend/models/lesson_plan.py @@ -0,0 +1,25 @@ +from sqlalchemy import Column, Integer, String, Text, DateTime, JSON, ForeignKey +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func +from database import Base + + +class LessonPlan(Base): + __tablename__ = "lesson_plans" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False) + title = Column(String(200), nullable=False) + subject = Column(String(50), default="") + grade = Column(String(50), default="") + objectives = Column(JSON, default=list) + key_points = Column(JSON, default=list) + difficulties = Column(JSON, default=list) + content = Column(JSON, default=dict) + duration = Column(Integer, default=45) + materials = Column(JSON, default=list) + homework = Column(JSON, default=list) + created_at = Column(DateTime, server_default=func.now()) + updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) + + author = relationship("User", back_populates="lesson_plans") diff --git a/backend/models/material.py b/backend/models/material.py new file mode 100644 index 0000000..c9fc780 --- /dev/null +++ b/backend/models/material.py @@ -0,0 +1,26 @@ +from sqlalchemy import Column, Integer, String, Text, DateTime, JSON, ForeignKey, Enum as SAEnum +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func +from database import Base + + +class Material(Base): + __tablename__ = "materials" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True) + filename = Column(String(255), nullable=False) + title = Column(String(200), nullable=False) + material_type = Column(String(50), default="text") + subject = Column(String(50), default="") + grade = Column(String(50), default="") + tags = Column(JSON, default=list) + summary = Column(Text, default="") + char_count = Column(Integer, default=0) + size = Column(Integer, default=0) + source = Column(String(50), default="upload") + status = Column(SAEnum("active", "archived", name="material_status"), default="active") + created_at = Column(DateTime, server_default=func.now()) + updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) + + author = relationship("User", back_populates="materials") diff --git a/backend/models/mindmap.py b/backend/models/mindmap.py new file mode 100644 index 0000000..3140517 --- /dev/null +++ b/backend/models/mindmap.py @@ -0,0 +1,18 @@ +from sqlalchemy import Column, Integer, String, DateTime, JSON, ForeignKey +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func +from database import Base + + +class MindMap(Base): + __tablename__ = "mind_maps" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True) + title = Column(String(200), nullable=False) + subject = Column(String(50), default="综合") + nodes = Column(JSON, default=list) + created_at = Column(DateTime, server_default=func.now()) + updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) + + author = relationship("User", back_populates="mind_maps") diff --git a/backend/models/notification.py b/backend/models/notification.py new file mode 100644 index 0000000..2e82a27 --- /dev/null +++ b/backend/models/notification.py @@ -0,0 +1,21 @@ +from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func +from database import Base + + +class Notification(Base): + """站内通知:评论/收藏/系统消息。""" + __tablename__ = "notifications" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True) + actor_id = Column(Integer, nullable=True) + ntype = Column(String(32), nullable=False, default="comment") + title = Column(String(200), nullable=False) + content = Column(String(500), default="") + link = Column(String(255), default="") + is_read = Column(Boolean, default=False) + created_at = Column(DateTime, server_default=func.now()) + + user = relationship("User", back_populates="notifications") diff --git a/backend/models/reset_code.py b/backend/models/reset_code.py new file mode 100644 index 0000000..5374f86 --- /dev/null +++ b/backend/models/reset_code.py @@ -0,0 +1,16 @@ +import datetime +from sqlalchemy import Column, Integer, String, DateTime +from sqlalchemy.sql import func +from database import Base + + +class ResetCode(Base): + """密码重置验证码:绑定手机号,5 分钟过期。""" + __tablename__ = "reset_codes" + + id = Column(Integer, primary_key=True, index=True) + phone = Column(String(20), nullable=False, index=True) + code = Column(String(6), nullable=False) + consumed = Column(Integer, default=0) + expires_at = Column(DateTime, nullable=False) + created_at = Column(DateTime, server_default=func.now()) diff --git a/backend/models/resource.py b/backend/models/resource.py new file mode 100644 index 0000000..bddce6b --- /dev/null +++ b/backend/models/resource.py @@ -0,0 +1,46 @@ +from sqlalchemy import Column, Integer, String, Text, DateTime, JSON, ForeignKey, Enum as SAEnum, UniqueConstraint +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func +from database import Base + + +class Resource(Base): + __tablename__ = "resources" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False) + resource_type = Column( + SAEnum("courseware", "animation", "exercise", "lesson_plan", "exam", "other", name="res_type"), + default="other", + ) + title = Column(String(200), nullable=False) + description = Column(Text, default="") + content_ref = Column(String(500), default="") + file_url = Column(String(500), default="") + cover_image = Column(String(500), default="") + tags = Column(JSON, default=list) + subject = Column(String(50), default="") + grade = Column(String(50), default="") + downloads = Column(Integer, default=0) + likes = Column(Integer, default=0) + views = Column(Integer, default=0) + is_public = Column(Integer, default=1) + status = Column(SAEnum("active", "archived", name="res_status"), default="active") + created_at = Column(DateTime, server_default=func.now()) + updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) + + author = relationship("User", back_populates="resources") + favorites = relationship("ResourceFavorite", back_populates="resource", cascade="all, delete-orphan") + + +class ResourceFavorite(Base): + __tablename__ = "resource_favorites" + __table_args__ = (UniqueConstraint("user_id", "resource_id", name="uq_resource_favorite_user_resource"),) + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True) + resource_id = Column(Integer, ForeignKey("resources.id"), nullable=False, index=True) + created_at = Column(DateTime, server_default=func.now()) + + user = relationship("User", back_populates="resource_favorites") + resource = relationship("Resource", back_populates="favorites") diff --git a/backend/models/user.py b/backend/models/user.py new file mode 100644 index 0000000..4cfe5a8 --- /dev/null +++ b/backend/models/user.py @@ -0,0 +1,36 @@ +from sqlalchemy import Column, Integer, String, DateTime, Enum as SAEnum, Boolean +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func +from database import Base + + +class User(Base): + __tablename__ = "users" + + id = Column(Integer, primary_key=True, index=True) + phone = Column(String(20), unique=True, index=True, nullable=False) + password_hash = Column(String(255), nullable=False) + name = Column(String(50), nullable=False) + avatar = Column(String(500), default="") + subject = Column(String(50), default="") + school = Column(String(100), default="") + grade = Column(String(50), default="") + role = Column(SAEnum("teacher", "admin", name="user_role"), default="teacher") + is_active = Column(Boolean, default=True) + created_at = Column(DateTime, server_default=func.now()) + updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) + + coursewares = relationship("Courseware", back_populates="author", lazy="selectin") + animations = relationship("Animation", back_populates="author", lazy="selectin") + exercises = relationship("Exercise", back_populates="author", lazy="selectin") + lesson_plans = relationship("LessonPlan", back_populates="author", lazy="selectin") + essay_grades = relationship("EssayGrade", back_populates="author", lazy="selectin", cascade="all, delete-orphan") + resources = relationship("Resource", back_populates="author", lazy="selectin") + materials = relationship("Material", back_populates="author", lazy="selectin", cascade="all, delete-orphan") + resource_favorites = relationship("ResourceFavorite", back_populates="user", lazy="selectin", cascade="all, delete-orphan") + posts = relationship("Post", back_populates="author", lazy="selectin") + credit_account = relationship("CreditAccount", back_populates="user", uselist=False, lazy="selectin", cascade="all, delete-orphan") + credit_transactions = relationship("CreditTransaction", back_populates="user", lazy="selectin", cascade="all, delete-orphan") + mind_maps = relationship("MindMap", back_populates="author", lazy="selectin", cascade="all, delete-orphan") + notifications = relationship("Notification", back_populates="user", lazy="selectin", cascade="all, delete-orphan") + chat_conversations = relationship("ChatConversation", back_populates="author", lazy="selectin", cascade="all, delete-orphan") diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..1ac34e5 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,22 @@ +fastapi==0.115.6 +uvicorn[standard]==0.34.0 +sqlalchemy==2.0.36 +alembic==1.14.0 +pydantic==2.10.4 +pydantic-settings==2.7.1 +python-jose[cryptography]==3.3.0 +bcrypt==4.0.1 +python-multipart==0.0.20 +httpx==0.28.1 +redis==5.2.1 +celery==5.4.0 +openai==1.58.1 +python-docx==1.1.2 +pillow==11.1.0 +python-pptx==1.0.2 +aiofiles==24.1.0 +python-dotenv==1.0.1 +typing_extensions>=4.13.0 +pypdf==5.1.0 +openpyxl==3.1.5 +slowapi==0.1.9 diff --git a/backend/routers/__init__.py b/backend/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/routers/admin.py b/backend/routers/admin.py new file mode 100644 index 0000000..a374e3d --- /dev/null +++ b/backend/routers/admin.py @@ -0,0 +1,236 @@ +from datetime import datetime, time, timedelta + +from fastapi import APIRouter, Depends, HTTPException, Query, Request +from sqlalchemy import func +from sqlalchemy.orm import Session + +from database import get_db +from models.animation import Animation +from models.courseware import Courseware +from models.credit import CreditAccount, CreditTransaction +from models.exercise import Exercise +from models.lesson_plan import LessonPlan +from models.material import Material +from models.resource import Resource, ResourceFavorite +from models.user import User +from models.audit import AuditLog +from schemas.admin import AdminDashboardOut, AdminGrantCredits, AdminUserOut, AdminUserUpdate +from schemas.resource import ResourceOut, ResourceAuthorOut +from services.auth import get_admin_user +from services.audit import log_action +from services.credits import grant_credits + +router = APIRouter(prefix="/api/admin", tags=["管理后台"]) + + +def _user_with_credits(user: User, db: Session) -> dict: + account = db.query(CreditAccount).filter(CreditAccount.user_id == user.id).first() + data = AdminUserOut.model_validate(user).model_dump() + data["credits"] = account.balance if account else 0 + return data + + +@router.get("/dashboard", response_model=AdminDashboardOut) +def dashboard(current_user: User = Depends(get_admin_user), db: Session = Depends(get_db)): + today = datetime.now().date() + today_start = datetime.combine(today, time.min) + + total_users = db.query(func.count(User.id)).scalar() or 0 + active_users = db.query(func.count(User.id)).filter(User.is_active.is_(True)).scalar() or 0 + admin_users = db.query(func.count(User.id)).filter(User.role == "admin").scalar() or 0 + new_users_today = db.query(func.count(User.id)).filter(User.created_at >= today_start).scalar() or 0 + + total_resources = db.query(func.count(Resource.id)).filter(Resource.status == "active").scalar() or 0 + public_resources = db.query(func.count(Resource.id)).filter(Resource.status == "active", Resource.is_public == 1).scalar() or 0 + + total_coursewares = db.query(func.count(Courseware.id)).filter(Courseware.status != "archived").scalar() or 0 + total_animations = db.query(func.count(Animation.id)).filter(Animation.status != "archived").scalar() or 0 + total_exercises = db.query(func.count(Exercise.id)).scalar() or 0 + total_lesson_plans = db.query(func.count(LessonPlan.id)).scalar() or 0 + + total_views = db.query(func.coalesce(func.sum(Resource.views), 0)).scalar() or 0 + total_downloads = db.query(func.coalesce(func.sum(Resource.downloads), 0)).scalar() or 0 + total_favorites = db.query(func.count(ResourceFavorite.id)).scalar() or 0 + total_credits_used = db.query(func.coalesce(func.sum(CreditAccount.total_used), 0)).scalar() or 0 + + exam_count = 0 + try: + from models.exam import Exam + exam_count = db.query(func.count(Exam.id)).scalar() or 0 + except Exception: + pass + + return AdminDashboardOut( + total_users=total_users, + active_users=active_users, + admin_users=admin_users, + new_users_today=new_users_today, + total_resources=total_resources, + public_resources=public_resources, + total_coursewares=total_coursewares, + total_animations=total_animations, + total_exercises=total_exercises, + total_lesson_plans=total_lesson_plans, + total_exams=exam_count, + total_views=total_views, + total_downloads=total_downloads, + total_favorites=total_favorites, + total_credits_used=total_credits_used, + ) + + +@router.get("/users", response_model=list[AdminUserOut]) +def list_users( + keyword: str = Query("", description="搜索手机号或姓名"), + role: str = Query("", description="按角色筛选"), + skip: int = Query(0, ge=0), + limit: int = Query(20, ge=1, le=100), + current_user: User = Depends(get_admin_user), + db: Session = Depends(get_db), +): + query = db.query(User) + if keyword: + like = f"%{keyword}%" + query = query.filter((User.phone.contains(like)) | (User.name.contains(like))) + if role: + query = query.filter(User.role == role) + users = query.order_by(User.created_at.desc()).offset(skip).limit(limit).all() + return [_user_with_credits(u, db) for u in users] + + +@router.put("/users/{user_id}", response_model=AdminUserOut) +def update_user( + user_id: int, + data: AdminUserUpdate, + request: Request, + current_user: User = Depends(get_admin_user), + db: Session = Depends(get_db), +): + user = db.query(User).filter(User.id == user_id).first() + if not user: + raise HTTPException(status_code=404, detail="用户不存在") + changes = [] + if data.role is not None: + if data.role not in {"teacher", "admin"}: + raise HTTPException(status_code=400, detail="角色无效") + changes.append(f"role:{user.role}->{data.role}") + user.role = data.role + if data.is_active is not None: + changes.append(f"active:{user.is_active}->{data.is_active}") + user.is_active = data.is_active + if data.name is not None: + changes.append(f"name:{user.name}->{data.name}") + user.name = data.name + db.commit() + db.refresh(user) + log_action(db, action="admin_update_user", user=current_user, request=request, target_type="user", target_id=user.id, detail=f"修改用户 {user.name or user.phone or user.id}:{', '.join(changes) or '无变更'}") + return _user_with_credits(user, db) + + +@router.post("/users/{user_id}/grant-credits", response_model=AdminUserOut) +def grant_user_credits( + user_id: int, + data: AdminGrantCredits, + request: Request, + current_user: User = Depends(get_admin_user), + db: Session = Depends(get_db), +): + user = db.query(User).filter(User.id == user_id).first() + if not user: + raise HTTPException(status_code=404, detail="用户不存在") + grant_credits(db, user, data.amount, action="admin_grant", description=f"管理员 {current_user.name} 发放 {data.amount} 积分") + db.commit() + db.refresh(user) + log_action(db, action="admin_grant_credits", user=current_user, request=request, target_type="user", target_id=user.id, detail=f"向 {user.name or user.phone or user.id} 发放 {data.amount} 积分") + return _user_with_credits(user, db) + + +@router.get("/resources", response_model=list[ResourceOut]) +def list_all_resources( + keyword: str = Query(""), + resource_type: str = Query(""), + status: str = Query("active"), + skip: int = Query(0, ge=0), + limit: int = Query(20, ge=1, le=100), + current_user: User = Depends(get_admin_user), + db: Session = Depends(get_db), +): + from sqlalchemy import or_ + + query = db.query(Resource) + if status: + query = query.filter(Resource.status == status) + if resource_type: + query = query.filter(Resource.resource_type == resource_type) + if keyword: + like = f"%{keyword}%" + query = query.filter(or_(Resource.title.contains(like), Resource.description.contains(like))) + resources = query.order_by(Resource.created_at.desc()).offset(skip).limit(limit).all() + result = [] + for res in resources: + out = ResourceOut.model_validate(res) + out.is_favorited = False + result.append(out) + return result + + +@router.put("/resources/{resource_id}/moderate", response_model=dict) +def moderate_resource( + resource_id: int, + request: Request, + status: str = Query(..., description="active 或 archived"), + current_user: User = Depends(get_admin_user), + db: Session = Depends(get_db), +): + if status not in {"active", "archived"}: + raise HTTPException(status_code=400, detail="状态无效") + res = db.query(Resource).filter(Resource.id == resource_id).first() + if not res: + raise HTTPException(status_code=404, detail="资源不存在") + prev = res.status + res.status = status + db.commit() + log_action(db, action="admin_moderate_resource", user=current_user, request=request, target_type="resource", target_id=res.id, detail=f"资源《{res.title}》状态 {prev} -> {status}") + return {"success": True, "status": res.status} + + +@router.get("/audit-logs", response_model=list[dict]) +def list_audit_logs( + action: str | None = None, + status: str | None = None, + user_id: int | None = None, + keyword: str | None = None, + skip: int = Query(0, ge=0), + limit: int = Query(50, ge=1, le=200), + current_user: User = Depends(get_admin_user), + db: Session = Depends(get_db), +): + """查询安全审计日志(仅管理员)。""" + from sqlalchemy import or_ + q = db.query(AuditLog) + if action: + q = q.filter(AuditLog.action == action) + if status: + q = q.filter(AuditLog.status == status) + if user_id: + q = q.filter(AuditLog.user_id == user_id) + if keyword: + like = f"%{keyword}%" + q = q.filter(or_(AuditLog.username.contains(like), AuditLog.detail.contains(like), AuditLog.ip.contains(like))) + rows = q.order_by(AuditLog.created_at.desc()).offset(skip).limit(limit).all() + return [ + { + "id": r.id, + "user_id": r.user_id, + "username": r.username, + "action": r.action, + "target_type": r.target_type, + "target_id": r.target_id, + "detail": r.detail, + "ip": r.ip, + "user_agent": r.user_agent, + "status": r.status, + "created_at": r.created_at.isoformat() if r.created_at else None, + } + for r in rows + ] diff --git a/backend/routers/ai.py b/backend/routers/ai.py new file mode 100644 index 0000000..8e88504 --- /dev/null +++ b/backend/routers/ai.py @@ -0,0 +1,297 @@ +import re +from io import BytesIO +from urllib.parse import quote + +from docx import Document +from pptx import Presentation +from PIL import Image +from pypdf import PdfReader +from openpyxl import load_workbook +from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Request +from fastapi.responses import StreamingResponse +from models.material import Material +from models.user import User +from schemas.ai import EssayGradeRequest, ExamExportRequest, ExamGenerateRequest, HtmlExportRequest +from services.limiter import limiter +from services.auth import get_current_user +from services.audit import log_action +from services.upload_security import validate_upload, sanitize_filename +from services.ai_service import AIService +from services.credits import credits_payload, spend_credits +from database import get_db +from sqlalchemy.orm import Session + +router = APIRouter(prefix="/api/ai", tags=["AI能力"]) +ai_service = AIService() + + +@router.post("/essay-grade", response_model=dict) +@limiter.limit("10/minute") +async def grade_essay( + request: Request, + data: EssayGradeRequest, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + account = spend_credits(db, current_user, "essay_grade", "作文批改") + db.commit() + result = await ai_service.grade_essay( + essay_text=data.essay_text, grade_level=data.grade_level, + essay_type=data.essay_type, total_score=data.total_score, + ) + return {"success": True, "data": result, "credits": credits_payload(account, "essay_grade")} + + +@router.post("/exam-generate", response_model=dict) +@limiter.limit("10/minute") +async def generate_exam( + request: Request, + data: ExamGenerateRequest, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + account = spend_credits(db, current_user, "exam_generate", f"智能命题:{data.subject}") + db.commit() + result = await ai_service.generate_exam( + subject=data.subject, grade=data.grade, + knowledge_points=data.knowledge_points, difficulty=data.difficulty, + question_types=data.question_types, count=data.count, total_score=data.total_score, + ) + return {"success": True, "data": result, "credits": credits_payload(account, "exam_generate")} + + +def _safe_filename(name: str, suffix: str) -> str: + stem = re.sub(r"[\\/:*?\"<>|\r\n]+", "_", name).strip(" .") or "export" + return f"{stem[:80]}{suffix}" + + +def _summarize_text(text: str, limit: int = 4000) -> str: + normalized = re.sub(r"[ \t]+", " ", text.replace("\r\n", "\n")).strip() + normalized = re.sub(r"\n{3,}", "\n\n", normalized) + return normalized[:limit] + + +def _title_from_filename(filename: str) -> str: + stem = filename.rsplit(".", 1)[0] if "." in filename else filename + title = re.sub(r"[_-]+", " ", stem).strip() or "教学素材" + return title[:200] + + +def _decode_text(content: bytes) -> str: + for encoding in ("utf-8-sig", "utf-8", "gb18030", "gbk"): + try: + return content.decode(encoding) + except UnicodeDecodeError: + continue + return content.decode("utf-8", errors="ignore") + + +def _extract_docx(content: bytes) -> str: + document = Document(BytesIO(content)) + paragraphs = [paragraph.text.strip() for paragraph in document.paragraphs if paragraph.text.strip()] + table_lines = [] + for table in document.tables: + for row in table.rows: + cells = [cell.text.strip() for cell in row.cells if cell.text.strip()] + if cells: + table_lines.append(" | ".join(cells)) + return "\n".join([*paragraphs, *table_lines]) + + +def _extract_pptx(content: bytes) -> str: + presentation = Presentation(BytesIO(content)) + lines = [] + for index, slide in enumerate(presentation.slides, start=1): + slide_lines = [] + for shape in slide.shapes: + text = getattr(shape, "text", "").strip() + if text: + slide_lines.append(text) + if slide_lines: + lines.append(f"第 {index} 页:\n" + "\n".join(slide_lines)) + return "\n\n".join(lines) + + +def _extract_pdf(content: bytes) -> str: + reader = PdfReader(BytesIO(content)) + pages = [] + for index, page in enumerate(reader.pages, start=1): + text = (page.extract_text() or "").strip() + if text: + pages.append(f"第 {index} 页:\n{text}") + return "\n\n".join(pages) + + +def _extract_xlsx(content: bytes) -> str: + workbook = load_workbook(BytesIO(content), data_only=True, read_only=True) + lines = [] + for sheet in workbook.worksheets: + row_lines = [] + for row in sheet.iter_rows(values_only=True): + cells = [str(cell).strip() for cell in row if cell is not None and str(cell).strip()] + if cells: + row_lines.append(" | ".join(cells)) + if row_lines: + lines.append(f"工作表 {sheet.title}:\n" + "\n".join(row_lines[:200])) + return "\n\n".join(lines) + + +async def _describe_image(content: bytes, filename: str) -> str: + try: + with Image.open(BytesIO(content)) as image: + width, height = image.size + mode = image.mode + except Exception: + width, height, mode = 0, 0, "" + meta = f"图片文件:{filename}" + if width and height: + meta += f";尺寸:{width}×{height}px" + if mode: + meta += f";色彩模式:{mode}" + + # Try AI vision OCR first; fall back to metadata-only description + try: + ocr_text = await ai_service.ocr_image(content, filename) + if ocr_text and "[无法识别文字内容]" not in ocr_text: + return f"{meta}\n\n【图片文字识别结果】\n{ocr_text}" + except Exception as exc: + import logging + logging.getLogger(__name__).warning("图片OCR失败,使用元数据描述: %s", exc) + + return ( + f"{meta}。\n" + "这是一份拍照/图片教学素材,可能包含教材页、题目、板书、图表、实验现象或课堂场景。" + "当前系统已保存图片素材类型,可将其带入课件、动画、练习、教案或命题流程;" + "生成时请教师在提示词中补充图片中的关键文字、题干或知识点,以便产出更准确。" + ) + + +@router.post("/materials/parse", response_model=dict) +@limiter.limit("20/minute") +async def parse_material( + request: Request, + file: UploadFile = File(...), + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + content = await file.read() + check = validate_upload(file, content=content, max_size=50 * 1024 * 1024) + filename = check.filename + suffix = check.suffix + media_type = check.media_type + + try: + if media_type.startswith("image/"): + text = await _describe_image(content, filename) + material_type = "image" + elif suffix == "docx": + text = _extract_docx(content) + material_type = "docx" + elif suffix == "pptx": + text = _extract_pptx(content) + material_type = "pptx" + elif suffix == "pdf": + text = _extract_pdf(content) + material_type = "pdf" + elif suffix in {"xlsx", "xls"}: + text = _extract_xlsx(content) + material_type = "xlsx" + elif suffix in {"txt", "md", "csv", "json"} or media_type.startswith("text/"): + text = _decode_text(content) + material_type = suffix or "text" + else: + raise HTTPException(status_code=415, detail="暂不支持该文件类型,请上传 txt、md、csv、json、docx、pptx、pdf、xlsx 或图片") + except HTTPException: + raise + except Exception as exc: + raise HTTPException(status_code=400, detail=f"材料解析失败:{exc}") from exc + + summary = _summarize_text(text) + account = spend_credits(db, current_user, "material_parse", f"解析材料:{filename}") + material = Material( + user_id=current_user.id, + filename=filename, + title=_title_from_filename(filename), + material_type=material_type, + summary=summary, + char_count=len(text), + size=len(content), + source="upload", + ) + db.add(material) + db.commit() + db.refresh(material) + log_action(db, action="material_parse", user=current_user, request=request, target_type="material", target_id=material.id, detail=f"解析 {filename}({material_type}, {len(content)}B)") + return { + "success": True, + "credits": credits_payload(account, "material_parse"), + "data": { + "material_id": material.id, + "filename": filename, + "title": material.title, + "material_type": material_type, + "size": len(content), + "summary": summary, + "char_count": len(text), + }, + } + + +@router.post("/export/html") +def export_html(data: HtmlExportRequest, current_user: User = Depends(get_current_user)): + content = f""" + + + + + {data.title} + + +{data.html} + + +""" + filename = _safe_filename(data.title, ".html") + return StreamingResponse( + BytesIO(content.encode("utf-8")), + media_type="text/html; charset=utf-8", + headers={"Content-Disposition": f"attachment; filename*=UTF-8''{quote(filename)}"}, + ) + + +@router.post("/export/exam-docx") +def export_exam_docx(data: ExamExportRequest, current_user: User = Depends(get_current_user)): + document = Document() + document.add_heading(data.title or "试卷", level=1) + meta = " / ".join(part for part in [data.subject, data.grade] if part) + if meta: + document.add_paragraph(meta) + + for idx, question in enumerate(data.questions, start=1): + q_type = question.get("type") or question.get("question_type") or "题目" + score = question.get("score") + heading = f"{idx}. [{q_type}]" + if score: + heading += f"({score}分)" + document.add_paragraph(heading) + document.add_paragraph(str(question.get("content") or question.get("question") or "")) + options = question.get("options") or [] + if isinstance(options, list): + for option in options: + document.add_paragraph(str(option), style=None) + + if data.answers: + document.add_page_break() + document.add_heading("参考答案", level=1) + for idx, answer in enumerate(data.answers, start=1): + document.add_paragraph(f"{idx}. {answer}") + + buffer = BytesIO() + document.save(buffer) + buffer.seek(0) + filename = _safe_filename(data.title, ".docx") + return StreamingResponse( + buffer, + media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document", + headers={"Content-Disposition": f"attachment; filename*=UTF-8''{quote(filename)}"}, + ) diff --git a/backend/routers/animation.py b/backend/routers/animation.py new file mode 100644 index 0000000..12eaa8d --- /dev/null +++ b/backend/routers/animation.py @@ -0,0 +1,188 @@ +import re +from html import escape +from io import BytesIO +from urllib.parse import quote + +from fastapi import APIRouter, Depends, HTTPException, Query, Request +from fastapi.responses import StreamingResponse +from sqlalchemy.orm import Session +from typing import Optional +from database import get_db +from models.animation import Animation +from models.resource import Resource +from models.user import User +from schemas.animation import AnimationGenerate, AnimationCreate, AnimationUpdate, AnimationOut +from services.credits import credits_payload, spend_credits +from services.limiter import limiter +from services.auth import get_current_user, get_optional_current_user +from services.ai_service import AIService + +router = APIRouter(prefix="/api/animations", tags=["教学动画"]) +ai_service = AIService() + + +def _safe_filename(name: str, suffix: str) -> str: + stem = re.sub(r"[\\/:*?\"<>|\r\n]+", "_", name).strip(" .") or "教学动画" + return f"{stem[:80]}{suffix}" + + +def _animation_or_404(anim_id: int, current_user: User | None, db: Session) -> Animation: + anim = db.query(Animation).filter(Animation.id == anim_id).first() + if not anim: + raise HTTPException(status_code=404, detail="动画不存在") + if (not current_user or anim.user_id != current_user.id) and not has_public_resource(db, anim_id): + raise HTTPException(status_code=403, detail="无权访问该动画") + return anim + + +def _build_animation_html(anim: Animation) -> str: + config = anim.config or {} + body = config.get("html") or config.get("content") or "" + if not isinstance(body, str) or not body.strip(): + raise HTTPException(status_code=400, detail="当前动画没有可导出的 HTML") + title = escape(anim.title or config.get("title") or "教学动画") + return f""" + + + + + {title} + + + +

    + {body} +
    + + +""" + + +def has_public_resource(db: Session, anim_id: int) -> bool: + return db.query(Resource).filter( + Resource.content_ref == f"animation:{anim_id}", + Resource.status == "active", + Resource.is_public == 1, + ).first() is not None + + +@limiter.limit("10/minute") +@router.post("/ai-generate", response_model=dict) +async def ai_generate_animation( + request: Request, + data: AnimationGenerate, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + account = spend_credits(db, current_user, "animation_generate", f"生成教学动画:{data.prompt[:80]}") + db.commit() + result = await ai_service.generate_animation(prompt=data.prompt, anim_type=data.anim_type) + return {"success": True, "data": result, "credits": credits_payload(account, "animation_generate")} + + +@router.post("/", response_model=AnimationOut, status_code=201) +def create_animation(data: AnimationCreate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + anim = Animation(user_id=current_user.id, **data.model_dump()) + db.add(anim) + db.commit() + db.refresh(anim) + return anim + + +@router.get("/", response_model=list[AnimationOut]) +def list_animations( + anim_type: Optional[str] = None, + skip: int = Query(0, ge=0), + limit: int = Query(20, ge=1, le=100), + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + query = db.query(Animation).filter(Animation.user_id == current_user.id) + if anim_type: + query = query.filter(Animation.anim_type == anim_type) + return query.order_by(Animation.updated_at.desc()).offset(skip).limit(limit).all() + + +@router.get("/{anim_id}", response_model=AnimationOut) +def get_animation( + anim_id: int, + current_user: User | None = Depends(get_optional_current_user), + db: Session = Depends(get_db), +): + return _animation_or_404(anim_id, current_user, db) + + +@router.get("/{anim_id}/export-html") +def export_animation_html( + anim_id: int, + current_user: User | None = Depends(get_optional_current_user), + db: Session = Depends(get_db), +): + anim = _animation_or_404(anim_id, current_user, db) + content = _build_animation_html(anim) + filename = _safe_filename(f"{anim.title or '教学动画'}-动画", ".html") + return StreamingResponse( + BytesIO(content.encode("utf-8")), + media_type="text/html; charset=utf-8", + headers={"Content-Disposition": f"attachment; filename*=UTF-8''{quote(filename)}"}, + ) + + +@router.put("/{anim_id}", response_model=AnimationOut) +def update_animation( + anim_id: int, data: AnimationUpdate, + current_user: User = Depends(get_current_user), db: Session = Depends(get_db), +): + anim = db.query(Animation).filter(Animation.id == anim_id, Animation.user_id == current_user.id).first() + if not anim: + raise HTTPException(status_code=404, detail="动画不存在") + for key, value in data.model_dump(exclude_unset=True).items(): + setattr(anim, key, value) + db.commit() + db.refresh(anim) + return anim + + +@router.post("/{anim_id}/remix", response_model=AnimationOut, status_code=201) +def remix_animation( + anim_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + source = db.query(Animation).filter(Animation.id == anim_id).first() + if not source: + raise HTTPException(status_code=404, detail="动画不存在") + if source.user_id != current_user.id and not has_public_resource(db, anim_id): + raise HTTPException(status_code=403, detail="无权改编该动画") + + clone = Animation( + user_id=current_user.id, + title=f"{source.title}(改编)", + anim_type=source.anim_type or "general", + description=source.description or "", + config=source.config or {}, + thumbnail=source.thumbnail or "", + status="draft", + ) + db.add(clone) + db.commit() + db.refresh(clone) + return clone + + +@router.delete("/{anim_id}", status_code=204) +def delete_animation(anim_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + anim = db.query(Animation).filter(Animation.id == anim_id, Animation.user_id == current_user.id).first() + if not anim: + raise HTTPException(status_code=404, detail="动画不存在") + db.query(Resource).filter( + Resource.user_id == current_user.id, + Resource.content_ref == f"animation:{anim_id}", + Resource.status == "active", + ).update({"status": "archived"}, synchronize_session=False) + db.delete(anim) + db.commit() diff --git a/backend/routers/auth.py b/backend/routers/auth.py new file mode 100644 index 0000000..486e5d8 --- /dev/null +++ b/backend/routers/auth.py @@ -0,0 +1,435 @@ +from fastapi import APIRouter, Depends, HTTPException, status, Request, UploadFile, File +from sqlalchemy import func +from sqlalchemy.orm import Session +from datetime import datetime, timedelta +import os +import secrets +from config import get_settings +from database import get_db +from services.upload_security import validate_upload +from services.password_policy import validate_password_strength +from models.animation import Animation +from models.classroom import ClassroomActivity +from models.community import Comment, Post, PostFavorite +from models.courseware import Courseware +from models.credit import CreditTransaction +from models.exam import Exam +from models.essay import EssayGrade +from models.exercise import Exercise +from models.lesson_plan import LessonPlan +from models.mindmap import MindMap +from models.reset_code import ResetCode +from models.resource import Resource, ResourceFavorite +from models.user import User +from schemas.user import CreditBenefitOut, UserLogin, UserRegister, UserUpdate, UserOut, Token, ProfileStatsOut, PasswordChange, SendCodeRequest, ResetPasswordRequest +from services.credits import DAILY_CHECKIN_CREDITS, grant_daily_checkin, has_credit_transaction_today, ensure_credit_account +from services.limiter import limiter +from services.auth import ( + get_password_hash, verify_password, + create_access_token, create_refresh_token, + get_current_user, +) +from services.audit import log_action + +router = APIRouter(prefix="/api/auth", tags=["认证"]) + + +def serialize_user(user: User, db: Session) -> dict: + account = ensure_credit_account(db, user) + data = UserOut.model_validate(user).model_dump() + data.update({ + "credits": account.balance, + "total_credits_granted": account.total_granted, + "total_credits_used": account.total_used, + }) + return data + + +@router.post("/register", response_model=Token, status_code=status.HTTP_201_CREATED) +@limiter.limit("5/minute") +def register(request: Request, data: UserRegister, db: Session = Depends(get_db)): + if db.query(User).filter(User.phone == data.phone).first(): + raise HTTPException(status_code=400, detail="该手机号已注册") + pwd_err = validate_password_strength(data.password) + if pwd_err: + raise HTTPException(status_code=400, detail=pwd_err) + user = User( + phone=data.phone, + password_hash=get_password_hash(data.password), + name=data.name, + subject=data.subject, + school=data.school, + grade=data.grade, + ) + db.add(user) + db.commit() + db.refresh(user) + log_action(db, action="register", user=user, request=request, target_type="user", target_id=user.id, detail=f"注册手机号 {user.phone}") + return Token( + access_token=create_access_token(user.id), + refresh_token=create_refresh_token(user.id), + ) + + +@router.post("/login", response_model=Token) +@limiter.limit("10/minute") +def login(request: Request, data: UserLogin, db: Session = Depends(get_db)): + user = db.query(User).filter(User.phone == data.phone).first() + if not user or not verify_password(data.password, user.password_hash): + log_action(db, action="login", request=request, target_type="user", detail=f"登录失败:{data.phone}", status="failed") + raise HTTPException(status_code=401, detail="手机号或密码错误") + if not user.is_active: + log_action(db, action="login", user=user, request=request, target_type="user", target_id=user.id, detail="账号已禁用", status="denied") + raise HTTPException(status_code=403, detail="账号已被禁用") + log_action(db, action="login", user=user, request=request, target_type="user", target_id=user.id, detail="登录成功") + return Token( + access_token=create_access_token(user.id), + refresh_token=create_refresh_token(user.id), + ) + + +@router.post("/refresh", response_model=Token) +@limiter.limit("30/minute") +def refresh_token(request: Request, refresh_token: str, db: Session = Depends(get_db)): + """用 refresh_token 换取新的 access_token(与新的 refresh_token)。""" + from jose import jwt, JWTError + from config import get_settings as _gs + _s = _gs() + try: + payload = jwt.decode(refresh_token, _s.SECRET_KEY, algorithms=[_s.ALGORITHM]) + if payload.get("type") != "refresh": + raise HTTPException(status_code=401, detail="无效的刷新凭证") + user_id = int(payload.get("sub")) + except (JWTError, TypeError, ValueError): + log_action(db, action="refresh_token", request=request, detail="刷新令牌无效或过期", status="failed") + raise HTTPException(status_code=401, detail="刷新凭证无效或已过期,请重新登录") + + user = db.query(User).filter(User.id == user_id).first() + if user is None: + raise HTTPException(status_code=401, detail="用户不存在") + if not user.is_active: + raise HTTPException(status_code=403, detail="账号已被禁用") + + log_action(db, action="refresh_token", user=user, request=request, target_type="user", target_id=user.id, detail="刷新访问令牌") + return Token( + access_token=create_access_token(user.id), + refresh_token=create_refresh_token(user.id), + ) + + +@router.get("/me", response_model=UserOut) +def get_profile(current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + db.commit() + return serialize_user(current_user, db) + + +@router.put("/me", response_model=UserOut) +def update_profile( + data: UserUpdate, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + update_data = data.model_dump(exclude_unset=True) + for key, value in update_data.items(): + setattr(current_user, key, value) + db.commit() + db.refresh(current_user) + return serialize_user(current_user, db) + + +@router.post("/change-password", response_model=dict) +def change_password( + data: PasswordChange, + request: Request, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + if not verify_password(data.old_password, current_user.password_hash): + log_action(db, action="change_password", user=current_user, request=request, target_type="user", target_id=current_user.id, detail="旧密码错误", status="failed") + raise HTTPException(status_code=400, detail="当前密码错误") + if data.old_password == data.new_password: + raise HTTPException(status_code=400, detail="新密码不能与当前密码相同") + new_pwd_err = validate_password_strength(data.new_password) + if new_pwd_err: + raise HTTPException(status_code=400, detail=new_pwd_err) + current_user.password_hash = get_password_hash(data.new_password) + db.commit() + log_action(db, action="change_password", user=current_user, request=request, target_type="user", target_id=current_user.id, detail="修改密码成功") + return {"success": True} + + +@router.post("/avatar", response_model=dict) +async def upload_avatar( + request: Request, + file: UploadFile = File(...), + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + """上传用户头像,返回可访问的 URL。""" + content = await file.read() + settings = get_settings() + check = validate_upload(file, content=content, max_size=5 * 1024 * 1024) + # 仅允许图片 + if check.suffix not in {"png", "jpg", "jpeg", "gif", "webp", "bmp"}: + raise HTTPException(status_code=415, detail="头像仅支持 png/jpg/jpeg/gif/webp/bmp 格式") + # 保存到 UPLOAD_DIR/avatars/ + avatars_dir = os.path.join(settings.UPLOAD_DIR, "avatars") + os.makedirs(avatars_dir, exist_ok=True) + filename = f"user_{current_user.id}_{int(__import__('time').time())}.{check.suffix}" + save_path = os.path.join(avatars_dir, filename) + with open(save_path, "wb") as f: + f.write(content) + avatar_url = f"/uploads/avatars/{filename}" + current_user.avatar = avatar_url + db.commit() + log_action(db, action="upload_avatar", user=current_user, request=request, target_type="user", target_id=current_user.id, detail=f"上传头像 {filename}") + return {"success": True, "avatar": avatar_url} + + +def credit_benefit_payload(user: User, db: Session) -> dict: + account = ensure_credit_account(db, user) + daily_claimed = has_credit_transaction_today(db, user, "daily_checkin") + recent_checkins = ( + db.query(CreditTransaction) + .filter(CreditTransaction.user_id == user.id, CreditTransaction.action == "daily_checkin") + .order_by(CreditTransaction.created_at.desc()) + .limit(7) + .all() + ) + return { + "credits": account.balance, + "daily_amount": DAILY_CHECKIN_CREDITS, + "daily_claimed": daily_claimed, + "next_claim_text": "明日可继续领取" if daily_claimed else "今日可领取", + "recent_checkins": recent_checkins, + } + + +@router.get("/credits/benefits", response_model=CreditBenefitOut) +def get_credit_benefits( + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + return credit_benefit_payload(current_user, db) + + +@router.post("/credits/daily-checkin", response_model=CreditBenefitOut) +def claim_daily_checkin( + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + grant_daily_checkin(db, current_user) + db.commit() + db.refresh(current_user) + return credit_benefit_payload(current_user, db) + + +@router.get("/stats", response_model=ProfileStatsOut) +def get_profile_stats( + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + user_id = current_user.id + account = ensure_credit_account(db, current_user) + + coursewares = db.query(Courseware).filter(Courseware.user_id == user_id, Courseware.status != "archived").all() + animations = db.query(Animation).filter(Animation.user_id == user_id).all() + exercises = db.query(Exercise).filter(Exercise.user_id == user_id).all() + lesson_plans = db.query(LessonPlan).filter(LessonPlan.user_id == user_id).all() + essay_grades = db.query(EssayGrade).filter(EssayGrade.user_id == user_id).all() + exams = db.query(Exam).filter(Exam.user_id == user_id).all() + mind_maps = db.query(MindMap).filter(MindMap.user_id == user_id).all() + resources = db.query(Resource).filter(Resource.user_id == user_id, Resource.status == "active").all() + classroom_activities = db.query(ClassroomActivity).filter(ClassroomActivity.user_id == user_id).all() + community_posts = db.query(Post).filter(Post.user_id == user_id).all() + + works_by_type = { + "courseware": len(coursewares), + "animation": len(animations), + "exercise": len(exercises), + "lesson_plan": len(lesson_plans), + "essay_grade": len(essay_grades), + "exam": len(exams), + "mindmap": len(mind_maps), + } + resources_by_type = {key: 0 for key in ["courseware", "animation", "exercise", "lesson_plan", "exam", "other"]} + for resource in resources: + resources_by_type[resource.resource_type or "other"] = resources_by_type.get(resource.resource_type or "other", 0) + 1 + + published_refs = {item.content_ref for item in resources if item.content_ref} + published_refs.update(f"courseware:{item.id}" for item in coursewares if item.status == "published") + published_refs.update(f"animation:{item.id}" for item in animations if item.status == "published") + published_works = len(published_refs) + favorite_count = db.query(ResourceFavorite).filter(ResourceFavorite.user_id == user_id).count() + community_favorite_count = db.query(PostFavorite).filter(PostFavorite.user_id == user_id).count() + community_comment_count = db.query(Comment).filter(Comment.user_id == user_id).count() + + def source_status(item, item_type: str, fallback: str = "") -> str: + if f"{item_type}:{item.id}" in published_refs: + return "published" + return fallback + + def recent_work(item, item_type: str, subtitle: str = "", status: str = "") -> dict: + return { + "id": item.id, + "item_type": item_type, + "title": item.title, + "subtitle": subtitle, + "status": status, + "updated_at": item.updated_at, + } + + classroom_type_labels = { + "poll": "课堂投票", + "qa": "即时问答", + "quiz": "随堂测验", + "checkin": "课堂签到", + "feedback": "学习反馈", + } + community_type_labels = { + "discussion": "案例", + "share": "模板", + "help": "需求", + } + + recent_works = [ + *[recent_work(item, "courseware", "课件", source_status(item, "courseware", item.status or "draft")) for item in coursewares], + *[recent_work(item, "animation", "动画", source_status(item, "animation", item.status or "draft")) for item in animations], + *[recent_work(item, "exercise", "互动练习", source_status(item, "exercise", "draft")) for item in exercises], + *[recent_work(item, "lesson_plan", "教案", source_status(item, "lesson_plan", "draft")) for item in lesson_plans], + *[recent_work(item, "essay_grade", "作文批改", "已批改") for item in essay_grades], + *[recent_work(item, "exam", "试卷", source_status(item, "exam", "draft")) for item in exams], + *[recent_work(item, "mindmap", "思维导图", "draft") for item in mind_maps], + ] + recent_works.sort(key=lambda item: item["updated_at"] or datetime.min, reverse=True) + + recent_resources = [ + { + "id": item.id, + "item_type": item.resource_type or "other", + "title": item.title, + "subtitle": "公开" if item.is_public else "私密", + "status": item.status or "active", + "updated_at": item.updated_at, + } + for item in sorted(resources, key=lambda resource: resource.updated_at or datetime.min, reverse=True)[:5] + ] + + recent_interactions = [ + *[ + { + "id": item.id, + "item_type": "classroom_activity", + "title": item.title, + "subtitle": f"{classroom_type_labels.get(item.activity_type or '', '课堂活动')} · {len(item.responses or [])} 条提交", + "status": item.status or "draft", + "updated_at": item.updated_at, + } + for item in classroom_activities + ], + *[ + { + "id": item.id, + "item_type": "community_post", + "title": item.title, + "subtitle": f"{community_type_labels.get(item.post_type or '', '社区内容')} · {item.likes or 0} 收藏 · {item.comments_count or 0} 评论", + "status": "published", + "updated_at": item.updated_at, + } + for item in community_posts + ], + ] + recent_interactions.sort(key=lambda item: item["updated_at"] or datetime.min, reverse=True) + + total_favorites = db.query(func.count(ResourceFavorite.id)).join(Resource).filter( + Resource.user_id == user_id, + Resource.status == "active", + ).scalar() or 0 + recent_credit_transactions = sorted( + list(current_user.credit_transactions or []), + key=lambda item: item.created_at or datetime.min, + reverse=True, + )[:8] + + return { + "credits": account.balance, + "total_credits_granted": account.total_granted, + "total_credits_used": account.total_used, + "total_works": sum(works_by_type.values()), + "draft_works": max(sum(works_by_type.values()) - published_works, 0), + "published_works": published_works, + "published_resources": len(resources), + "public_resources": sum(1 for item in resources if item.is_public), + "private_resources": sum(1 for item in resources if not item.is_public), + "favorite_resources": favorite_count, + "total_views": sum(item.views or 0 for item in resources), + "total_downloads": sum(item.downloads or 0 for item in resources), + "total_favorites": total_favorites, + "classroom_activities": len(classroom_activities), + "active_classroom_activities": sum(1 for item in classroom_activities if item.status == "active"), + "classroom_responses": sum(len(item.responses or []) for item in classroom_activities), + "community_posts": len(community_posts), + "community_favorites": community_favorite_count, + "community_comments": community_comment_count, + "works_by_type": works_by_type, + "resources_by_type": resources_by_type, + "recent_works": recent_works[:5], + "recent_resources": recent_resources, + "recent_interactions": recent_interactions[:6], + "recent_credit_transactions": recent_credit_transactions, + } + + +@limiter.limit("3/minute") +@router.post("/send-code", response_model=dict) +def send_reset_code( + request: Request, + data: SendCodeRequest, + db: Session = Depends(get_db), +): + """发送密码重置验证码。生产环境对接短信网关;开发环境返回 code 便于调试。""" + user = db.query(User).filter(User.phone == data.phone).first() + if not user: + # 不暴露手机号是否注册,统一返回成功 + return {"success": True, "message": "验证码已发送"} + code = f"{secrets.randbelow(1000000):06d}" + expires = datetime.now() + timedelta(minutes=5) + # 使该手机号之前未消费的验证码失效 + db.query(ResetCode).filter( + ResetCode.phone == data.phone, ResetCode.consumed == 0 + ).update({"consumed": 1}, synchronize_session=False) + db.add(ResetCode(phone=data.phone, code=code, expires_at=expires, consumed=0)) + db.commit() + # 开发环境返回验证码;生产环境去掉 dev_code 字段 + return {"success": True, "message": "验证码已发送", "dev_code": code} + + +@router.post("/reset-password", response_model=dict) +def reset_password( + request: Request, + data: ResetPasswordRequest, + db: Session = Depends(get_db), +): + """通过验证码重置密码。""" + pwd_err = validate_password_strength(data.new_password) + if pwd_err: + raise HTTPException(status_code=400, detail=pwd_err) + record = db.query(ResetCode).filter( + ResetCode.phone == data.phone, + ResetCode.code == data.code, + ResetCode.consumed == 0, + ).order_by(ResetCode.created_at.desc()).first() + if not record: + raise HTTPException(status_code=400, detail="验证码无效或已过期") + if record.expires_at < datetime.now(): + record.consumed = 1 + db.commit() + raise HTTPException(status_code=400, detail="验证码已过期,请重新获取") + user = db.query(User).filter(User.phone == data.phone).first() + if not user: + raise HTTPException(status_code=400, detail="账号不存在") + user.password_hash = get_password_hash(data.new_password) + record.consumed = 1 + db.commit() + log_action(db, action="reset_password", user=user, request=request, target_type="user", target_id=user.id, detail="通过验证码重置密码") + return {"success": True, "message": "密码重置成功"} diff --git a/backend/routers/chat.py b/backend/routers/chat.py new file mode 100644 index 0000000..1441254 --- /dev/null +++ b/backend/routers/chat.py @@ -0,0 +1,184 @@ +from fastapi import APIRouter, Depends, HTTPException, Query, Request +from sqlalchemy.orm import Session +from database import get_db +from models.chat import ChatConversation, ChatMessage +from models.user import User +from schemas.chat import ConversationCreate, ConversationRename, ChatSend, MessageOut, ConversationOut +from services.credits import credits_payload, spend_credits +from services.limiter import limiter +from services.auth import get_current_user +from services.audit import log_action +from services.ai_service import AIService + +router = APIRouter(prefix="/api/chat", tags=["AI教学助手"]) +ai_service = AIService() + +# 历史上下文窗口:最近 N 轮(user+assistant 计为 2 条),避免 token 超限 +MAX_HISTORY_TURNS = 12 +MAX_TITLE_LEN = 60 + + +def _ensure_owned(db: Session, conversation_id: int, user: User) -> ChatConversation: + item = db.query(ChatConversation).filter( + ChatConversation.id == conversation_id, + ChatConversation.user_id == user.id, + ).first() + if not item: + raise HTTPException(status_code=404, detail="对话不存在") + return item + + +def _derive_title(text: str) -> str: + cleaned = " ".join(text.split()) + return (cleaned[:MAX_TITLE_LEN] + "…") if len(cleaned) > MAX_TITLE_LEN else (cleaned or "新对话") + + +@router.get("/conversations", response_model=list[ConversationOut]) +def list_conversations( + keyword: str | None = None, + skip: int = Query(0, ge=0), + limit: int = Query(30, ge=1, le=100), + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + query = db.query(ChatConversation).filter(ChatConversation.user_id == current_user.id) + if keyword: + query = query.filter(ChatConversation.title.contains(keyword)) + return ( + query.order_by(ChatConversation.pinned.desc(), ChatConversation.updated_at.desc()) + .offset(skip).limit(limit).all() + ) + + +@router.post("/conversations", response_model=ConversationOut, status_code=201) +def create_conversation( + data: ConversationCreate, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + item = ChatConversation( + user_id=current_user.id, + title=(data.title or "新对话").strip() or "新对话", + subject=data.subject, + grade=data.grade, + ) + db.add(item) + db.commit() + db.refresh(item) + return item + + +@router.get("/conversations/{conversation_id}", response_model=ConversationOut) +def get_conversation( + conversation_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + return _ensure_owned(db, conversation_id, current_user) + + +@router.put("/conversations/{conversation_id}", response_model=ConversationOut) +def rename_conversation( + conversation_id: int, + data: ConversationRename, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + item = _ensure_owned(db, conversation_id, current_user) + item.title = data.title.strip() or item.title + if data.pinned is not None: + item.pinned = data.pinned + db.commit() + db.refresh(item) + return item + + +@router.delete("/conversations/{conversation_id}", status_code=204) +def delete_conversation( + conversation_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + item = _ensure_owned(db, conversation_id, current_user) + db.delete(item) + db.commit() + return None + + +@router.post("/conversations/{conversation_id}/messages", response_model=dict) +@limiter.limit("20/minute") +async def send_message( + request: Request, + conversation_id: int, + data: ChatSend, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + conversation = _ensure_owned(db, conversation_id, current_user) + account = spend_credits(db, current_user, "chat_generate", "AI教学助手对话") + db.add(ChatMessage( + conversation_id=conversation.id, + role="user", + content=data.content, + )) + db.flush() + + history = ( + db.query(ChatMessage) + .filter(ChatMessage.conversation_id == conversation.id) + .order_by(ChatMessage.id.desc()) + .limit(MAX_HISTORY_TURNS) + .all() + ) + history = list(reversed(history)) + history_payload = [{"role": m.role, "content": m.content} for m in history[:-1]] if history else [] + + try: + reply = await ai_service.chat_teaching( + history=history_payload, + user_message=data.content, + subject=data.subject or conversation.subject, + grade=data.grade or conversation.grade, + ) + except RuntimeError as exc: + msg = str(exc) + if "CONNECT" in msg or "TIMEOUT" in msg: + reply = ( + "无法连接到 AI 服务,请检查网络或稍后重试。\n" + "你的问题我已记录,服务恢复后重新发送即可继续对话。" + ) + elif "EMPTY" in msg: + reply = ( + "AI 模型本次未返回内容(推理模型可能因思考超时导致空回复),请稍后重试或换一种问法。" + ) + else: + reply = ( + "当前 AI 服务暂时不可用,请稍后重试。我已经记录了你的问题," + "服务恢复后再次发送即可继续对话。" + ) + + assistant_msg = ChatMessage( + conversation_id=conversation.id, + role="assistant", + content=reply, + ) + db.add(assistant_msg) + + first_message = len(conversation.messages) <= 2 + if conversation.title == "新对话" or first_message: + conversation.title = _derive_title(data.content) + db.commit() + db.refresh(assistant_msg) + + log_action( + db, action="chat_send", user=current_user, request=request, + target_type="chat_conversation", target_id=conversation.id, + detail=f"AI助手对话({len(data.content)}字)", + ) + + return { + "success": True, + "data": MessageOut.model_validate(assistant_msg).model_dump(), + "title": conversation.title, + "credits": credits_payload(account, "chat_generate"), + } diff --git a/backend/routers/classroom.py b/backend/routers/classroom.py new file mode 100644 index 0000000..22744af --- /dev/null +++ b/backend/routers/classroom.py @@ -0,0 +1,513 @@ +import re +from collections import Counter +from datetime import datetime, timezone +from io import BytesIO +from html import escape +from urllib.parse import quote + +from docx import Document +from fastapi import APIRouter, Depends, HTTPException, Query +from fastapi.responses import StreamingResponse +from sqlalchemy.orm import Session + +from database import get_db +from models.classroom import ClassroomActivity +from models.user import User +from schemas.classroom import ( + ClassroomActivityCreate, + ClassroomAnalysisOut, + ClassroomActivityOut, + ClassroomActivityUpdate, + ClassroomResponseCreate, +) +from services.auth import get_current_user + +router = APIRouter(prefix="/api/classroom", tags=["课堂工具"]) + + +STOP_WORDS = { + "这个", "我们", "老师", "学生", "因为", "所以", "可以", "需要", "没有", "已经", + "the", "and", "that", "with", "this", "from", "have", "will", +} + + +def _format_answer(answer) -> str: + if isinstance(answer, str): + return answer.strip() + if answer is None: + return "" + return str(answer) + + +def _percent(count: int, total: int) -> int: + return round((count / total) * 100) if total else 0 + + +def _extract_keywords(text: str) -> list[dict]: + words = re.findall(r"[\u4e00-\u9fa5]{2,}|[a-zA-Z0-9]{2,}", text) + counter = Counter(word for word in words if word.lower() not in STOP_WORDS) + return [{"keyword": word, "count": count} for word, count in counter.most_common(10)] + + +def _infer_correct_answer(activity: ClassroomActivity) -> str: + config = activity.config or {} + for key in ("answer", "correct_answer", "correctAnswer"): + value = config.get(key) + if value: + return str(value).strip() + return "" + + +def _build_mastery_level(activity_type: str, response_count: int, accuracy: int | None, option_stats: list[dict]) -> str: + if response_count <= 0: + return "暂无数据" + if accuracy is not None: + if accuracy >= 85: + return "整体掌握较好" + if accuracy >= 60: + return "基础掌握,仍需巩固" + return "薄弱点明显,建议重点讲评" + if option_stats: + top = max(option_stats, key=lambda item: item["count"]) + if top["percent"] >= 70: + return f"倾向集中:{top['option']}" + if top["percent"] >= 45: + return f"主要倾向:{top['option']}" + return "分布分散,需要追问原因" + if activity_type in {"qa", "feedback"}: + return "已收集开放反馈" + return "已收集课堂数据" + + +def _build_diagnosis( + activity: ClassroomActivity, + response_count: int, + participant_count: int, + option_stats: list[dict], + keywords: list[dict], + accuracy: int | None, +) -> list[str]: + if response_count <= 0: + return ["尚未收到学生提交,建议先发布活动并提醒学生扫码参与。"] + diagnosis = [f"本次共收到 {response_count} 条提交,覆盖 {participant_count} 名学生。"] + if accuracy is not None: + diagnosis.append(f"按正确答案统计,当前正确率为 {accuracy}%。") + if option_stats: + top = max(option_stats, key=lambda item: item["count"]) + diagnosis.append(f"选择最多的是“{top['option']}”,占比 {top['percent']}%。") + weak_options = [item for item in option_stats if item["count"] and item["percent"] >= 20 and item is not top] + if weak_options: + names = "、".join(item["option"] for item in weak_options[:3]) + diagnosis.append(f"仍有较多学生选择“{names}”,建议追问选择原因。") + if keywords: + diagnosis.append("开放回答中高频词包括:" + "、".join(item["keyword"] for item in keywords[:5]) + "。") + if activity.activity_type == "checkin": + diagnosis.append("该活动更适合作为参与记录,可结合后续测验或反馈判断掌握情况。") + return diagnosis + + +def _build_teaching_suggestions( + activity: ClassroomActivity, + response_count: int, + option_stats: list[dict], + accuracy: int | None, + keywords: list[dict], +) -> list[str]: + if response_count <= 0: + return ["先用投放链接收集至少 5 条反馈,再生成更可靠的讲评建议。"] + suggestions = [] + if accuracy is not None and accuracy < 60: + suggestions.append("用 5 分钟回到概念源头,先讲清关键定义或公式来源,再做同类例题。") + suggestions.append("安排 3 道低门槛变式题,确认学生能独立完成基本步骤。") + elif accuracy is not None and accuracy < 85: + suggestions.append("挑选一个典型错选项进行对比讲评,让学生说出错误思路。") + suggestions.append("追加 2 到 3 道变式练习,覆盖易错条件和表达规范。") + elif accuracy is not None: + suggestions.append("整体掌握较好,可进入拓展任务或让学生互相解释解题依据。") + elif option_stats: + top = max(option_stats, key=lambda item: item["count"]) + suggestions.append(f"围绕“{top['option']}”这个主流反馈追问原因,确认学生是理解到位还是凭感觉选择。") + suggestions.append("让不同选择的学生各举一个理由,形成板书对比后再归纳共识。") + else: + suggestions.append("把高频回答聚类成 2 到 3 类,在下一轮讲评中分别回应。") + if keywords: + suggestions.append(f"优先处理“{keywords[0]['keyword']}”相关问题,并设计一个即时追问。") + suggestions.append("课末再投放一次简短反馈,比较讲评前后的变化。") + return suggestions[:5] + + +def _build_followup_prompt(activity: ClassroomActivity, analysis: dict) -> str: + question = analysis.get("question") or activity.title + diagnosis = ";".join(analysis.get("diagnosis") or []) + material_context = str((activity.config or {}).get("material_context") or "").strip() + material_part = f"参考素材:{material_context}。" if material_context else "" + return ( + f"基于课堂数据回收《{activity.title}》生成一套二次巩固练习。" + f"回收题目:{question}。" + f"{material_part}" + f"学情诊断:{diagnosis}。" + "要求题目有梯度,覆盖高频错误点,附答案和解析。" + ) + + +def _safe_filename(name: str, suffix: str) -> str: + stem = re.sub(r"[\\/:*?\"<>|\r\n]+", "_", name).strip(" .") or "课堂诊断报告" + return f"{stem[:80]}{suffix}" + + +def _excel_cell(value, cell_type: str = "String") -> str: + if value is None: + value = "" + if isinstance(value, datetime): + value = value.strftime("%Y-%m-%d %H:%M:%S") + text = escape(str(value), quote=False) + return f"{text}" + + +def _excel_row(values: list) -> str: + return "" + "".join(_excel_cell(value) for value in values) + "" + + +def _excel_sheet(name: str, rows: list[list]) -> str: + safe_name = escape(name[:31] or "Sheet", quote=True) + body = "\n".join(_excel_row(row) for row in rows) + return f"{body}
    " + + +def _build_activity_workbook(activity: ClassroomActivity, analysis: dict) -> bytes: + responses = list(activity.responses or []) + detail_rows = [ + ["活动ID", "活动标题", "活动类型", "学生姓名", "回答", "提交时间", "来源"], + *[ + [ + activity.id, + activity.title, + activity.activity_type, + res.get("student_name") or "匿名", + _format_answer(res.get("answer")), + res.get("submitted_at") or "", + (res.get("meta") or {}).get("source", ""), + ] + for res in responses + ], + ] + stats_rows = [ + ["指标", "数值"], + ["提交数", analysis["response_count"]], + ["参与学生数", analysis["participant_count"]], + ["掌握水平", analysis["mastery_level"]], + ["正确率", "" if analysis["accuracy"] is None else f"{analysis['accuracy']}%"], + ["回收题目", analysis["question"]], + ] + if analysis["option_stats"]: + stats_rows.extend([[], ["选项", "人数", "占比"]]) + stats_rows.extend([[item["option"], item["count"], f"{item['percent']}%"] for item in analysis["option_stats"]]) + if analysis["keywords"]: + stats_rows.extend([[], ["关键词", "次数"]]) + stats_rows.extend([[item["keyword"], item["count"]] for item in analysis["keywords"]]) + + diagnosis_rows = [ + ["诊断结论"], + *[[item] for item in analysis["diagnosis"]], + [], + ["讲评建议"], + *[[item] for item in analysis["teaching_suggestions"]], + [], + ["二次练习提示词"], + [analysis["followup_prompt"]], + ] + workbook = f""" + + +{_excel_sheet("提交明细", detail_rows)} +{_excel_sheet("统计汇总", stats_rows)} +{_excel_sheet("诊断建议", diagnosis_rows)} + +""" + return workbook.encode("utf-8") + + +def _analysis_payload(activity: ClassroomActivity) -> dict: + responses = list(activity.responses or []) + response_count = len(responses) + participants = { + str(res.get("student_name") or "匿名").strip() or "匿名" + for res in responses + } + participant_count = len(participants) + answers = [_format_answer(res.get("answer")) for res in responses] + answer_text = " ".join(answers) + config = activity.config or {} + options = [str(option).strip() for option in config.get("options", []) if str(option).strip()] + option_stats = [] + if options: + counter = Counter(answers) + option_stats = [ + {"option": option, "count": counter.get(option, 0), "percent": _percent(counter.get(option, 0), response_count)} + for option in options + ] + + correct_answer = _infer_correct_answer(activity) + accuracy = None + if correct_answer and response_count: + accuracy = _percent(sum(1 for answer in answers if answer == correct_answer), response_count) + + common_answers = [answer for answer, _ in Counter(answer for answer in answers if answer).most_common(6)] + keywords = _extract_keywords(answer_text) + mastery_level = _build_mastery_level(activity.activity_type, response_count, accuracy, option_stats) + diagnosis = _build_diagnosis(activity, response_count, participant_count, option_stats, keywords, accuracy) + teaching_suggestions = _build_teaching_suggestions(activity, response_count, option_stats, accuracy, keywords) + payload = { + "activity_id": activity.id, + "title": activity.title, + "activity_type": activity.activity_type, + "question": config.get("question") or activity.description or activity.title, + "response_count": response_count, + "participant_count": participant_count, + "option_stats": option_stats, + "keywords": keywords, + "common_answers": common_answers, + "mastery_level": mastery_level, + "accuracy": accuracy, + "diagnosis": diagnosis, + "teaching_suggestions": teaching_suggestions, + "generated_at": datetime.now(timezone.utc), + } + payload["followup_prompt"] = _build_followup_prompt(activity, payload) + return payload + + +def _public_activity_payload(activity: ClassroomActivity) -> ClassroomActivity: + config = dict(activity.config or {}) + for key in ("answer", "correct_answer", "correctAnswer", "material_context"): + config.pop(key, None) + setattr(activity, "config", config) + return activity + + +def _add_bullets(document: Document, items: list[str]) -> None: + for item in items: + document.add_paragraph(str(item), style="List Bullet") + + +@router.post("/activities", response_model=ClassroomActivityOut, status_code=201) +def create_activity( + data: ClassroomActivityCreate, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + activity = ClassroomActivity(user_id=current_user.id, **data.model_dump()) + db.add(activity) + db.commit() + db.refresh(activity) + return activity + + +@router.get("/activities", response_model=list[ClassroomActivityOut]) +def list_activities( + activity_type: str = "", + skip: int = Query(0, ge=0), + limit: int = Query(20, ge=1, le=100), + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + query = db.query(ClassroomActivity).filter(ClassroomActivity.user_id == current_user.id) + if activity_type: + query = query.filter(ClassroomActivity.activity_type == activity_type) + return query.order_by(ClassroomActivity.updated_at.desc()).offset(skip).limit(limit).all() + + +@router.get("/activities/{activity_id}", response_model=ClassroomActivityOut) +def get_activity( + activity_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + activity = db.query(ClassroomActivity).filter( + ClassroomActivity.id == activity_id, + ClassroomActivity.user_id == current_user.id, + ).first() + if not activity: + raise HTTPException(status_code=404, detail="课堂活动不存在") + return activity + + +@router.get("/activities/{activity_id}/analysis", response_model=ClassroomAnalysisOut) +def analyze_activity( + activity_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + activity = db.query(ClassroomActivity).filter( + ClassroomActivity.id == activity_id, + ClassroomActivity.user_id == current_user.id, + ).first() + if not activity: + raise HTTPException(status_code=404, detail="课堂活动不存在") + return _analysis_payload(activity) + + +@router.get("/activities/{activity_id}/analysis/export") +def export_activity_analysis( + activity_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + activity = db.query(ClassroomActivity).filter( + ClassroomActivity.id == activity_id, + ClassroomActivity.user_id == current_user.id, + ).first() + if not activity: + raise HTTPException(status_code=404, detail="课堂活动不存在") + + analysis = _analysis_payload(activity) + document = Document() + document.add_heading(f"课堂学情诊断报告:{activity.title}", level=1) + document.add_paragraph(f"活动类型:{activity.activity_type}") + document.add_paragraph(f"回收题目:{analysis['question']}") + document.add_paragraph(f"生成时间:{analysis['generated_at'].strftime('%Y-%m-%d %H:%M:%S UTC')}") + + document.add_heading("核心指标", level=2) + document.add_paragraph(f"提交数:{analysis['response_count']}") + document.add_paragraph(f"参与学生数:{analysis['participant_count']}") + document.add_paragraph(f"掌握水平:{analysis['mastery_level']}") + if analysis["accuracy"] is not None: + document.add_paragraph(f"正确率:{analysis['accuracy']}%") + + if analysis["option_stats"]: + document.add_heading("选项分布", level=2) + table = document.add_table(rows=1, cols=3) + table.style = "Table Grid" + hdr = table.rows[0].cells + hdr[0].text = "选项" + hdr[1].text = "人数" + hdr[2].text = "占比" + for item in analysis["option_stats"]: + row = table.add_row().cells + row[0].text = item["option"] + row[1].text = str(item["count"]) + row[2].text = f"{item['percent']}%" + + if analysis["keywords"]: + document.add_heading("高频关键词", level=2) + document.add_paragraph("、".join(f"{item['keyword']}({item['count']})" for item in analysis["keywords"])) + + if analysis["common_answers"]: + document.add_heading("常见回答", level=2) + _add_bullets(document, analysis["common_answers"]) + + document.add_heading("诊断结论", level=2) + _add_bullets(document, analysis["diagnosis"]) + + document.add_heading("讲评建议", level=2) + _add_bullets(document, analysis["teaching_suggestions"]) + + document.add_heading("二次练习提示词", level=2) + document.add_paragraph(analysis["followup_prompt"]) + + buffer = BytesIO() + document.save(buffer) + buffer.seek(0) + filename = _safe_filename(f"{activity.title}-学情诊断报告", ".docx") + return StreamingResponse( + buffer, + media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document", + headers={"Content-Disposition": f"attachment; filename*=UTF-8''{quote(filename)}"}, + ) + + +@router.get("/activities/{activity_id}/responses/export") +def export_activity_responses( + activity_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + activity = db.query(ClassroomActivity).filter( + ClassroomActivity.id == activity_id, + ClassroomActivity.user_id == current_user.id, + ).first() + if not activity: + raise HTTPException(status_code=404, detail="课堂活动不存在") + + analysis = _analysis_payload(activity) + buffer = BytesIO(_build_activity_workbook(activity, analysis)) + filename = _safe_filename(f"{activity.title}-课堂数据回收", ".xls") + return StreamingResponse( + buffer, + media_type="application/vnd.ms-excel", + headers={"Content-Disposition": f"attachment; filename*=UTF-8''{quote(filename)}"}, + ) + + +@router.get("/public/activities/{activity_id}", response_model=ClassroomActivityOut) +def get_public_activity( + activity_id: int, + db: Session = Depends(get_db), +): + activity = db.query(ClassroomActivity).filter(ClassroomActivity.id == activity_id).first() + if not activity or activity.status not in {"active", "closed"}: + raise HTTPException(status_code=404, detail="课堂活动不存在") + return _public_activity_payload(activity) + + +@router.put("/activities/{activity_id}", response_model=ClassroomActivityOut) +def update_activity( + activity_id: int, + data: ClassroomActivityUpdate, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + activity = db.query(ClassroomActivity).filter( + ClassroomActivity.id == activity_id, + ClassroomActivity.user_id == current_user.id, + ).first() + if not activity: + raise HTTPException(status_code=404, detail="课堂活动不存在") + for key, value in data.model_dump(exclude_unset=True).items(): + setattr(activity, key, value) + db.commit() + db.refresh(activity) + return activity + + +@router.post("/activities/{activity_id}/responses", response_model=dict, status_code=201) +def submit_response( + activity_id: int, + data: ClassroomResponseCreate, + db: Session = Depends(get_db), +): + activity = db.query(ClassroomActivity).filter(ClassroomActivity.id == activity_id).first() + if not activity or activity.status == "archived": + raise HTTPException(status_code=404, detail="课堂活动不存在") + if activity.status != "active": + raise HTTPException(status_code=409, detail="课堂活动未发布或已结束") + + responses = list(activity.responses or []) + responses.append({ + "student_name": data.student_name, + "answer": data.answer, + "meta": data.meta, + "submitted_at": datetime.now(timezone.utc).isoformat(), + }) + activity.responses = responses + db.commit() + return {"success": True, "count": len(responses)} + + +@router.delete("/activities/{activity_id}", status_code=204) +def delete_activity( + activity_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + activity = db.query(ClassroomActivity).filter( + ClassroomActivity.id == activity_id, + ClassroomActivity.user_id == current_user.id, + ).first() + if not activity: + raise HTTPException(status_code=404, detail="课堂活动不存在") + db.delete(activity) + db.commit() diff --git a/backend/routers/community.py b/backend/routers/community.py new file mode 100644 index 0000000..845100d --- /dev/null +++ b/backend/routers/community.py @@ -0,0 +1,210 @@ +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy import or_ +from sqlalchemy.orm import Session +from typing import Optional +from database import get_db +from models.community import Post, Comment, PostFavorite +from models.user import User +from schemas.community import PostCreate, PostUpdate, PostOut, CommentCreate, CommentOut +from services.auth import get_current_user, get_optional_current_user +from routers.notification import push_notification + +router = APIRouter(prefix="/api/community", tags=["教师社区"]) + + +def _sync_post_favorite_count(post: Post, db: Session) -> None: + post.likes = db.query(PostFavorite).filter(PostFavorite.post_id == post.id).count() + + +def _serialize_post(post: Post, current_user: User | None, db: Session) -> Post: + is_favorited = False + if current_user: + is_favorited = db.query(PostFavorite).filter( + PostFavorite.user_id == current_user.id, + PostFavorite.post_id == post.id, + ).first() is not None + setattr(post, "is_favorited", is_favorited) + setattr(post, "author", post.author) + return post + + +@router.post("/posts", response_model=PostOut, status_code=201) +def create_post(data: PostCreate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + post = Post(user_id=current_user.id, **data.model_dump()) + db.add(post) + db.commit() + db.refresh(post) + return _serialize_post(post, current_user, db) + + +@router.get("/posts", response_model=list[PostOut]) +def list_posts( + post_type: Optional[str] = None, + keyword: Optional[str] = None, + scope: Optional[str] = None, + skip: int = Query(0, ge=0), + limit: int = Query(20, ge=1, le=100), + current_user: User | None = Depends(get_optional_current_user), + db: Session = Depends(get_db), +): + query = db.query(Post) + if scope in {"mine", "favorite"} and not current_user: + raise HTTPException(status_code=401, detail="请先登录") + if scope == "mine": + query = query.filter(Post.user_id == current_user.id) + elif scope == "favorite": + query = query.join(PostFavorite, PostFavorite.post_id == Post.id).filter( + PostFavorite.user_id == current_user.id, + ) + if post_type: + query = query.filter(Post.post_type == post_type) + if keyword: + query = query.filter(or_( + Post.title.contains(keyword), + Post.content.contains(keyword), + )) + posts = query.order_by(Post.is_pinned.desc(), Post.created_at.desc()).offset(skip).limit(limit).all() + return [_serialize_post(post, current_user, db) for post in posts] + + +@router.get("/posts/ranking", response_model=list[PostOut]) +def ranking_posts( + limit: int = Query(5, ge=1, le=20), + current_user: User | None = Depends(get_optional_current_user), + db: Session = Depends(get_db), +): + score = Post.views + Post.likes * 10 + Post.comments_count * 3 + posts = ( + db.query(Post) + .order_by(Post.is_pinned.desc(), score.desc(), Post.created_at.desc()) + .limit(limit) + .all() + ) + return [_serialize_post(post, current_user, db) for post in posts] + + +@router.get("/posts/{post_id}", response_model=PostOut) +def get_post( + post_id: int, + current_user: User | None = Depends(get_optional_current_user), + db: Session = Depends(get_db), +): + post = db.query(Post).filter(Post.id == post_id).first() + if not post: + raise HTTPException(status_code=404, detail="帖子不存在") + post.views = (post.views or 0) + 1 + db.commit() + db.refresh(post) + return _serialize_post(post, current_user, db) + + +@router.put("/posts/{post_id}", response_model=PostOut) +def update_post( + post_id: int, + data: PostUpdate, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + post = db.query(Post).filter(Post.id == post_id, Post.user_id == current_user.id).first() + if not post: + raise HTTPException(status_code=404, detail="帖子不存在") + update_data = data.model_dump(exclude_unset=True) + for key, value in update_data.items(): + setattr(post, key, value) + db.commit() + db.refresh(post) + return _serialize_post(post, current_user, db) + + +@router.post("/posts/{post_id}/like", response_model=dict) +def like_post( + post_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + post = db.query(Post).filter(Post.id == post_id).first() + if not post: + raise HTTPException(status_code=404, detail="帖子不存在") + favorite = db.query(PostFavorite).filter( + PostFavorite.user_id == current_user.id, + PostFavorite.post_id == post_id, + ).first() + if not favorite: + db.add(PostFavorite(user_id=current_user.id, post_id=post_id)) + db.flush() + push_notification(db, user_id=post.user_id, actor_id=current_user.id, ntype="like", title=f"{current_user.name} 收藏了你的帖子", content=post.title[:60], link=f"/community/{post_id}") + _sync_post_favorite_count(post, db) + db.commit() + return {"success": True, "likes": post.likes, "is_favorited": True} + + +@router.delete("/posts/{post_id}/like", response_model=dict) +def unlike_post( + post_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + post = db.query(Post).filter(Post.id == post_id).first() + if not post: + raise HTTPException(status_code=404, detail="帖子不存在") + favorite = db.query(PostFavorite).filter( + PostFavorite.user_id == current_user.id, + PostFavorite.post_id == post_id, + ).first() + if favorite: + db.delete(favorite) + db.flush() + _sync_post_favorite_count(post, db) + db.commit() + return {"success": True, "likes": post.likes, "is_favorited": False} + + +@router.get("/posts/{post_id}/comments", response_model=list[CommentOut]) +def list_comments(post_id: int, skip: int = Query(0, ge=0), limit: int = Query(50, ge=1, le=200), db: Session = Depends(get_db)): + return db.query(Comment).filter(Comment.post_id == post_id).order_by(Comment.created_at).offset(skip).limit(limit).all() + + +@router.post("/posts/{post_id}/comments", response_model=CommentOut, status_code=201) +def create_comment( + post_id: int, data: CommentCreate, + current_user: User = Depends(get_current_user), db: Session = Depends(get_db), +): + post = db.query(Post).filter(Post.id == post_id).first() + if not post: + raise HTTPException(status_code=404, detail="帖子不存在") + comment = Comment(post_id=post_id, user_id=current_user.id, **data.model_dump()) + db.add(comment) + post.comments_count = (post.comments_count or 0) + 1 + push_notification(db, user_id=post.user_id, actor_id=current_user.id, ntype="comment", title=f"{current_user.name} 评论了你的帖子", content=post.title[:60], link=f"/community/{post_id}") + db.commit() + db.refresh(comment) + return comment + + +@router.delete("/posts/{post_id}/comments/{comment_id}", status_code=204) +def delete_comment( + post_id: int, + comment_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + post = db.query(Post).filter(Post.id == post_id).first() + if not post: + raise HTTPException(status_code=404, detail="帖子不存在") + comment = db.query(Comment).filter(Comment.id == comment_id, Comment.post_id == post_id).first() + if not comment: + raise HTTPException(status_code=404, detail="评论不存在") + if comment.user_id != current_user.id and post.user_id != current_user.id: + raise HTTPException(status_code=403, detail="无权删除该评论") + db.delete(comment) + post.comments_count = max((post.comments_count or 0) - 1, 0) + db.commit() + + +@router.delete("/posts/{post_id}", status_code=204) +def delete_post(post_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + post = db.query(Post).filter(Post.id == post_id, Post.user_id == current_user.id).first() + if not post: + raise HTTPException(status_code=404, detail="帖子不存在") + db.delete(post) + db.commit() diff --git a/backend/routers/courseware.py b/backend/routers/courseware.py new file mode 100644 index 0000000..f6826e3 --- /dev/null +++ b/backend/routers/courseware.py @@ -0,0 +1,317 @@ +import html +import re +import secrets +from io import BytesIO +from urllib.parse import quote + +from docx import Document +from fastapi import APIRouter, Depends, HTTPException, Query, Request +from fastapi.responses import StreamingResponse +from sqlalchemy.orm import Session +from typing import Optional +from database import get_db +from models.courseware import Courseware, CoursewareShare +from models.resource import Resource +from models.user import User +from schemas.courseware import CoursewareCreate, CoursewareAIGenerate, CoursewareShareOut, CoursewareUpdate, CoursewareOut +from services.credits import credits_payload, spend_credits +from services.limiter import limiter +from services.auth import get_current_user, get_optional_current_user +from services.ai_service import AIService + +router = APIRouter(prefix="/api/coursewares", tags=["课件管理"]) +ai_service = AIService() + + +def _safe_filename(name: str, suffix: str) -> str: + stem = re.sub(r"[\\/:*?\"<>|\r\n]+", "_", name).strip(" .") or "课件" + return f"{stem[:80]}{suffix}" + + +def _plain_text_from_html(raw_html: str) -> str: + text = str(raw_html or "") + text = re.sub(r"<(script|style)[\s\S]*?", " ", text, flags=re.IGNORECASE) + text = re.sub(r"", "\n", text, flags=re.IGNORECASE) + text = re.sub(r"", "\n", text, flags=re.IGNORECASE) + text = re.sub(r"<[^>]+>", " ", text) + text = html.unescape(text) + text = re.sub(r"[ \t\u00a0]+", " ", text) + text = re.sub(r"\n\s+", "\n", text) + text = re.sub(r"\n{3,}", "\n\n", text) + return text.strip() + + +def _courseware_or_404( + courseware_id: int, + current_user: User | None, + db: Session, +) -> Courseware: + cw = db.query(Courseware).filter(Courseware.id == courseware_id).first() + if not cw or cw.status == "archived": + raise HTTPException(status_code=404, detail="课件不存在") + if cw.status != "published" and (not current_user or cw.user_id != current_user.id): + raise HTTPException(status_code=403, detail="无权访问该课件") + return cw + + +def _add_if_present(document: Document, label: str, value: str | None) -> None: + if value: + document.add_paragraph(f"{label}:{value}") + + +def _share_payload(share: CoursewareShare, request: Request | None = None) -> dict: + data = CoursewareShareOut.model_validate(share).model_dump() + data["share_url"] = f"/preview/share/{share.token}" + return data + + +@limiter.limit("10/minute") +@router.post("/ai-generate", response_model=dict) +async def ai_generate_courseware( + request: Request, + data: CoursewareAIGenerate, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + account = spend_credits(db, current_user, "courseware_generate", f"生成互动课件:{data.prompt[:80]}") + db.commit() + result = await ai_service.generate_courseware( + prompt=data.prompt, subject=data.subject, grade=data.grade, page_count=data.page_count, + aspect_ratio=data.aspect_ratio, + ) + return {"success": True, "data": result, "credits": credits_payload(account, "courseware_generate")} + + +@router.post("/", response_model=CoursewareOut, status_code=201) +def create_courseware(data: CoursewareCreate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + cw = Courseware(user_id=current_user.id, **data.model_dump()) + db.add(cw) + db.commit() + db.refresh(cw) + return cw + + +@router.get("/", response_model=list[CoursewareOut]) +def list_coursewares( + subject: Optional[str] = None, + grade: Optional[str] = None, + status: Optional[str] = None, + skip: int = Query(0, ge=0), + limit: int = Query(20, ge=1, le=100), + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + query = db.query(Courseware).filter(Courseware.user_id == current_user.id) + if subject: + query = query.filter(Courseware.subject == subject) + if grade: + query = query.filter(Courseware.grade == grade) + if status: + query = query.filter(Courseware.status == status) + else: + query = query.filter(Courseware.status != "archived") + return query.order_by(Courseware.updated_at.desc()).offset(skip).limit(limit).all() + + +@router.get("/shares/mine/list", response_model=list[CoursewareShareOut]) +def list_my_courseware_shares( + request: Request, + skip: int = Query(0, ge=0), + limit: int = Query(20, ge=1, le=100), + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + shares = ( + db.query(CoursewareShare) + .filter(CoursewareShare.user_id == current_user.id, CoursewareShare.status == "active") + .order_by(CoursewareShare.updated_at.desc()) + .offset(skip) + .limit(limit) + .all() + ) + return [_share_payload(share, request) for share in shares] + + +@router.delete("/shares/mine/{share_id}", status_code=204) +def revoke_courseware_share( + share_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + share = db.query(CoursewareShare).filter( + CoursewareShare.id == share_id, + CoursewareShare.user_id == current_user.id, + CoursewareShare.status == "active", + ).first() + if not share: + raise HTTPException(status_code=404, detail="分享不存在") + share.status = "revoked" + db.commit() + + +@router.get("/shares/{token}", response_model=CoursewareShareOut) +def get_courseware_share( + token: str, + request: Request, + db: Session = Depends(get_db), +): + share = db.query(CoursewareShare).filter( + CoursewareShare.token == token, + CoursewareShare.status == "active", + ).first() + if not share: + raise HTTPException(status_code=404, detail="分享不存在或已失效") + share.views = (share.views or 0) + 1 + db.commit() + db.refresh(share) + return _share_payload(share, request) + + +@router.get("/{courseware_id}", response_model=CoursewareOut) +def get_courseware( + courseware_id: int, + current_user: User | None = Depends(get_optional_current_user), + db: Session = Depends(get_db), +): + return _courseware_or_404(courseware_id, current_user, db) + + +@router.post("/{courseware_id}/share", response_model=CoursewareShareOut, status_code=201) +def create_courseware_share( + courseware_id: int, + request: Request, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + cw = db.query(Courseware).filter( + Courseware.id == courseware_id, + Courseware.user_id == current_user.id, + Courseware.status != "archived", + ).first() + if not cw: + raise HTTPException(status_code=404, detail="课件不存在") + share = db.query(CoursewareShare).filter( + CoursewareShare.courseware_id == courseware_id, + CoursewareShare.user_id == current_user.id, + CoursewareShare.status == "active", + ).first() + if not share: + share = CoursewareShare( + courseware_id=courseware_id, + user_id=current_user.id, + token=secrets.token_urlsafe(18), + ) + db.add(share) + share.title = cw.title + share.subject = cw.subject or "" + share.grade = cw.grade or "" + share.description = cw.description or "" + share.content = cw.content or [] + share.tags = list(cw.tags or []) + share.status = "active" + db.commit() + db.refresh(share) + return _share_payload(share, request) + + +@router.get("/{courseware_id}/export-docx") +def export_courseware_docx( + courseware_id: int, + current_user: User | None = Depends(get_optional_current_user), + db: Session = Depends(get_db), +): + cw = _courseware_or_404(courseware_id, current_user, db) + document = Document() + document.add_heading(cw.title or "互动课件讲义", level=1) + + meta_parts = [part for part in [cw.subject, cw.grade] if part] + if meta_parts: + document.add_paragraph(" / ".join(meta_parts)) + _add_if_present(document, "课件说明", cw.description) + if cw.tags: + document.add_paragraph("标签:" + "、".join(str(tag) for tag in cw.tags if str(tag).strip())) + + pages = cw.content or [] + document.add_paragraph(f"页数:{len(pages)}") + + for index, page in enumerate(pages, start=1): + if not isinstance(page, dict): + continue + page_title = str(page.get("title") or f"第 {index} 页").strip() + document.add_heading(f"第 {index} 页:{page_title}", level=2) + page_type = page.get("type") + if page_type: + document.add_paragraph(f"页面类型:{page_type}") + text = _plain_text_from_html(page.get("content") or "") + document.add_paragraph(text or "该页暂无可提取文本。") + notes = str(page.get("notes") or "").strip() + if notes: + document.add_paragraph(f"教师备注:{notes}") + + buffer = BytesIO() + document.save(buffer) + buffer.seek(0) + filename = _safe_filename(f"{cw.title or '课件'}-讲义", ".docx") + return StreamingResponse( + buffer, + media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document", + headers={"Content-Disposition": f"attachment; filename*=UTF-8''{quote(filename)}"}, + ) + + +@router.put("/{courseware_id}", response_model=CoursewareOut) +def update_courseware( + courseware_id: int, data: CoursewareUpdate, + current_user: User = Depends(get_current_user), db: Session = Depends(get_db), +): + cw = db.query(Courseware).filter(Courseware.id == courseware_id, Courseware.user_id == current_user.id).first() + if not cw: + raise HTTPException(status_code=404, detail="课件不存在") + for key, value in data.model_dump(exclude_unset=True).items(): + setattr(cw, key, value) + db.commit() + db.refresh(cw) + return cw + + +@router.post("/{courseware_id}/remix", response_model=CoursewareOut, status_code=201) +def remix_courseware( + courseware_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + source = db.query(Courseware).filter(Courseware.id == courseware_id).first() + if not source or source.status == "archived": + raise HTTPException(status_code=404, detail="课件不存在") + if source.status != "published" and source.user_id != current_user.id: + raise HTTPException(status_code=403, detail="无权改编该课件") + + clone = Courseware( + user_id=current_user.id, + title=f"{source.title}(改编)", + subject=source.subject or "", + grade=source.grade or "", + description=source.description or "", + content=source.content or [], + cover_image=source.cover_image or "", + tags=list(source.tags or []), + status="draft", + ) + db.add(clone) + db.commit() + db.refresh(clone) + return clone + + +@router.delete("/{courseware_id}", status_code=204) +def delete_courseware(courseware_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + cw = db.query(Courseware).filter(Courseware.id == courseware_id, Courseware.user_id == current_user.id).first() + if not cw: + raise HTTPException(status_code=404, detail="课件不存在") + cw.status = "archived" + db.query(Resource).filter( + Resource.user_id == current_user.id, + Resource.content_ref == f"courseware:{courseware_id}", + Resource.status == "active", + ).update({"status": "archived"}, synchronize_session=False) + db.commit() diff --git a/backend/routers/essay.py b/backend/routers/essay.py new file mode 100644 index 0000000..81762b9 --- /dev/null +++ b/backend/routers/essay.py @@ -0,0 +1,83 @@ +from typing import Optional +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session +from database import get_db +from models.essay import EssayGrade +from models.user import User +from schemas.essay import EssayGradeCreate, EssayGradeOut +from services.auth import get_current_user +from services.ai_service import AIService +from services.credits import spend_credits + +router = APIRouter(prefix="/api/essay-grades", tags=["作文批改"]) +ai_service = AIService() + + +@router.post("/", response_model=EssayGradeOut, status_code=201) +async def create_essay_grade( + data: EssayGradeCreate, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + """Submit an essay for AI grading; the result is persisted automatically.""" + account = spend_credits(db, current_user, "essay_grade", f"作文批改:{data.title[:40]}") + db.commit() + + result = await ai_service.grade_essay( + essay_text=data.essay_text, grade_level=data.grade_level, + essay_type=data.essay_type, total_score=data.total_score, + ) + + item = EssayGrade( + user_id=current_user.id, + title=data.title, + essay_text=data.essay_text, + grade_level=data.grade_level, + essay_type=data.essay_type, + total_score=data.total_score, + result=result, + ) + db.add(item) + db.commit() + db.refresh(item) + return item + + +@router.get("/", response_model=list[EssayGradeOut]) +def list_essay_grades( + keyword: Optional[str] = None, + skip: int = Query(0, ge=0), + limit: int = Query(20, ge=1, le=100), + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + query = db.query(EssayGrade).filter(EssayGrade.user_id == current_user.id) + if keyword: + query = query.filter(EssayGrade.title.contains(keyword)) + return query.order_by(EssayGrade.updated_at.desc()).offset(skip).limit(limit).all() + + +@router.get("/{grade_id}", response_model=EssayGradeOut) +def get_essay_grade( + grade_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + item = db.query(EssayGrade).filter(EssayGrade.id == grade_id, EssayGrade.user_id == current_user.id).first() + if not item: + raise HTTPException(status_code=404, detail="批改记录不存在") + return item + + +@router.delete("/{grade_id}", status_code=204) +def delete_essay_grade( + grade_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + item = db.query(EssayGrade).filter(EssayGrade.id == grade_id, EssayGrade.user_id == current_user.id).first() + if not item: + raise HTTPException(status_code=404, detail="批改记录不存在") + db.delete(item) + db.commit() + return None diff --git a/backend/routers/exam.py b/backend/routers/exam.py new file mode 100644 index 0000000..bbecd82 --- /dev/null +++ b/backend/routers/exam.py @@ -0,0 +1,198 @@ +import re +from io import BytesIO +from urllib.parse import quote + +from docx import Document +from fastapi import APIRouter, Depends, HTTPException, Query +from fastapi.responses import StreamingResponse +from sqlalchemy.orm import Session + +from database import get_db +from models.exam import Exam +from models.resource import Resource +from models.user import User +from schemas.exam import ExamCreate, ExamOut, ExamUpdate +from services.auth import get_current_user, get_optional_current_user + +router = APIRouter(prefix="/api/exams", tags=["智能命题"]) + + +def _safe_filename(name: str, suffix: str) -> str: + stem = re.sub(r"[\\/:*?\"<>|\r\n]+", "_", name).strip(" .") or "试卷" + return f"{stem[:80]}{suffix}" + + +def _as_list(value) -> list: + if isinstance(value, list): + return value + if value in (None, ""): + return [] + return [value] + + +def _exam_or_404(exam_id: int, current_user: User | None, db: Session) -> Exam: + exam = db.query(Exam).filter(Exam.id == exam_id).first() + if not exam: + raise HTTPException(status_code=404, detail="试卷不存在") + if (not current_user or exam.user_id != current_user.id) and not has_public_resource(db, exam_id): + raise HTTPException(status_code=403, detail="无权访问该试卷") + return exam + + +def has_public_resource(db: Session, exam_id: int) -> bool: + return db.query(Resource).filter( + Resource.content_ref == f"exam:{exam_id}", + Resource.status == "active", + Resource.is_public == 1, + ).first() is not None + + +@router.post("/", response_model=ExamOut, status_code=201) +def create_exam(data: ExamCreate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + exam = Exam(user_id=current_user.id, **data.model_dump()) + db.add(exam) + db.commit() + db.refresh(exam) + return exam + + +@router.get("/", response_model=list[ExamOut]) +def list_exams( + subject: str = "", + skip: int = Query(0, ge=0), + limit: int = Query(20, ge=1, le=100), + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + query = db.query(Exam).filter(Exam.user_id == current_user.id) + if subject: + query = query.filter(Exam.subject == subject) + return query.order_by(Exam.updated_at.desc()).offset(skip).limit(limit).all() + + +@router.get("/{exam_id}", response_model=ExamOut) +def get_exam( + exam_id: int, + current_user: User | None = Depends(get_optional_current_user), + db: Session = Depends(get_db), +): + return _exam_or_404(exam_id, current_user, db) + + +@router.get("/{exam_id}/export-docx") +def export_exam_docx( + exam_id: int, + current_user: User | None = Depends(get_optional_current_user), + db: Session = Depends(get_db), +): + exam = _exam_or_404(exam_id, current_user, db) + document = Document() + document.add_heading(exam.title or "智能试卷", level=1) + meta = [ + exam.subject, + exam.grade, + f"{exam.duration or 90}分钟", + f"总分{exam.total_score or 100}分", + f"难度:{exam.difficulty or 'medium'}", + ] + if exam.knowledge_points: + meta.append("知识点:" + "、".join(str(item) for item in exam.knowledge_points if str(item).strip())) + document.add_paragraph(" / ".join(str(part) for part in meta if part)) + + for index, question in enumerate(exam.questions or [], start=1): + if not isinstance(question, dict): + document.add_paragraph(f"{index}. {question}") + continue + q_type = question.get("type") or question.get("question_type") or "题目" + score = question.get("score") + heading = f"{index}. [{q_type}]" + if score: + heading += f"({score}分)" + document.add_paragraph(heading) + document.add_paragraph(str(question.get("content") or question.get("question") or question.get("stem") or "")) + options = _as_list(question.get("options")) + for option_index, option in enumerate(options): + prefix = chr(65 + option_index) if option_index < 26 else str(option_index + 1) + document.add_paragraph(f"{prefix}. {option}") + + answers = exam.answers or [question.get("answer") for question in exam.questions or [] if isinstance(question, dict)] + if answers: + document.add_page_break() + document.add_heading("参考答案与解析", level=1) + for index, question in enumerate(exam.questions or [], start=1): + answer = answers[index - 1] if index - 1 < len(answers) else "" + analysis = question.get("analysis") or question.get("explanation") if isinstance(question, dict) else "" + document.add_paragraph(f"{index}. 答案:{answer}") + if analysis: + document.add_paragraph(f"解析:{analysis}") + + buffer = BytesIO() + document.save(buffer) + buffer.seek(0) + filename = _safe_filename(f"{exam.title or '智能试卷'}-试卷", ".docx") + return StreamingResponse( + buffer, + media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document", + headers={"Content-Disposition": f"attachment; filename*=UTF-8''{quote(filename)}"}, + ) + + +@router.put("/{exam_id}", response_model=ExamOut) +def update_exam( + exam_id: int, + data: ExamUpdate, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + exam = db.query(Exam).filter(Exam.id == exam_id, Exam.user_id == current_user.id).first() + if not exam: + raise HTTPException(status_code=404, detail="试卷不存在") + for key, value in data.model_dump(exclude_unset=True).items(): + setattr(exam, key, value) + db.commit() + db.refresh(exam) + return exam + + +@router.post("/{exam_id}/remix", response_model=ExamOut, status_code=201) +def remix_exam( + exam_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + source = db.query(Exam).filter(Exam.id == exam_id).first() + if not source: + raise HTTPException(status_code=404, detail="试卷不存在") + if source.user_id != current_user.id and not has_public_resource(db, exam_id): + raise HTTPException(status_code=403, detail="无权改编该试卷") + + clone = Exam( + user_id=current_user.id, + title=f"{source.title}(改编)", + subject=source.subject or "", + grade=source.grade or "", + questions=list(source.questions or []), + answers=list(source.answers or []), + duration=source.duration or 90, + total_score=source.total_score or 100, + difficulty=source.difficulty or "medium", + knowledge_points=list(source.knowledge_points or []), + ) + db.add(clone) + db.commit() + db.refresh(clone) + return clone + + +@router.delete("/{exam_id}", status_code=204) +def delete_exam(exam_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + exam = db.query(Exam).filter(Exam.id == exam_id, Exam.user_id == current_user.id).first() + if not exam: + raise HTTPException(status_code=404, detail="试卷不存在") + db.query(Resource).filter( + Resource.user_id == current_user.id, + Resource.content_ref == f"exam:{exam_id}", + Resource.status == "active", + ).update({"status": "archived"}, synchronize_session=False) + db.delete(exam) + db.commit() diff --git a/backend/routers/exercise.py b/backend/routers/exercise.py new file mode 100644 index 0000000..d5fafd9 --- /dev/null +++ b/backend/routers/exercise.py @@ -0,0 +1,248 @@ +import re +from io import BytesIO +from urllib.parse import quote + +from docx import Document +from fastapi import APIRouter, Depends, HTTPException, Query, Request +from fastapi.responses import StreamingResponse +from sqlalchemy.orm import Session +from typing import Optional +from database import get_db +from models.exercise import Exercise, ExerciseAttempt +from models.resource import Resource +from models.user import User +from schemas.exercise import ExerciseCreate, ExerciseAIGenerate, ExerciseAttemptCreate, ExerciseAttemptOut, ExerciseOut, ExerciseUpdate +from services.credits import credits_payload, spend_credits +from services.limiter import limiter +from services.auth import get_current_user, get_optional_current_user +from services.ai_service import AIService + +router = APIRouter(prefix="/api/exercises", tags=["互动练习"]) +ai_service = AIService() + + +def _safe_filename(name: str, suffix: str) -> str: + stem = re.sub(r"[\\/:*?\"<>|\r\n]+", "_", name).strip(" .") or "课堂练习" + return f"{stem[:80]}{suffix}" + + +def _as_list(value) -> list: + if isinstance(value, list): + return value + if value in (None, ""): + return [] + return [value] + + +def _exercise_or_404(exercise_id: int, current_user: User | None, db: Session) -> Exercise: + ex = db.query(Exercise).filter(Exercise.id == exercise_id).first() + if not ex: + raise HTTPException(status_code=404, detail="练习不存在") + if (not current_user or ex.user_id != current_user.id) and not has_public_resource(db, exercise_id): + raise HTTPException(status_code=403, detail="无权访问该练习") + return ex + + +def has_public_resource(db: Session, exercise_id: int) -> bool: + return db.query(Resource).filter( + Resource.content_ref == f"exercise:{exercise_id}", + Resource.status == "active", + Resource.is_public == 1, + ).first() is not None + + +@limiter.limit("10/minute") +@router.post("/ai-generate", response_model=dict) +async def ai_generate_exercise( + request: Request, + data: ExerciseAIGenerate, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + account = spend_credits(db, current_user, "exercise_generate", f"生成互动练习:{data.prompt[:80]}") + db.commit() + result = await ai_service.generate_exercise( + prompt=data.prompt, exercise_type=data.exercise_type, + subject=data.subject, knowledge_points=data.knowledge_points, count=data.count, + ) + return {"success": True, "data": result, "credits": credits_payload(account, "exercise_generate")} + + +@router.post("/", response_model=ExerciseOut, status_code=201) +def create_exercise(data: ExerciseCreate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + ex = Exercise(user_id=current_user.id, **data.model_dump()) + db.add(ex) + db.commit() + db.refresh(ex) + return ex + + +@router.get("/", response_model=list[ExerciseOut]) +def list_exercises( + exercise_type: Optional[str] = None, + subject: Optional[str] = None, + skip: int = Query(0, ge=0), + limit: int = Query(20, ge=1, le=100), + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + query = db.query(Exercise).filter(Exercise.user_id == current_user.id) + if exercise_type: + query = query.filter(Exercise.exercise_type == exercise_type) + if subject: + query = query.filter(Exercise.subject == subject) + return query.order_by(Exercise.updated_at.desc()).offset(skip).limit(limit).all() + + +@router.get("/{exercise_id}", response_model=ExerciseOut) +def get_exercise( + exercise_id: int, + current_user: User | None = Depends(get_optional_current_user), + db: Session = Depends(get_db), +): + return _exercise_or_404(exercise_id, current_user, db) + + +@router.get("/{exercise_id}/export-docx") +def export_exercise_docx( + exercise_id: int, + current_user: User | None = Depends(get_optional_current_user), + db: Session = Depends(get_db), +): + ex = _exercise_or_404(exercise_id, current_user, db) + document = Document() + document.add_heading(ex.title or "课堂练习", level=1) + meta_parts = [ex.subject, ex.exercise_type, f"{len(ex.questions or [])}题"] + if ex.knowledge_points: + meta_parts.append("知识点:" + "、".join(str(item) for item in ex.knowledge_points if str(item).strip())) + document.add_paragraph(" / ".join(str(part) for part in meta_parts if part)) + + for index, question in enumerate(ex.questions or [], start=1): + if not isinstance(question, dict): + document.add_paragraph(f"{index}. {question}") + continue + q_type = question.get("type") or ex.exercise_type or "练习" + difficulty = question.get("difficulty") + heading = f"{index}. [{q_type}]" + if difficulty: + heading += f"({difficulty})" + document.add_paragraph(heading) + document.add_paragraph(str(question.get("question") or question.get("content") or question.get("stem") or "")) + options = _as_list(question.get("options")) + for option_index, option in enumerate(options): + prefix = chr(65 + option_index) if option_index < 26 else str(option_index + 1) + document.add_paragraph(f"{prefix}. {option}") + answer = question.get("answer") + if answer not in (None, ""): + document.add_paragraph(f"答案:{answer}") + explanation = question.get("explanation") or question.get("analysis") + if explanation: + document.add_paragraph(f"解析:{explanation}") + + buffer = BytesIO() + document.save(buffer) + buffer.seek(0) + filename = _safe_filename(f"{ex.title or '课堂练习'}-练习", ".docx") + return StreamingResponse( + buffer, + media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document", + headers={"Content-Disposition": f"attachment; filename*=UTF-8''{quote(filename)}"}, + ) + + +@router.put("/{exercise_id}", response_model=ExerciseOut) +def update_exercise( + exercise_id: int, + data: ExerciseUpdate, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + ex = db.query(Exercise).filter(Exercise.id == exercise_id, Exercise.user_id == current_user.id).first() + if not ex: + raise HTTPException(status_code=404, detail="练习不存在") + for key, value in data.model_dump(exclude_unset=True).items(): + setattr(ex, key, value) + db.commit() + db.refresh(ex) + return ex + + +@router.post("/{exercise_id}/remix", response_model=ExerciseOut, status_code=201) +def remix_exercise( + exercise_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + source = db.query(Exercise).filter(Exercise.id == exercise_id).first() + if not source: + raise HTTPException(status_code=404, detail="练习不存在") + if source.user_id != current_user.id and not has_public_resource(db, exercise_id): + raise HTTPException(status_code=403, detail="无权改编该练习") + + clone = Exercise( + user_id=current_user.id, + title=f"{source.title}(改编)", + exercise_type=source.exercise_type or "choice", + subject=source.subject or "", + knowledge_points=list(source.knowledge_points or []), + questions=list(source.questions or []), + settings=dict(source.settings or {}), + ) + db.add(clone) + db.commit() + db.refresh(clone) + return clone + + +@router.delete("/{exercise_id}", status_code=204) +def delete_exercise(exercise_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + ex = db.query(Exercise).filter(Exercise.id == exercise_id, Exercise.user_id == current_user.id).first() + if not ex: + raise HTTPException(status_code=404, detail="练习不存在") + db.query(Resource).filter( + Resource.user_id == current_user.id, + Resource.content_ref == f"exercise:{exercise_id}", + Resource.status == "active", + ).update({"status": "archived"}, synchronize_session=False) + db.delete(ex) + db.commit() + + +@router.post("/{exercise_id}/attempt", response_model=dict, status_code=201) +def submit_attempt(exercise_id: int, data: ExerciseAttemptCreate, db: Session = Depends(get_db)): + exercise = db.query(Exercise).filter(Exercise.id == exercise_id).first() + if not exercise: + raise HTTPException(status_code=404, detail="练习不存在") + attempt = ExerciseAttempt( + exercise_id=exercise_id, + student_name=data.student_name, + answers=data.answers, + score=data.score, + total=data.total, + duration=data.duration, + ) + db.add(attempt) + db.commit() + db.refresh(attempt) + return {"success": True, "id": attempt.id} + + +@router.get("/{exercise_id}/attempts", response_model=list[ExerciseAttemptOut]) +def list_attempts( + exercise_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + exercise = db.query(Exercise).filter( + Exercise.id == exercise_id, + Exercise.user_id == current_user.id, + ).first() + if not exercise: + raise HTTPException(status_code=404, detail="练习不存在") + return ( + db.query(ExerciseAttempt) + .filter(ExerciseAttempt.exercise_id == exercise_id) + .order_by(ExerciseAttempt.created_at.desc()) + .limit(200) + .all() + ) diff --git a/backend/routers/lesson_plan.py b/backend/routers/lesson_plan.py new file mode 100644 index 0000000..71130ab --- /dev/null +++ b/backend/routers/lesson_plan.py @@ -0,0 +1,245 @@ +import re +from io import BytesIO +from urllib.parse import quote + +from docx import Document +from fastapi import APIRouter, Depends, HTTPException, Query, Request +from fastapi.responses import StreamingResponse +from sqlalchemy.orm import Session +from database import get_db +from models.lesson_plan import LessonPlan +from models.resource import Resource +from models.user import User +from schemas.lesson_plan import LessonPlanGenerate, LessonPlanCreate, LessonPlanOut, LessonPlanUpdate +from services.credits import credits_payload, spend_credits +from services.limiter import limiter +from services.auth import get_current_user, get_optional_current_user +from services.ai_service import AIService + +router = APIRouter(prefix="/api/lesson-plans", tags=["教案管理"]) +ai_service = AIService() + + +def _safe_filename(name: str, suffix: str) -> str: + stem = re.sub(r"[\\/:*?\"<>|\r\n]+", "_", name).strip(" .") or "教案" + return f"{stem[:80]}{suffix}" + + +def _as_list(value) -> list: + if isinstance(value, list): + return value + if value in (None, ""): + return [] + return [value] + + +def _add_bullets(document: Document, items: list) -> None: + for item in items: + if isinstance(item, dict): + text = ";".join(f"{key}:{value}" for key, value in item.items() if value not in (None, "", [])) + else: + text = str(item) + if text.strip(): + document.add_paragraph(text.strip(), style="List Bullet") + + +def _lesson_plan_or_404(plan_id: int, current_user: User | None, db: Session) -> LessonPlan: + lp = db.query(LessonPlan).filter(LessonPlan.id == plan_id).first() + if not lp: + raise HTTPException(status_code=404, detail="教案不存在") + if (not current_user or lp.user_id != current_user.id) and not has_public_resource(db, plan_id): + raise HTTPException(status_code=403, detail="无权访问该教案") + return lp + + +def has_public_resource(db: Session, plan_id: int) -> bool: + return db.query(Resource).filter( + Resource.content_ref == f"lesson_plan:{plan_id}", + Resource.status == "active", + Resource.is_public == 1, + ).first() is not None + + +@limiter.limit("10/minute") +@router.post("/ai-generate", response_model=dict) +async def ai_generate_lesson_plan( + request: Request, + data: LessonPlanGenerate, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + account = spend_credits(db, current_user, "lesson_plan_generate", f"生成教案:{data.title[:80]}") + db.commit() + result = await ai_service.generate_lesson_plan( + title=data.title, subject=data.subject, grade=data.grade, + objectives=data.objectives, duration=data.duration, + extra_requirements=data.extra_requirements, + ) + return {"success": True, "data": result, "credits": credits_payload(account, "lesson_plan_generate")} + + +@router.post("/", response_model=LessonPlanOut, status_code=201) +def create_lesson_plan(data: LessonPlanCreate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + lp = LessonPlan(user_id=current_user.id, **data.model_dump()) + db.add(lp) + db.commit() + db.refresh(lp) + return lp + + +@router.get("/", response_model=list[LessonPlanOut]) +def list_lesson_plans( + subject: str = None, + skip: int = Query(0, ge=0), + limit: int = Query(20, ge=1, le=100), + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + query = db.query(LessonPlan).filter(LessonPlan.user_id == current_user.id) + if subject: + query = query.filter(LessonPlan.subject == subject) + return query.order_by(LessonPlan.updated_at.desc()).offset(skip).limit(limit).all() + + +@router.get("/{plan_id}", response_model=LessonPlanOut) +def get_lesson_plan( + plan_id: int, + current_user: User | None = Depends(get_optional_current_user), + db: Session = Depends(get_db), +): + return _lesson_plan_or_404(plan_id, current_user, db) + + +@router.get("/{plan_id}/export-docx") +def export_lesson_plan_docx( + plan_id: int, + current_user: User | None = Depends(get_optional_current_user), + db: Session = Depends(get_db), +): + lp = _lesson_plan_or_404(plan_id, current_user, db) + content = lp.content or {} + + document = Document() + document.add_heading(lp.title or content.get("title") or "大单元教案", level=1) + meta = " / ".join(str(part) for part in [lp.subject, lp.grade, f"{lp.duration or 45}分钟"] if part) + if meta: + document.add_paragraph(meta) + + sections = [ + ("教学目标", lp.objectives or content.get("objectives")), + ("教学重点", lp.key_points or content.get("key_points")), + ("教学难点", lp.difficulties or content.get("difficulties")), + ("教学材料", lp.materials or content.get("materials")), + ] + for title, items in sections: + items = _as_list(items) + if items: + document.add_heading(title, level=2) + _add_bullets(document, items) + + phases = _as_list(content.get("phases") or content.get("steps")) + if phases: + document.add_heading("教学过程", level=2) + for index, phase in enumerate(phases, start=1): + if not isinstance(phase, dict): + document.add_paragraph(f"{index}. {phase}") + continue + name = phase.get("name") or phase.get("title") or f"教学环节 {index}" + duration = phase.get("duration") + heading = f"{index}. {name}" + if duration: + heading += f"({duration}分钟)" + document.add_heading(heading, level=3) + for label, key in [ + ("教学活动", "activities"), + ("教师行为", "teacher_actions"), + ("学生行为", "student_actions"), + ("资源材料", "resources"), + ("评价方式", "assessment"), + ]: + items = _as_list(phase.get(key)) + if items: + document.add_paragraph(label) + _add_bullets(document, items) + + homework = _as_list(lp.homework or content.get("homework")) + if homework: + document.add_heading("课后作业", level=2) + _add_bullets(document, homework) + + reflection = content.get("reflection") + if reflection: + document.add_heading("教学反思", level=2) + document.add_paragraph(str(reflection)) + + buffer = BytesIO() + document.save(buffer) + buffer.seek(0) + filename = _safe_filename(f"{lp.title or '大单元教案'}-教案", ".docx") + return StreamingResponse( + buffer, + media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document", + headers={"Content-Disposition": f"attachment; filename*=UTF-8''{quote(filename)}"}, + ) + + +@router.put("/{plan_id}", response_model=LessonPlanOut) +def update_lesson_plan( + plan_id: int, + data: LessonPlanUpdate, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + lp = db.query(LessonPlan).filter(LessonPlan.id == plan_id, LessonPlan.user_id == current_user.id).first() + if not lp: + raise HTTPException(status_code=404, detail="教案不存在") + for key, value in data.model_dump(exclude_unset=True).items(): + setattr(lp, key, value) + db.commit() + db.refresh(lp) + return lp + + +@router.post("/{plan_id}/remix", response_model=LessonPlanOut, status_code=201) +def remix_lesson_plan( + plan_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + source = db.query(LessonPlan).filter(LessonPlan.id == plan_id).first() + if not source: + raise HTTPException(status_code=404, detail="教案不存在") + if source.user_id != current_user.id and not has_public_resource(db, plan_id): + raise HTTPException(status_code=403, detail="无权改编该教案") + + clone = LessonPlan( + user_id=current_user.id, + title=f"{source.title}(改编)", + subject=source.subject or "", + grade=source.grade or "", + objectives=list(source.objectives or []), + key_points=list(source.key_points or []), + difficulties=list(source.difficulties or []), + content=dict(source.content or {}), + duration=source.duration or 45, + materials=list(source.materials or []), + homework=list(source.homework or []), + ) + db.add(clone) + db.commit() + db.refresh(clone) + return clone + + +@router.delete("/{plan_id}", status_code=204) +def delete_lesson_plan(plan_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + lp = db.query(LessonPlan).filter(LessonPlan.id == plan_id, LessonPlan.user_id == current_user.id).first() + if not lp: + raise HTTPException(status_code=404, detail="教案不存在") + db.query(Resource).filter( + Resource.user_id == current_user.id, + Resource.content_ref == f"lesson_plan:{plan_id}", + Resource.status == "active", + ).update({"status": "archived"}, synchronize_session=False) + db.delete(lp) + db.commit() diff --git a/backend/routers/material.py b/backend/routers/material.py new file mode 100644 index 0000000..3fdc5be --- /dev/null +++ b/backend/routers/material.py @@ -0,0 +1,98 @@ +from typing import Optional + +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy import or_ +from sqlalchemy.orm import Session + +from database import get_db +from models.material import Material +from models.user import User +from schemas.material import MaterialCreate, MaterialOut, MaterialUpdate +from services.auth import get_current_user + +router = APIRouter(prefix="/api/materials", tags=["素材库"]) + + +@router.post("/", response_model=MaterialOut, status_code=201) +def create_material( + data: MaterialCreate, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + material = Material(user_id=current_user.id, **data.model_dump()) + db.add(material) + db.commit() + db.refresh(material) + return material + + +@router.get("/", response_model=list[MaterialOut]) +def list_materials( + material_type: Optional[str] = None, + subject: Optional[str] = None, + keyword: Optional[str] = None, + skip: int = Query(0, ge=0), + limit: int = Query(20, ge=1, le=100), + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + query = db.query(Material).filter(Material.user_id == current_user.id, Material.status == "active") + if material_type: + query = query.filter(Material.material_type == material_type) + if subject: + query = query.filter(Material.subject == subject) + if keyword: + query = query.filter(or_( + Material.title.contains(keyword), + Material.filename.contains(keyword), + Material.summary.contains(keyword), + Material.subject.contains(keyword), + Material.grade.contains(keyword), + )) + return query.order_by(Material.updated_at.desc()).offset(skip).limit(limit).all() + + +@router.get("/{material_id}", response_model=MaterialOut) +def get_material( + material_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + material = db.query(Material).filter( + Material.id == material_id, + Material.user_id == current_user.id, + Material.status == "active", + ).first() + if not material: + raise HTTPException(status_code=404, detail="素材不存在") + return material + + +@router.put("/{material_id}", response_model=MaterialOut) +def update_material( + material_id: int, + data: MaterialUpdate, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + material = db.query(Material).filter(Material.id == material_id, Material.user_id == current_user.id).first() + if not material or material.status == "archived": + raise HTTPException(status_code=404, detail="素材不存在") + for key, value in data.model_dump(exclude_unset=True).items(): + setattr(material, key, value) + db.commit() + db.refresh(material) + return material + + +@router.delete("/{material_id}", status_code=204) +def delete_material( + material_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + material = db.query(Material).filter(Material.id == material_id, Material.user_id == current_user.id).first() + if not material or material.status == "archived": + raise HTTPException(status_code=404, detail="素材不存在") + material.status = "archived" + db.commit() diff --git a/backend/routers/mindmap.py b/backend/routers/mindmap.py new file mode 100644 index 0000000..f30a5b6 --- /dev/null +++ b/backend/routers/mindmap.py @@ -0,0 +1,108 @@ +from typing import Optional +from fastapi import APIRouter, Depends, HTTPException, Query, Request +from sqlalchemy.orm import Session +from database import get_db +from models.mindmap import MindMap +from models.user import User +from schemas.mindmap import ( + MindMapGenerate, MindMapCreate, MindMapUpdate, MindMapOut, +) +from services.credits import credits_payload, spend_credits +from services.limiter import limiter +from services.auth import get_current_user +from services.ai_service import AIService + +router = APIRouter(prefix="/api/mind-maps", tags=["思维导图管理"]) +ai_service = AIService() + + +@limiter.limit("10/minute") +@router.post("/ai-generate", response_model=dict) +async def ai_generate_mind_map( + request: Request, + data: MindMapGenerate, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + account = spend_credits(db, current_user, "mindmap_generate", f"生成思维导图:{data.topic[:80]}") + db.commit() + result = await ai_service.generate_mindmap( + topic=data.topic, subject=data.subject, grade=data.grade, + ) + return {"success": True, "data": result, "credits": credits_payload(account, "mindmap_generate")} + + +@router.post("/", response_model=MindMapOut, status_code=201) +def create_mind_map( + data: MindMapCreate, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + item = MindMap(user_id=current_user.id, **data.model_dump()) + db.add(item) + db.commit() + db.refresh(item) + return item + + +@router.get("/", response_model=list[MindMapOut]) +def list_mind_maps( + keyword: Optional[str] = None, + skip: int = Query(0, ge=0), + limit: int = Query(20, ge=1, le=100), + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + query = db.query(MindMap).filter(MindMap.user_id == current_user.id) + if keyword: + query = query.filter(MindMap.title.contains(keyword)) + return query.order_by(MindMap.updated_at.desc()).offset(skip).limit(limit).all() + + +@router.get("/{map_id}", response_model=MindMapOut) +def get_mind_map( + map_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + item = db.query(MindMap).filter( + MindMap.id == map_id, MindMap.user_id == current_user.id, + ).first() + if not item: + raise HTTPException(status_code=404, detail="思维导图不存在") + return item + + +@router.put("/{map_id}", response_model=MindMapOut) +def update_mind_map( + map_id: int, + data: MindMapUpdate, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + item = db.query(MindMap).filter( + MindMap.id == map_id, MindMap.user_id == current_user.id, + ).first() + if not item: + raise HTTPException(status_code=404, detail="思维导图不存在") + for key, value in data.model_dump(exclude_unset=True).items(): + setattr(item, key, value) + db.commit() + db.refresh(item) + return item + + +@router.delete("/{map_id}", status_code=204) +def delete_mind_map( + map_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + item = db.query(MindMap).filter( + MindMap.id == map_id, MindMap.user_id == current_user.id, + ).first() + if not item: + raise HTTPException(status_code=404, detail="思维导图不存在") + db.delete(item) + db.commit() + return None diff --git a/backend/routers/notification.py b/backend/routers/notification.py new file mode 100644 index 0000000..aa5add4 --- /dev/null +++ b/backend/routers/notification.py @@ -0,0 +1,101 @@ +from typing import Optional +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session +from database import get_db +from models.notification import Notification +from models.user import User +from schemas.notification import NotificationOut +from services.auth import get_current_user + +router = APIRouter(prefix="/api/notifications", tags=["通知"]) + + +def push_notification( + db: Session, + user_id: int, + actor_id: Optional[int], + ntype: str, + title: str, + content: str = "", + link: str = "", +) -> None: + """创建一条通知,不通知自己。""" + if actor_id and actor_id == user_id: + return + db.add(Notification( + user_id=user_id, actor_id=actor_id, ntype=ntype, + title=title, content=content, link=link, is_read=False, + )) + + +@router.get("", response_model=list[NotificationOut]) +def list_notifications( + skip: int = Query(0, ge=0), + limit: int = Query(20, ge=1, le=100), + unread_only: bool = Query(False), + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + query = db.query(Notification).filter(Notification.user_id == current_user.id) + if unread_only: + query = query.filter(Notification.is_read == False) + return query.order_by(Notification.created_at.desc()).offset(skip).limit(limit).all() + + +@router.get("/unread-count") +def unread_count( + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + count = db.query(Notification).filter( + Notification.user_id == current_user.id, + Notification.is_read == False, + ).count() + return {"count": count} + + +@router.put("/{notification_id}/read", response_model=dict) +def mark_read( + notification_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + item = db.query(Notification).filter( + Notification.id == notification_id, + Notification.user_id == current_user.id, + ).first() + if not item: + raise HTTPException(status_code=404, detail="通知不存在") + item.is_read = True + db.commit() + return {"success": True} + + +@router.put("/read-all", response_model=dict) +def mark_all_read( + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + db.query(Notification).filter( + Notification.user_id == current_user.id, + Notification.is_read == False, + ).update({"is_read": True}, synchronize_session=False) + db.commit() + return {"success": True} + + +@router.delete("/{notification_id}", status_code=204) +def delete_notification( + notification_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + item = db.query(Notification).filter( + Notification.id == notification_id, + Notification.user_id == current_user.id, + ).first() + if not item: + raise HTTPException(status_code=404, detail="通知不存在") + db.delete(item) + db.commit() + return None diff --git a/backend/routers/resource.py b/backend/routers/resource.py new file mode 100644 index 0000000..a8c068e --- /dev/null +++ b/backend/routers/resource.py @@ -0,0 +1,267 @@ +from fastapi import APIRouter, Depends, HTTPException, Query, Request +from sqlalchemy import or_ +from sqlalchemy.orm import Session +from typing import Optional +from database import get_db +from models.resource import Resource, ResourceFavorite +from models.user import User +from schemas.resource import ResourceCreate, ResourceOut, ResourceUpdate +from services.auth import get_current_user, get_optional_current_user +from routers.notification import push_notification +from services.audit import log_action + +router = APIRouter(prefix="/api/resources", tags=["资源库"]) + + +def _sync_favorite_count(res: Resource, db: Session) -> None: + res.likes = db.query(ResourceFavorite).filter(ResourceFavorite.resource_id == res.id).count() + + +def _serialize_resource(res: Resource, current_user: User | None, db: Session) -> Resource: + is_favorited = False + if current_user: + is_favorited = db.query(ResourceFavorite).filter( + ResourceFavorite.user_id == current_user.id, + ResourceFavorite.resource_id == res.id, + ).first() is not None + setattr(res, "is_favorited", is_favorited) + setattr(res, "author", res.author) + return res + + + +def _apply_sort(query, sort: str | None): + sort = (sort or "latest").lower() + if sort == "popular": + return query.order_by(Resource.views.desc(), Resource.created_at.desc()) + if sort in {"downloads", "download"}: + return query.order_by(Resource.downloads.desc(), Resource.created_at.desc()) + if sort in {"likes", "like"}: + return query.order_by(Resource.likes.desc(), Resource.created_at.desc()) + return query.order_by(Resource.created_at.desc()) + +@router.post("/", response_model=ResourceOut, status_code=201) +def create_resource(data: ResourceCreate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + res = Resource(user_id=current_user.id, **data.model_dump()) + db.add(res) + db.commit() + db.refresh(res) + return _serialize_resource(res, current_user, db) + + +@router.post("/publish", response_model=ResourceOut, status_code=201) +def publish_resource(request: Request, data: ResourceCreate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + query = db.query(Resource).filter(Resource.user_id == current_user.id) + if data.content_ref: + query = query.filter(Resource.content_ref == data.content_ref) + else: + query = query.filter(Resource.title == data.title, Resource.resource_type == data.resource_type) + res = query.first() + is_new = res is None + if res: + for key, value in data.model_dump().items(): + setattr(res, key, value) + res.status = "active" + else: + res = Resource(user_id=current_user.id, status="active", **data.model_dump()) + db.add(res) + db.commit() + db.refresh(res) + log_action(db, action="publish_resource", user=current_user, request=request, target_type="resource", target_id=res.id, detail=f"{'新建' if is_new else '更新'}资源《{res.title}》({res.resource_type})") + return _serialize_resource(res, current_user, db) + + +@router.get("/", response_model=list[ResourceOut]) +def list_resources( + resource_type: Optional[str] = None, + subject: Optional[str] = None, + keyword: Optional[str] = None, + sort: Optional[str] = Query("latest"), + skip: int = Query(0, ge=0), + limit: int = Query(20, ge=1, le=100), + current_user: User | None = Depends(get_optional_current_user), + db: Session = Depends(get_db), +): + query = db.query(Resource).filter(Resource.status == "active") + if resource_type: + query = query.filter(Resource.resource_type == resource_type) + if subject: + query = query.filter(Resource.subject == subject) + if keyword: + query = query.filter(or_( + Resource.title.contains(keyword), + Resource.description.contains(keyword), + Resource.subject.contains(keyword), + Resource.grade.contains(keyword), + )) + if current_user: + query = query.filter((Resource.user_id == current_user.id) | (Resource.is_public == 1)) + else: + query = query.filter(Resource.is_public == 1) + resources = _apply_sort(query, sort).offset(skip).limit(limit).all() + return [_serialize_resource(res, current_user, db) for res in resources] + + +@router.put("/{resource_id}", response_model=ResourceOut) +def update_resource( + resource_id: int, + data: ResourceUpdate, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + res = db.query(Resource).filter(Resource.id == resource_id, Resource.user_id == current_user.id).first() + if not res: + raise HTTPException(status_code=404, detail="资源不存在") + for key, value in data.model_dump(exclude_unset=True).items(): + setattr(res, key, value) + db.commit() + db.refresh(res) + return _serialize_resource(res, current_user, db) + + +@router.get("/favorites/me", response_model=list[ResourceOut]) +def list_favorite_resources( + resource_type: Optional[str] = None, + subject: Optional[str] = None, + keyword: Optional[str] = None, + sort: Optional[str] = Query("latest"), + skip: int = Query(0, ge=0), + limit: int = Query(20, ge=1, le=100), + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + query = ( + db.query(Resource) + .join(ResourceFavorite, ResourceFavorite.resource_id == Resource.id) + .filter(ResourceFavorite.user_id == current_user.id, Resource.status == "active") + ) + if resource_type: + query = query.filter(Resource.resource_type == resource_type) + if subject: + query = query.filter(Resource.subject == subject) + if keyword: + query = query.filter(or_( + Resource.title.contains(keyword), + Resource.description.contains(keyword), + Resource.subject.contains(keyword), + Resource.grade.contains(keyword), + )) + order = ResourceFavorite.created_at.desc() if (sort or "latest") == "latest" else None + resources = (query.order_by(order) if order is not None else _apply_sort(query, sort)).offset(skip).limit(limit).all() + return [_serialize_resource(res, current_user, db) for res in resources] + + +@router.get("/mine", response_model=list[ResourceOut]) +def list_my_resources( + resource_type: Optional[str] = None, + subject: Optional[str] = None, + keyword: Optional[str] = None, + sort: Optional[str] = Query("latest"), + skip: int = Query(0, ge=0), + limit: int = Query(20, ge=1, le=100), + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + query = db.query(Resource).filter(Resource.user_id == current_user.id, Resource.status == "active") + if resource_type: + query = query.filter(Resource.resource_type == resource_type) + if subject: + query = query.filter(Resource.subject == subject) + if keyword: + query = query.filter(or_( + Resource.title.contains(keyword), + Resource.description.contains(keyword), + Resource.subject.contains(keyword), + Resource.grade.contains(keyword), + )) + resources = (_apply_sort(query, sort) if (sort or "latest") != "latest" else query.order_by(Resource.updated_at.desc())).offset(skip).limit(limit).all() + return [_serialize_resource(res, current_user, db) for res in resources] + + +@router.get("/{resource_id}", response_model=ResourceOut) +def get_resource( + resource_id: int, + current_user: User | None = Depends(get_optional_current_user), + db: Session = Depends(get_db), +): + res = db.query(Resource).filter(Resource.id == resource_id).first() + if not res or res.status == "archived": + raise HTTPException(status_code=404, detail="资源不存在") + if not res.is_public and (not current_user or res.user_id != current_user.id): + raise HTTPException(status_code=403, detail="无权访问该资源") + res.views = (res.views or 0) + 1 + db.commit() + db.refresh(res) + return _serialize_resource(res, current_user, db) + + +@router.post("/{resource_id}/download", response_model=dict) +def download_resource( + resource_id: int, + current_user: User | None = Depends(get_optional_current_user), + db: Session = Depends(get_db), +): + res = db.query(Resource).filter(Resource.id == resource_id, Resource.status == "active").first() + if not res: + raise HTTPException(status_code=404, detail="资源不存在") + if not res.is_public and (not current_user or res.user_id != current_user.id): + raise HTTPException(status_code=403, detail="无权访问该资源") + res.downloads = (res.downloads or 0) + 1 + db.commit() + return {"success": True, "downloads": res.downloads, "file_url": res.file_url} + + +@router.post("/{resource_id}/like", response_model=dict) +def like_resource( + resource_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + res = db.query(Resource).filter(Resource.id == resource_id, Resource.status == "active").first() + if not res: + raise HTTPException(status_code=404, detail="资源不存在") + if not res.is_public and res.user_id != current_user.id: + raise HTTPException(status_code=403, detail="无权访问该资源") + favorite = db.query(ResourceFavorite).filter( + ResourceFavorite.user_id == current_user.id, + ResourceFavorite.resource_id == resource_id, + ).first() + if not favorite: + db.add(ResourceFavorite(user_id=current_user.id, resource_id=resource_id)) + db.flush() + push_notification(db, user_id=res.user_id, actor_id=current_user.id, ntype="like", title=f"{current_user.name} 收藏了你的资源", content=res.title[:60], link=f"/resources/{res.id}") + _sync_favorite_count(res, db) + db.commit() + return {"success": True, "likes": res.likes, "is_favorited": True} + + +@router.delete("/{resource_id}/like", response_model=dict) +def unlike_resource( + resource_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + res = db.query(Resource).filter(Resource.id == resource_id, Resource.status == "active").first() + if not res: + raise HTTPException(status_code=404, detail="资源不存在") + if not res.is_public and res.user_id != current_user.id: + raise HTTPException(status_code=403, detail="无权访问该资源") + favorite = db.query(ResourceFavorite).filter( + ResourceFavorite.user_id == current_user.id, + ResourceFavorite.resource_id == resource_id, + ).first() + if favorite: + db.delete(favorite) + db.flush() + _sync_favorite_count(res, db) + db.commit() + return {"success": True, "likes": res.likes, "is_favorited": False} + + +@router.delete("/{resource_id}", status_code=204) +def delete_resource(resource_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + res = db.query(Resource).filter(Resource.id == resource_id, Resource.user_id == current_user.id).first() + if not res: + raise HTTPException(status_code=404, detail="资源不存在") + res.status = "archived" + db.commit() diff --git a/backend/routers/search.py b/backend/routers/search.py new file mode 100644 index 0000000..c333274 --- /dev/null +++ b/backend/routers/search.py @@ -0,0 +1,301 @@ +from fastapi import APIRouter, Depends, Query +from sqlalchemy import or_ +from sqlalchemy.orm import Session + +from database import get_db +from models.animation import Animation +from models.classroom import ClassroomActivity +from models.community import Post +from models.courseware import Courseware +from models.exam import Exam +from models.exercise import Exercise +from models.lesson_plan import LessonPlan +from models.material import Material +from models.mindmap import MindMap +from models.resource import Resource +from models.user import User +from schemas.search import SearchResponse +from services.auth import get_optional_current_user + +router = APIRouter(prefix="/api/search", tags=["全局搜索"]) + + +def _has_keyword(*values, keyword: str) -> bool: + if not keyword: + return True + haystack = " ".join(str(value or "") for value in values).lower() + return keyword.lower() in haystack + + +def _tag_list(value) -> list[str]: + if not isinstance(value, list): + return [] + return [str(item) for item in value if str(item).strip()][:6] + + +def _item( + *, + id, + source: str, + source_label: str, + title: str, + route: str, + description: str = "", + subject: str = "", + grade: str = "", + tags=None, + action_route: str = "", + is_public: bool = False, + updated_at=None, +) -> dict: + return { + "id": id, + "source": source, + "source_label": source_label, + "title": title or "未命名", + "description": description or "", + "subject": subject or "", + "grade": grade or "", + "tags": _tag_list(tags), + "route": route, + "action_route": action_route, + "is_public": bool(is_public), + "updated_at": updated_at, + } + + +def _sort_key(item: dict): + return item["updated_at"] or "" + + +def _resource_query(keyword: str, current_user: User | None, db: Session, limit: int) -> list[dict]: + query = db.query(Resource).filter(Resource.status == "active") + if current_user: + query = query.filter((Resource.user_id == current_user.id) | (Resource.is_public == 1)) + else: + query = query.filter(Resource.is_public == 1) + if keyword: + query = query.filter(or_( + Resource.title.contains(keyword), + Resource.description.contains(keyword), + Resource.subject.contains(keyword), + Resource.grade.contains(keyword), + )) + return [ + _item( + id=res.id, + source="resource", + source_label="资源", + title=res.title, + description=res.description, + subject=res.subject, + grade=res.grade, + tags=res.tags, + route=f"/resources/{res.id}", + action_route=f"/resources/{res.id}", + is_public=bool(res.is_public), + updated_at=res.updated_at, + ) + for res in query.order_by(Resource.updated_at.desc()).limit(limit).all() + ] + + +def _community_query(keyword: str, current_user: User | None, db: Session, limit: int) -> list[dict]: + query = db.query(Post) + if keyword: + query = query.filter(or_(Post.title.contains(keyword), Post.content.contains(keyword))) + return [ + _item( + id=post.id, + source="community", + source_label="模板社区", + title=post.title, + description=post.content, + tags=post.tags, + route=f"/community/{post.id}", + action_route=f"/community/{post.id}", + is_public=True, + updated_at=post.updated_at, + ) + for post in query.order_by(Post.updated_at.desc()).limit(limit).all() + ] + + +def _my_work_queries(keyword: str, current_user: User | None, db: Session, limit: int) -> list[dict]: + if not current_user: + return [] + user_id = current_user.id + items = [] + + coursewares = db.query(Courseware).filter(Courseware.user_id == user_id, Courseware.status != "archived").all() + for work in coursewares: + if _has_keyword(work.title, work.description, work.subject, work.grade, " ".join(_tag_list(work.tags)), keyword=keyword): + items.append(_item( + id=work.id, + source="courseware", + source_label="我的课件", + title=work.title, + description=work.description, + subject=work.subject, + grade=work.grade, + tags=work.tags, + route=f"/courseware/{work.id}", + action_route=f"/courseware/{work.id}", + updated_at=work.updated_at, + )) + + animations = db.query(Animation).filter(Animation.user_id == user_id).all() + for work in animations: + if _has_keyword(work.title, work.description, work.anim_type, keyword=keyword): + items.append(_item( + id=work.id, + source="animation", + source_label="我的动画", + title=work.title, + description=work.description, + subject=str(work.anim_type or ""), + route=f"/animation?open={work.id}", + action_route=f"/animation?open={work.id}", + updated_at=work.updated_at, + )) + + exercises = db.query(Exercise).filter(Exercise.user_id == user_id).all() + for work in exercises: + if _has_keyword(work.title, work.subject, " ".join(_tag_list(work.knowledge_points)), keyword=keyword): + items.append(_item( + id=work.id, + source="exercise", + source_label="我的练习", + title=work.title, + subject=work.subject, + tags=work.knowledge_points, + route=f"/exercise?open={work.id}", + action_route=f"/exercise?open={work.id}", + updated_at=work.updated_at, + )) + + lesson_plans = db.query(LessonPlan).filter(LessonPlan.user_id == user_id).all() + for work in lesson_plans: + if _has_keyword(work.title, work.subject, work.grade, " ".join(_tag_list(work.objectives)), keyword=keyword): + items.append(_item( + id=work.id, + source="lesson_plan", + source_label="我的教案", + title=work.title, + subject=work.subject, + grade=work.grade, + tags=work.objectives, + route=f"/lesson-plan?open={work.id}", + action_route=f"/lesson-plan?open={work.id}", + updated_at=work.updated_at, + )) + + mind_maps = db.query(MindMap).filter(MindMap.user_id == user_id).all() + for work in mind_maps: + if _has_keyword(work.title, work.subject, keyword=keyword): + items.append(_item( + id=work.id, + source="mindmap", + source_label="我的导图", + title=work.title, + subject=work.subject, + route=f"/mindmap?open={work.id}", + action_route=f"/mindmap?open={work.id}", + updated_at=work.updated_at, + )) + + exams = db.query(Exam).filter(Exam.user_id == user_id).all() + for work in exams: + if _has_keyword(work.title, work.subject, work.grade, " ".join(_tag_list(work.knowledge_points)), keyword=keyword): + items.append(_item( + id=work.id, + source="exam", + source_label="我的试卷", + title=work.title, + subject=work.subject, + grade=work.grade, + tags=work.knowledge_points, + route=f"/exam?open={work.id}", + action_route=f"/exam?open={work.id}", + updated_at=work.updated_at, + )) + + items.sort(key=_sort_key, reverse=True) + return items[:limit] + + +def _material_query(keyword: str, current_user: User | None, db: Session, limit: int) -> list[dict]: + if not current_user: + return [] + query = db.query(Material).filter(Material.user_id == current_user.id, Material.status == "active") + if keyword: + query = query.filter(or_( + Material.title.contains(keyword), + Material.filename.contains(keyword), + Material.summary.contains(keyword), + Material.subject.contains(keyword), + Material.grade.contains(keyword), + )) + return [ + _item( + id=item.id, + source="material", + source_label="素材", + title=item.title, + description=item.summary, + subject=item.subject, + grade=item.grade, + tags=item.tags, + route="/materials", + action_route=f"/courseware/create", + updated_at=item.updated_at, + ) + for item in query.order_by(Material.updated_at.desc()).limit(limit).all() + ] + + +def _classroom_query(keyword: str, current_user: User | None, db: Session, limit: int) -> list[dict]: + if not current_user: + return [] + activities = db.query(ClassroomActivity).filter(ClassroomActivity.user_id == current_user.id).all() + items = [] + for activity in activities: + config = activity.config or {} + if _has_keyword(activity.title, activity.description, config.get("question"), activity.activity_type, keyword=keyword): + items.append(_item( + id=activity.id, + source="classroom", + source_label="课堂活动", + title=activity.title, + description=activity.description or config.get("question") or "", + subject=activity.activity_type or "", + route=f"/classroom?open={activity.id}", + action_route=f"/classroom?open={activity.id}", + updated_at=activity.updated_at, + )) + items.sort(key=_sort_key, reverse=True) + return items[:limit] + + +@router.get("", response_model=SearchResponse) +def global_search( + keyword: str = Query("", max_length=100), + limit: int = Query(6, ge=1, le=20), + current_user: User | None = Depends(get_optional_current_user), + db: Session = Depends(get_db), +): + keyword = keyword.strip() + resources = _resource_query(keyword, current_user, db, limit) + community = _community_query(keyword, current_user, db, limit) + works = _my_work_queries(keyword, current_user, db, limit) + materials = _material_query(keyword, current_user, db, limit) + classroom = _classroom_query(keyword, current_user, db, limit) + return { + "keyword": keyword, + "total": len(resources) + len(community) + len(works) + len(materials) + len(classroom), + "resources": resources, + "community": community, + "works": works, + "materials": materials, + "classroom": classroom, + } diff --git a/backend/schemas/__init__.py b/backend/schemas/__init__.py new file mode 100644 index 0000000..084b95c --- /dev/null +++ b/backend/schemas/__init__.py @@ -0,0 +1,9 @@ +from .user import * +from .courseware import * +from .animation import * +from .exercise import * +from .lesson_plan import * +from .exam import * +from .resource import * +from .community import * +from .ai import * diff --git a/backend/schemas/admin.py b/backend/schemas/admin.py new file mode 100644 index 0000000..634688e --- /dev/null +++ b/backend/schemas/admin.py @@ -0,0 +1,53 @@ +from datetime import datetime +from typing import Optional + +from pydantic import BaseModel, Field, field_validator + + +class AdminUserOut(BaseModel): + id: int + phone: str + name: str + avatar: str = "" + subject: str = "" + school: str = "" + grade: str = "" + role: str = "teacher" + is_active: bool = True + credits: int = 0 + created_at: datetime | None = None + + @field_validator("avatar", "subject", "school", "grade", "name", "role", mode="before") + @classmethod + def _none_to_empty(cls, v): + return "" if v is None else v + + model_config = {"from_attributes": True} + + +class AdminUserUpdate(BaseModel): + role: Optional[str] = None + is_active: Optional[bool] = None + name: Optional[str] = None + + +class AdminGrantCredits(BaseModel): + amount: int = Field(..., gt=0, le=10000, description="积分数量") + + +class AdminDashboardOut(BaseModel): + total_users: int = 0 + active_users: int = 0 + admin_users: int = 0 + total_resources: int = 0 + public_resources: int = 0 + total_coursewares: int = 0 + total_animations: int = 0 + total_exercises: int = 0 + total_lesson_plans: int = 0 + total_exams: int = 0 + total_views: int = 0 + total_downloads: int = 0 + total_favorites: int = 0 + total_credits_used: int = 0 + new_users_today: int = 0 diff --git a/backend/schemas/ai.py b/backend/schemas/ai.py new file mode 100644 index 0000000..fe37bff --- /dev/null +++ b/backend/schemas/ai.py @@ -0,0 +1,38 @@ +from pydantic import BaseModel, Field +from typing import Any + + +class AIRequest(BaseModel): + prompt: str = Field(..., min_length=1, description="AI请求内容") + context: dict[str, Any] = Field(default_factory=dict, description="上下文信息") + stream: bool = Field(default=False, description="是否流式返回") + + +class EssayGradeRequest(BaseModel): + essay_text: str = Field(..., min_length=10, description="作文文本") + grade_level: str = Field(default="初中", description="年级段") + essay_type: str = Field(default="记叙文", description="作文类型") + total_score: int = Field(default=50, description="满分") + + +class ExamGenerateRequest(BaseModel): + subject: str = Field(..., min_length=1) + grade: str = "" + knowledge_points: list[str] = [] + difficulty: str = Field(default="medium", pattern=r"^(easy|medium|hard)$") + question_types: list[str] = Field(default=["choice"]) + count: int = Field(default=10, ge=1, le=50) + total_score: int = Field(default=100) + + +class HtmlExportRequest(BaseModel): + title: str = Field(default="教学内容", max_length=200) + html: str = Field(..., min_length=1) + + +class ExamExportRequest(BaseModel): + title: str = Field(default="试卷", max_length=200) + subject: str = "" + grade: str = "" + questions: list[dict[str, Any]] = [] + answers: list[Any] = [] diff --git a/backend/schemas/animation.py b/backend/schemas/animation.py new file mode 100644 index 0000000..d39818f --- /dev/null +++ b/backend/schemas/animation.py @@ -0,0 +1,40 @@ +from pydantic import BaseModel, Field +from datetime import datetime +from typing import Optional, Any + + +class AnimationGenerate(BaseModel): + prompt: str = Field(..., min_length=1, description="教学动画描述") + anim_type: str = Field(default="general", pattern=r"^(math|physics|chemistry|biology|geography|general)$") + subject: str = Field(default="", max_length=50) + + +class AnimationCreate(BaseModel): + title: str = Field(..., min_length=1, max_length=200) + anim_type: str = "general" + description: str = "" + config: dict[str, Any] = {} + + +class AnimationUpdate(BaseModel): + title: Optional[str] = None + anim_type: Optional[str] = None + description: Optional[str] = None + config: Optional[dict[str, Any]] = None + thumbnail: Optional[str] = None + status: Optional[str] = None + + +class AnimationOut(BaseModel): + id: int + user_id: int + title: str + anim_type: str = "general" + description: str = "" + config: dict = {} + thumbnail: str = "" + status: str = "draft" + created_at: datetime | None = None + updated_at: datetime | None = None + + model_config = {"from_attributes": True} diff --git a/backend/schemas/chat.py b/backend/schemas/chat.py new file mode 100644 index 0000000..26647fc --- /dev/null +++ b/backend/schemas/chat.py @@ -0,0 +1,41 @@ +from datetime import datetime +from pydantic import BaseModel, Field + + +class ConversationCreate(BaseModel): + title: str | None = Field(default=None, max_length=200) + subject: str = "" + grade: str = "" + + +class ConversationRename(BaseModel): + title: str = Field(..., min_length=1, max_length=200) + pinned: bool | None = None + + +class ChatSend(BaseModel): + content: str = Field(..., min_length=1, max_length=4000) + subject: str = "" + grade: str = "" + + +class MessageOut(BaseModel): + id: int + role: str + content: str + created_at: datetime | None = None + + model_config = {"from_attributes": True} + + +class ConversationOut(BaseModel): + id: int + title: str + subject: str = "" + grade: str = "" + pinned: bool = False + created_at: datetime | None = None + updated_at: datetime | None = None + messages: list[MessageOut] = Field(default_factory=list) + + model_config = {"from_attributes": True} diff --git a/backend/schemas/classroom.py b/backend/schemas/classroom.py new file mode 100644 index 0000000..d76ae71 --- /dev/null +++ b/backend/schemas/classroom.py @@ -0,0 +1,71 @@ +from datetime import datetime +from typing import Any, Optional + +from pydantic import BaseModel, Field + + +class ClassroomActivityCreate(BaseModel): + title: str = Field(..., min_length=1, max_length=200) + activity_type: str = Field(default="poll", max_length=50) + description: str = "" + config: dict[str, Any] = {} + status: str = "draft" + + +class ClassroomActivityUpdate(BaseModel): + title: Optional[str] = None + activity_type: Optional[str] = None + description: Optional[str] = None + config: Optional[dict[str, Any]] = None + responses: Optional[list[dict[str, Any]]] = None + status: Optional[str] = None + + +class ClassroomResponseCreate(BaseModel): + student_name: str = Field(default="", max_length=50) + answer: Any + meta: dict[str, Any] = {} + + +class ClassroomActivityOut(BaseModel): + id: int + user_id: int + title: str + activity_type: str = "poll" + description: str = "" + config: dict = {} + responses: list = [] + status: str = "draft" + created_at: datetime | None = None + updated_at: datetime | None = None + + model_config = {"from_attributes": True} + + +class ClassroomOptionStat(BaseModel): + option: str + count: int = 0 + percent: int = 0 + + +class ClassroomKeywordStat(BaseModel): + keyword: str + count: int = 0 + + +class ClassroomAnalysisOut(BaseModel): + activity_id: int + title: str + activity_type: str = "poll" + question: str = "" + response_count: int = 0 + participant_count: int = 0 + option_stats: list[ClassroomOptionStat] = Field(default_factory=list) + keywords: list[ClassroomKeywordStat] = Field(default_factory=list) + common_answers: list[str] = Field(default_factory=list) + mastery_level: str = "暂无数据" + accuracy: int | None = None + diagnosis: list[str] = Field(default_factory=list) + teaching_suggestions: list[str] = Field(default_factory=list) + followup_prompt: str = "" + generated_at: datetime | None = None diff --git a/backend/schemas/community.py b/backend/schemas/community.py new file mode 100644 index 0000000..6d15cd3 --- /dev/null +++ b/backend/schemas/community.py @@ -0,0 +1,67 @@ +from pydantic import BaseModel, Field +from datetime import datetime +from typing import Any, Optional + + +class CommunityAuthorOut(BaseModel): + id: int + name: str = "" + avatar: str = "" + subject: str = "" + school: str = "" + + model_config = {"from_attributes": True} + + +class PostCreate(BaseModel): + title: str = Field(..., min_length=1, max_length=200) + content: str = Field(..., min_length=1) + post_type: str = "discussion" + tags: list[str] = Field(default_factory=list) + attachments: list[dict[str, Any]] = Field(default_factory=list) + + +class PostUpdate(BaseModel): + title: Optional[str] = Field(None, min_length=1, max_length=200) + content: Optional[str] = Field(None, min_length=1) + post_type: Optional[str] = None + tags: Optional[list[str]] = None + attachments: Optional[list[dict[str, Any]]] = None + + +class PostOut(BaseModel): + id: int + user_id: int + title: str + content: str = "" + post_type: str = "discussion" + tags: list = Field(default_factory=list) + attachments: list = Field(default_factory=list) + views: int = 0 + likes: int = 0 + comments_count: int = 0 + is_pinned: int = 0 + is_favorited: bool = False + author: CommunityAuthorOut | None = None + created_at: datetime | None = None + updated_at: datetime | None = None + + model_config = {"from_attributes": True} + + +class CommentCreate(BaseModel): + content: str = Field(..., min_length=1) + parent_id: Optional[int] = None + + +class CommentOut(BaseModel): + id: int + post_id: int + user_id: int + content: str + parent_id: Optional[int] = None + likes: int = 0 + author: CommunityAuthorOut | None = None + created_at: datetime | None = None + + model_config = {"from_attributes": True} diff --git a/backend/schemas/courseware.py b/backend/schemas/courseware.py new file mode 100644 index 0000000..6e9d785 --- /dev/null +++ b/backend/schemas/courseware.py @@ -0,0 +1,69 @@ +from pydantic import BaseModel, Field +from datetime import datetime +from typing import Optional, Any + + +class CoursewareCreate(BaseModel): + title: str = Field(..., min_length=1, max_length=200) + subject: str = Field(default="", max_length=50) + grade: str = Field(default="", max_length=50) + description: str = "" + content: list[Any] = [] + tags: list[str] = [] + status: str = "draft" + + +class CoursewareAIGenerate(BaseModel): + prompt: str = Field(..., min_length=1, description="教学想法/描述") + subject: str = Field(default="", max_length=50) + grade: str = Field(default="", max_length=50) + page_count: int = Field(default=8, ge=3, le=30) + aspect_ratio: str = Field(default="16:9", description="画面比例: 16:9, 4:3, 1:1") + + +class CoursewareUpdate(BaseModel): + title: Optional[str] = None + subject: Optional[str] = None + grade: Optional[str] = None + description: Optional[str] = None + content: Optional[list[Any]] = None + cover_image: Optional[str] = None + status: Optional[str] = None + tags: Optional[list[str]] = None + + +class CoursewareOut(BaseModel): + id: int + user_id: int + title: str + subject: str = "" + grade: str = "" + description: str = "" + content: list = [] + cover_image: str = "" + status: str = "draft" + version: int = 1 + tags: list = [] + created_at: datetime | None = None + updated_at: datetime | None = None + + model_config = {"from_attributes": True} + + +class CoursewareShareOut(BaseModel): + id: int + courseware_id: int + token: str + title: str + subject: str = "" + grade: str = "" + description: str = "" + content: list = [] + tags: list = [] + views: int = 0 + status: str = "active" + share_url: str = "" + created_at: datetime | None = None + updated_at: datetime | None = None + + model_config = {"from_attributes": True} diff --git a/backend/schemas/essay.py b/backend/schemas/essay.py new file mode 100644 index 0000000..f87a98f --- /dev/null +++ b/backend/schemas/essay.py @@ -0,0 +1,27 @@ +from datetime import datetime +from typing import Any +from pydantic import BaseModel, Field + + +class EssayGradeCreate(BaseModel): + title: str = Field(default="作文批改报告", min_length=1, max_length=200) + essay_text: str = Field(..., min_length=1) + grade_level: str = "初中" + essay_type: str = "记叙文" + total_score: int = 50 + result: dict[str, Any] = Field(default_factory=dict) + + +class EssayGradeOut(BaseModel): + id: int + user_id: int + title: str + essay_text: str = "" + grade_level: str = "初中" + essay_type: str = "记叙文" + total_score: int = 50 + result: dict = Field(default_factory=dict) + created_at: datetime | None = None + updated_at: datetime | None = None + + model_config = {"from_attributes": True} diff --git a/backend/schemas/exam.py b/backend/schemas/exam.py new file mode 100644 index 0000000..9f2a290 --- /dev/null +++ b/backend/schemas/exam.py @@ -0,0 +1,46 @@ +from datetime import datetime +from typing import Any, Optional + +from pydantic import BaseModel, Field + + +class ExamCreate(BaseModel): + title: str = Field(..., min_length=1, max_length=200) + subject: str = "" + grade: str = "" + questions: list[dict[str, Any]] = [] + answers: list[Any] = [] + duration: int = 90 + total_score: int = 100 + difficulty: str = "medium" + knowledge_points: list[str] = [] + + +class ExamUpdate(BaseModel): + title: Optional[str] = None + subject: Optional[str] = None + grade: Optional[str] = None + questions: Optional[list[dict[str, Any]]] = None + answers: Optional[list[Any]] = None + duration: Optional[int] = None + total_score: Optional[int] = None + difficulty: Optional[str] = None + knowledge_points: Optional[list[str]] = None + + +class ExamOut(BaseModel): + id: int + user_id: int + title: str + subject: str = "" + grade: str = "" + questions: list = [] + answers: list = [] + duration: int = 90 + total_score: int = 100 + difficulty: str = "medium" + knowledge_points: list = [] + created_at: datetime | None = None + updated_at: datetime | None = None + + model_config = {"from_attributes": True} diff --git a/backend/schemas/exercise.py b/backend/schemas/exercise.py new file mode 100644 index 0000000..8b051cc --- /dev/null +++ b/backend/schemas/exercise.py @@ -0,0 +1,65 @@ +from pydantic import BaseModel, Field +from datetime import datetime +from typing import Optional, Any + + +class ExerciseAIGenerate(BaseModel): + prompt: str = Field(..., min_length=1, description="练习生成描述") + exercise_type: str = Field(default="choice") + subject: str = "" + knowledge_points: list[str] = [] + count: int = Field(default=5, ge=1, le=50) + + +class ExerciseCreate(BaseModel): + title: str = Field(..., min_length=1, max_length=200) + exercise_type: str = "choice" + subject: str = "" + knowledge_points: list[str] = [] + questions: list[dict[str, Any]] = [] + settings: dict[str, Any] = {} + + +class ExerciseUpdate(BaseModel): + title: Optional[str] = None + exercise_type: Optional[str] = None + subject: Optional[str] = None + knowledge_points: Optional[list[str]] = None + questions: Optional[list[dict[str, Any]]] = None + settings: Optional[dict[str, Any]] = None + + +class ExerciseAttemptCreate(BaseModel): + student_name: str = "" + answers: list[Any] = [] + score: int = 0 + total: int = 0 + duration: int = 0 + + +class ExerciseAttemptOut(BaseModel): + id: int + exercise_id: int + student_name: str = "" + answers: list[Any] = [] + score: int = 0 + total: int = 0 + duration: int = 0 + created_at: datetime | None = None + + model_config = {"from_attributes": True} + + +class ExerciseOut(BaseModel): + id: int + user_id: int + title: str + exercise_type: str = "choice" + subject: str = "" + knowledge_points: list = [] + questions: list = [] + settings: dict = {} + created_at: datetime | None = None + updated_at: datetime | None = None + + model_config = {"from_attributes": True} diff --git a/backend/schemas/lesson_plan.py b/backend/schemas/lesson_plan.py new file mode 100644 index 0000000..4643778 --- /dev/null +++ b/backend/schemas/lesson_plan.py @@ -0,0 +1,57 @@ +from pydantic import BaseModel, Field +from datetime import datetime +from typing import Optional, Any + + +class LessonPlanGenerate(BaseModel): + title: str = Field(..., min_length=1) + subject: str = "" + grade: str = "" + objectives: list[str] = [] + duration: int = Field(default=45, ge=20, le=120) + extra_requirements: str = "" + + +class LessonPlanCreate(BaseModel): + title: str = Field(..., min_length=1, max_length=200) + subject: str = "" + grade: str = "" + objectives: list[str] = [] + key_points: list[str] = [] + difficulties: list[str] = [] + content: dict[str, Any] = {} + duration: int = 45 + materials: list[str] = [] + homework: list[str] = [] + + +class LessonPlanUpdate(BaseModel): + title: Optional[str] = None + subject: Optional[str] = None + grade: Optional[str] = None + objectives: Optional[list[str]] = None + key_points: Optional[list[str]] = None + difficulties: Optional[list[str]] = None + content: Optional[dict[str, Any]] = None + duration: Optional[int] = None + materials: Optional[list[str]] = None + homework: Optional[list[str]] = None + + +class LessonPlanOut(BaseModel): + id: int + user_id: int + title: str + subject: str = "" + grade: str = "" + objectives: list = [] + key_points: list = [] + difficulties: list = [] + content: dict = {} + duration: int = 45 + materials: list = [] + homework: list = [] + created_at: datetime | None = None + updated_at: datetime | None = None + + model_config = {"from_attributes": True} diff --git a/backend/schemas/material.py b/backend/schemas/material.py new file mode 100644 index 0000000..b6cf860 --- /dev/null +++ b/backend/schemas/material.py @@ -0,0 +1,44 @@ +from datetime import datetime +from typing import Optional +from pydantic import BaseModel, Field + + +class MaterialCreate(BaseModel): + filename: str = Field(..., min_length=1, max_length=255) + title: str = Field(..., min_length=1, max_length=200) + material_type: str = "text" + subject: str = "" + grade: str = "" + tags: list[str] = Field(default_factory=list) + summary: str = "" + char_count: int = 0 + size: int = 0 + source: str = "upload" + + +class MaterialUpdate(BaseModel): + title: Optional[str] = None + subject: Optional[str] = None + grade: Optional[str] = None + tags: Optional[list[str]] = None + summary: Optional[str] = None + status: Optional[str] = None + + +class MaterialOut(BaseModel): + id: int + user_id: int + filename: str + title: str + material_type: str = "text" + subject: str = "" + grade: str = "" + tags: list = Field(default_factory=list) + summary: str = "" + char_count: int = 0 + size: int = 0 + source: str = "upload" + created_at: datetime | None = None + updated_at: datetime | None = None + + model_config = {"from_attributes": True} diff --git a/backend/schemas/mindmap.py b/backend/schemas/mindmap.py new file mode 100644 index 0000000..b987d90 --- /dev/null +++ b/backend/schemas/mindmap.py @@ -0,0 +1,32 @@ +from datetime import datetime +from typing import Any +from pydantic import BaseModel, Field + + +class MindMapGenerate(BaseModel): + topic: str = Field(..., min_length=1, max_length=200) + subject: str = "综合" + grade: str = "" + + +class MindMapCreate(BaseModel): + title: str = Field(..., min_length=1, max_length=200) + subject: str = "综合" + nodes: list[Any] = Field(default_factory=list) + + +class MindMapUpdate(BaseModel): + title: str | None = None + nodes: list[Any] | None = None + + +class MindMapOut(BaseModel): + id: int + user_id: int + title: str + subject: str = "综合" + nodes: list[Any] = Field(default_factory=list) + created_at: datetime | None = None + updated_at: datetime | None = None + + model_config = {"from_attributes": True} diff --git a/backend/schemas/notification.py b/backend/schemas/notification.py new file mode 100644 index 0000000..a78b7cf --- /dev/null +++ b/backend/schemas/notification.py @@ -0,0 +1,16 @@ +from datetime import datetime +from pydantic import BaseModel + + +class NotificationOut(BaseModel): + id: int + user_id: int + actor_id: int | None = None + ntype: str = "comment" + title: str + content: str = "" + link: str = "" + is_read: bool = False + created_at: datetime | None = None + + model_config = {"from_attributes": True} diff --git a/backend/schemas/resource.py b/backend/schemas/resource.py new file mode 100644 index 0000000..ef19a93 --- /dev/null +++ b/backend/schemas/resource.py @@ -0,0 +1,63 @@ +from pydantic import BaseModel, Field +from datetime import datetime +from typing import Optional, Any + + +class ResourceAuthorOut(BaseModel): + id: int + name: str = "" + avatar: str = "" + subject: str = "" + school: str = "" + + model_config = {"from_attributes": True} + + +class ResourceCreate(BaseModel): + resource_type: str = "other" + title: str = Field(..., min_length=1, max_length=200) + description: str = "" + content_ref: str = "" + file_url: str = "" + tags: list[str] = Field(default_factory=list) + subject: str = "" + grade: str = "" + is_public: int = 1 + + +class ResourceUpdate(BaseModel): + resource_type: Optional[str] = None + title: Optional[str] = None + description: Optional[str] = None + content_ref: Optional[str] = None + file_url: Optional[str] = None + cover_image: Optional[str] = None + tags: Optional[list[str]] = None + subject: Optional[str] = None + grade: Optional[str] = None + is_public: Optional[int] = None + status: Optional[str] = None + + +class ResourceOut(BaseModel): + id: int + user_id: int + resource_type: str + title: str + description: str = "" + content_ref: str = "" + file_url: str = "" + cover_image: str = "" + tags: list = Field(default_factory=list) + subject: str = "" + grade: str = "" + downloads: int = 0 + likes: int = 0 + views: int = 0 + is_public: int = 1 + is_favorited: bool = False + author: ResourceAuthorOut | None = None + created_at: datetime | None = None + updated_at: datetime | None = None + + model_config = {"from_attributes": True} diff --git a/backend/schemas/search.py b/backend/schemas/search.py new file mode 100644 index 0000000..b74af70 --- /dev/null +++ b/backend/schemas/search.py @@ -0,0 +1,28 @@ +from datetime import datetime + +from pydantic import BaseModel, Field + + +class SearchResultItem(BaseModel): + id: int | str + source: str + source_label: str + title: str + description: str = "" + subject: str = "" + grade: str = "" + tags: list[str] = Field(default_factory=list) + route: str + action_route: str = "" + is_public: bool = False + updated_at: datetime | None = None + + +class SearchResponse(BaseModel): + keyword: str = "" + total: int = 0 + resources: list[SearchResultItem] = Field(default_factory=list) + community: list[SearchResultItem] = Field(default_factory=list) + works: list[SearchResultItem] = Field(default_factory=list) + materials: list[SearchResultItem] = Field(default_factory=list) + classroom: list[SearchResultItem] = Field(default_factory=list) diff --git a/backend/schemas/user.py b/backend/schemas/user.py new file mode 100644 index 0000000..f5c5568 --- /dev/null +++ b/backend/schemas/user.py @@ -0,0 +1,130 @@ +from pydantic import BaseModel, Field, field_validator +from datetime import datetime +from typing import Optional + + +class UserLogin(BaseModel): + phone: str = Field(..., pattern=r"^1[3-9]\d{9}$", description="手机号") + password: str = Field(..., min_length=6, max_length=50, description="密码") + + +class UserRegister(BaseModel): + phone: str = Field(..., pattern=r"^1[3-9]\d{9}$") + password: str = Field(..., min_length=6, max_length=50) + name: str = Field(..., min_length=2, max_length=50) + subject: str = Field(default="", max_length=50) + school: str = Field(default="", max_length=100) + grade: str = Field(default="", max_length=50) + + +class UserUpdate(BaseModel): + name: Optional[str] = None + avatar: Optional[str] = None + subject: Optional[str] = None + school: Optional[str] = None + grade: Optional[str] = None + + +class PasswordChange(BaseModel): + old_password: str = Field(..., min_length=6, max_length=50, description="当前密码") + new_password: str = Field(..., min_length=6, max_length=50, description="新密码") + + +class UserOut(BaseModel): + id: int + phone: str + name: str + avatar: str = "" + subject: str = "" + school: str = "" + grade: str = "" + role: str = "teacher" + credits: int = 0 + total_credits_granted: int = 0 + total_credits_used: int = 0 + created_at: datetime | None = None + + @field_validator("avatar", "subject", "school", "grade", "name", "role", mode="before") + @classmethod + def _none_to_empty(cls, v): + return "" if v is None else v + + model_config = {"from_attributes": True} + + +class Token(BaseModel): + access_token: str + refresh_token: str + token_type: str = "bearer" + + +class TokenPayload(BaseModel): + sub: int + exp: datetime + + +class ProfileRecentItem(BaseModel): + id: int + item_type: str + title: str + subtitle: str = "" + status: str = "" + updated_at: datetime | None = None + + +class CreditTransactionOut(BaseModel): + id: int + amount: int + action: str = "" + description: str = "" + balance_after: int = 0 + created_at: datetime | None = None + + model_config = {"from_attributes": True} + + +class CreditBenefitOut(BaseModel): + credits: int = 0 + daily_amount: int = 0 + daily_claimed: bool = False + next_claim_text: str = "" + recent_checkins: list[CreditTransactionOut] = Field(default_factory=list) + + +class ProfileStatsOut(BaseModel): + credits: int = 0 + total_credits_granted: int = 0 + total_credits_used: int = 0 + total_works: int = 0 + draft_works: int = 0 + published_works: int = 0 + published_resources: int = 0 + public_resources: int = 0 + private_resources: int = 0 + favorite_resources: int = 0 + total_views: int = 0 + total_downloads: int = 0 + total_favorites: int = 0 + classroom_activities: int = 0 + active_classroom_activities: int = 0 + classroom_responses: int = 0 + community_posts: int = 0 + community_favorites: int = 0 + community_comments: int = 0 + works_by_type: dict[str, int] = Field(default_factory=dict) + resources_by_type: dict[str, int] = Field(default_factory=dict) + recent_works: list[ProfileRecentItem] = Field(default_factory=list) + recent_resources: list[ProfileRecentItem] = Field(default_factory=list) + recent_interactions: list[ProfileRecentItem] = Field(default_factory=list) + recent_credit_transactions: list[CreditTransactionOut] = Field(default_factory=list) + + + +class SendCodeRequest(BaseModel): + phone: str = Field(..., pattern=r"^1[3-9]\d{9}$", description="手机号") + + +class ResetPasswordRequest(BaseModel): + phone: str = Field(..., pattern=r"^1[3-9]\d{9}$") + code: str = Field(..., min_length=6, max_length=6, description="6位验证码") + new_password: str = Field(..., min_length=6, max_length=50, description="新密码") diff --git a/backend/services/__init__.py b/backend/services/__init__.py new file mode 100644 index 0000000..70e03d1 --- /dev/null +++ b/backend/services/__init__.py @@ -0,0 +1,7 @@ +from .auth import get_password_hash, verify_password, create_access_token, create_refresh_token, get_current_user +from .ai_service import AIService + +__all__ = [ + "get_password_hash", "verify_password", "create_access_token", + "create_refresh_token", "get_current_user", "AIService", +] diff --git a/backend/services/ai_service.py b/backend/services/ai_service.py new file mode 100644 index 0000000..c5c7c07 --- /dev/null +++ b/backend/services/ai_service.py @@ -0,0 +1,3190 @@ +import json +import re +import html as html_lib +import httpx +import logging +import time +import base64 +from config import get_settings + +settings = get_settings() +logger = logging.getLogger(__name__) + + +class AIService: + # Shared circuit-breaker timestamp: all instances share one breaker so a + # single AI outage trips the breaker globally across every router. + _ai_unavailable_until: float = 0.0 + + def __init__(self): + self.api_key = settings.AI_API_KEY + self.api_base = settings.AI_API_BASE.rstrip("/") + self.model = settings.AI_MODEL + self.client = httpx.AsyncClient( + base_url=self.api_base, + headers={"Authorization": f"Bearer {self.api_key}"}, + timeout=httpx.Timeout(connect=10.0, read=120.0, write=15.0, pool=10.0), + ) + + async def _chat(self, messages: list[dict], temperature: float = 0.7, max_tokens: int = 8192) -> str: + if time.monotonic() < AIService._ai_unavailable_until: + logger.warning("AI provider skipped (circuit breaker active for %.0fs more)", AIService._ai_unavailable_until - time.monotonic()) + raise RuntimeError("AI_PROVIDER_UNAVAILABLE") + try: + response = await self.client.post( + "/chat/completions", + json={ + "model": self.model, + "messages": messages, + "temperature": temperature, + "max_tokens": max_tokens, + # glm-5.x is a reasoning model: without disabling + # thinking it burns the entire token budget on + # chain-of-thought and returns EMPTY content + # (finish_reason=length), forcing every feature to + # fall back to static templates. Disable for real output. + "enable_thinking": False, + "thinking": {"type": "disabled"}, + }, + ) + response.raise_for_status() + except httpx.ConnectError as exc: + # Connection-level failure: server likely down. Short breaker so we retry sooner. + logger.error("AI provider connect failed: %s: %s", type(exc).__name__, exc) + AIService._ai_unavailable_until = time.monotonic() + 20 + raise RuntimeError("AI_PROVIDER_CONNECT_ERROR") from exc + except httpx.TimeoutException as exc: + # Timeout (connect/read): server slow or unreachable. Short breaker. + logger.error("AI provider timeout: %s: %s", type(exc).__name__, exc) + AIService._ai_unavailable_until = time.monotonic() + 20 + raise RuntimeError("AI_PROVIDER_TIMEOUT") from exc + except httpx.HTTPStatusError as exc: + # HTTP error (4xx/5xx): server is up but rejected the request. + logger.error("AI provider HTTP %s: %s", exc.response.status_code, str(exc.response.text)[:200]) + breaker = 30 if exc.response.status_code >= 500 else 120 + AIService._ai_unavailable_until = time.monotonic() + breaker + raise RuntimeError("AI_PROVIDER_HTTP_ERROR") from exc + except Exception as exc: + logger.error("AI provider request failed: %s: %s", type(exc).__name__, exc) + AIService._ai_unavailable_until = time.monotonic() + 30 + raise RuntimeError("AI_PROVIDER_UNAVAILABLE") from exc + data = response.json() + content = data["choices"][0]["message"]["content"] or "" + # Reasoning models may return empty content if thinking consumed the token budget + if not content.strip(): + logger.warning("AI provider returned empty content (finish=%s, model=%s) - reasoning budget likely exhausted", data["choices"][0].get("finish_reason","?"), self.model) + raise RuntimeError("AI_PROVIDER_EMPTY_RESPONSE") + finish_reason = data["choices"][0].get("finish_reason", "") + if finish_reason == "length": + content = content + '..."}' + return content + + async def _chat_json(self, messages: list[dict], temperature: float = 0.3, max_tokens: int = 8192) -> dict: + content = await self._chat(messages, temperature, max_tokens) + return self._extract_json(content) + + + async def ocr_image(self, image_bytes: bytes, filename: str = "") -> str: + """Extract text from an image using the AI model's vision capability.""" + if not self.api_key or self.api_key == "change-me-in-production": + raise RuntimeError("AI_PROVIDER_UNAVAILABLE") + if time.monotonic() < AIService._ai_unavailable_until: + raise RuntimeError("AI_PROVIDER_UNAVAILABLE") + + b64 = base64.b64encode(image_bytes).decode("ascii") + ext = "png" + lower_name = (filename or "").lower() + if lower_name.endswith((".jpg", ".jpeg")): + ext = "jpeg" + elif lower_name.endswith(".webp"): + ext = "webp" + elif lower_name.endswith(".gif"): + ext = "gif" + + messages = [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": ( + "请仔细识别这张图片中的所有文字内容,包括题目、公式、表格和标注。" + "完整提取文字,保持原有结构和层次。" + "如果是题目,请保留题号和选项格式。" + "数学公式用可读文本表示(例如 a² + b² = c²)。" + "只输出识别到的文字内容,不要添加解释或说明。" + "如果图片中没有文字或无法识别,请回复:[无法识别文字内容]" + ), + }, + { + "type": "image_url", + "image_url": {"url": f"data:image/{ext};base64,{b64}"}, + }, + ], + } + ] + + try: + response = await self.client.post( + "/chat/completions", + json={ + "model": self.model, + "messages": messages, + "temperature": 0.1, + "max_tokens": 4096, + "enable_thinking": False, + "thinking": {"type": "disabled"}, + }, + timeout=httpx.Timeout(connect=10.0, read=90.0, write=15.0, pool=10.0), + ) + response.raise_for_status() + except Exception as exc: + logger.error("Vision OCR request failed: %s", exc) + AIService._ai_unavailable_until = time.monotonic() + 60 + raise RuntimeError("AI_PROVIDER_UNAVAILABLE") from exc + + data = response.json() + return data["choices"][0]["message"]["content"].strip() + + def _extract_json(self, content: str) -> dict: + content = content.strip() + + # Strip markdown code fences + import re as _re + content = _re.sub(r'^```\w*\n?', '', content) + content = _re.sub(r'\n?```$', '', content) + content = content.strip() + + first_brace = content.find('{') + if first_brace == -1: + return {"raw_content": content} + + last_brace = content.rfind('}') + if last_brace <= first_brace: + return {"raw_content": content} + + # Try the full span first + candidate = content[first_brace:last_brace + 1] + try: + result = json.loads(candidate) + if isinstance(result, dict): + return result + except json.JSONDecodeError: + pass + + # Try auto-fixing truncated JSON by closing open structures + fixed = self._try_fix_truncated_json(candidate) + if fixed: + return fixed + + # Try replacing HTML double quotes with single quotes inside the html field + fixed2 = _re.sub(r'(style|onclick|onchange)=\\?"([^"]*?)\\?"', r"\1='\2'", candidate) + if fixed2 != candidate: + try: + result = json.loads(fixed2) + if isinstance(result, dict): + return result + except json.JSONDecodeError: + pass + + # Find all '}' positions from end to start + pos = last_brace + while pos > first_brace: + if content[pos] == '}': + try: + result = json.loads(content[first_brace:pos + 1]) + if isinstance(result, dict): + return result + except json.JSONDecodeError: + pass + pos -= 1 + + # Last resort: try to extract html field via regex + html_match = _re.search(r'"html"\s*:\s*"((?:[^"\\]|\\.)*)"', content, _re.DOTALL) + if html_match: + try: + html_val = json.loads('"' + html_match.group(1) + '"') + return {"title": "动画", "description": "", "html": html_val} + except: + pass + + # All attempts failed - save debug info + try: + with open("debug_json_fail.txt", "w", encoding="utf-8") as f: + f.write(f"Content length: {len(content)}\n") + f.write(f"First 500 chars:\n{content[:500]}\n\n") + f.write(f"Last 500 chars:\n{content[-500:]}\n\n") + f.write(f"Full content:\n{content}\n") + except: + pass + + return {"raw_content": content} + + def _try_fix_truncated_json(self, candidate: str) -> dict | None: + """Try to fix truncated JSON by closing open strings/brackets.""" + import re as _re + # Count brackets + opens = candidate.count('{') - candidate.count('}') + opens_bracket = candidate.count('[') - candidate.count(']') + + # Check if we're inside a string + in_string = False + escape = False + for ch in candidate: + if escape: + escape = False + continue + if ch == '\\': + escape = True + continue + if ch == '"': + in_string = not in_string + + fixed = candidate + if in_string: + fixed += '' + # Close open string if inside one + if in_string: + fixed += '"' + # Close open brackets + for _ in range(max(0, opens_bracket)): + fixed += ']' + for _ in range(max(0, opens)): + fixed += '}' + + try: + result = json.loads(fixed) + if isinstance(result, dict): + # If html field exists, this is usable + if 'html' in result: + return result + except json.JSONDecodeError: + pass + + # Try simpler fix: just close the outermost object + if in_string: + # Find last complete key-value pair before truncation + # Try: add closing quote, skip to end of object + for suffix in ['"}', '"}]}', '"}]}', '"]}', '"}' ]: + try: + result = json.loads(candidate + suffix) + if isinstance(result, dict): + return result + except json.JSONDecodeError: + continue + + return None + + async def chat_teaching(self, history: list[dict], user_message: str, subject: str = "", grade: str = "") -> str: + """General-purpose conversational teaching assistant (multi-turn).""" + context_bits = [] + if subject: + context_bits.append(f"任教学科:{subject}") + if grade: + context_bits.append(f"教学学段/年级:{grade}") + context_line = (";".join(context_bits) + "。") if context_bits else "" + + system_prompt = ( + "你是“智教助手”,一位经验丰富、耐心细致的 K12 全学科教学顾问与备课搭档。" + "你的任务是帮助一线教师解决真实的教学问题:备课思路、知识点讲解、活动设计、" + "课堂提问、学情诊断、家校沟通、教法选择等。\n" + "回答要求:\n" + "1. 用简体中文回答,语气专业、亲切、可落地,避免空话套话。\n" + "2. 结构清晰,善用标题、要点、表格和示例,必要时分步骤说明。\n" + "3. 紧扣中国课程标准和一线课堂实际,给出可直接使用的内容(如完整提问、活动脚本、讲解话术)。\n" + "4. 遇到模糊请求时主动追问关键信息(年级、学科、目标、时长)再给出方案。\n" + "5. 对学生的易错点、认知难点要有针对性提醒;涉及数值/事实要准确。\n" + f"{context_line}" + ) + + messages: list[dict] = [{"role": "system", "content": system_prompt}] + for turn in (history or []): + role = turn.get("role") + content = (turn.get("content") or "").strip() + if role in {"user", "assistant"} and content: + messages.append({"role": role, "content": content}) + messages.append({"role": "user", "content": user_message}) + try: + return await self._chat(messages, temperature=0.6, max_tokens=8192) + except RuntimeError: + return self._fallback_chat(history or [], user_message, subject, grade) + + async def generate_mindmap(self, topic: str, subject: str = "综合", grade: str = "") -> dict: + """生成知识思维导图,返回层级节点结构。""" + topic = self._repair_text(topic) + grade_hint = f",适用学段:{grade}" if grade else "" + system_prompt = f"""你是一位资深学科教学专家,擅长梳理知识结构。根据教师给出的主题,生成一份层次清晰、内容专业的知识思维导图。 + +要求: +- 围绕主题梳理 4 到 6 个一级分支(核心知识维度) +- 每个一级分支下展开 2 到 4 个二级子节点 +- 节点标题用精炼短语(4 到 14 字),不要长句 +- 内容要符合学科规范与教学逻辑,覆盖该主题的核心知识脉络(学科:{subject}{grade_hint}) +- 如果主题偏小众,也尽量给出合理、可教学的结构 + +直接输出JSON(不要markdown代码块,不要其他文字): +{{"title":"主题名","nodes":[{{"title":"一级分支标题","color":"#00a870","children":[{{"title":"二级子节点标题","children":[{{"title":"可展开的细节要点"}}]}}]}}]}}""" + try: + result = await self._chat_json([ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": topic}, + ], temperature=0.5, max_tokens=8192) + return self._normalize_mindmap_result(result, topic, subject) + except RuntimeError: + return self._fallback_mindmap(topic, subject) + + def _normalize_mindmap_result(self, result: dict, topic: str, subject: str) -> dict: + topic = self._repair_text(topic) + nodes = result.get("nodes") + if not isinstance(nodes, list) or not nodes: + return self._fallback_mindmap(topic, subject) + palette = ["#00a870", "#2563eb", "#d97706", "#db2777", "#7c3aed", "#0891b2", "#4f46e5", "#ea580c"] + clean: list = [] + for i, branch in enumerate(nodes): + if not isinstance(branch, dict) or not str(branch.get("title", "")).strip(): + continue + color = branch.get("color") or palette[i % len(palette)] + children = self._collect_mindmap_children(branch.get("children")) + if not children: + continue + clean.append({"title": str(branch["title"]).strip()[:30], "color": color, "children": children}) + if len(clean) < 2: + return self._fallback_mindmap(topic, subject) + return {"title": str(result.get("title") or topic)[:80], "nodes": clean[:8]} + + def _collect_mindmap_children(self, raw) -> list: + if not isinstance(raw, list): + return [] + out: list = [] + for item in raw: + if isinstance(item, dict) and str(item.get("title", "")).strip(): + title = str(item["title"]).strip()[:30] + sub = self._collect_mindmap_children(item.get("children")) + node = {"title": title} + if sub: + node["children"] = sub + out.append(node) + elif isinstance(item, str) and item.strip(): + out.append({"title": item.strip()[:30]}) + return out[:6] + + def _fallback_chat(self, history: list, user_message: str, subject: str = "", grade: str = "") -> str: + """AI 离线时的教学顾问兜底:按意图匹配给出可落地、结构化的教学建议。""" + subject = (subject or "").strip() + grade = (grade or "").strip() + msg = self._repair_text(user_message).strip() + ctx_bits = [] + if subject: + ctx_bits.append(subject) + if grade: + ctx_bits.append(grade) + ctx = ("(" + "·".join(ctx_bits) + ")") if ctx_bits else "" + offline_note = ( + "\n\n— 当前为离线知识模式(AI 云端未连通),以上为基于教学经验的通用建议;" + "如需针对你具体班级、教材版本的个性化方案,请在 AI 服务恢复后再次提问。" + ) + + if any(k in msg for k in ["你好", "您好", "在吗", "谢谢", "感谢", "辛苦"]) and len(msg) <= 12: + hi = "你好!我是智教助手,你的教学顾问与备课搭档。" + (f"已记录你的任教学段{ctx}。" if ctx else "") + return ( + hi + "\n\n我可以帮你:备课与教学设计、知识点讲解思路、课堂活动与提问设计、" + "学情诊断与分层教学、家校沟通、复习备考、课堂管理、学生动机激发等。\n\n" + "直接告诉我你想解决的教学问题,例如:\n" + "- \"如何讲清分数乘法的算理?\"\n" + "- \"设计一节《草船借箭》的导入环节\"\n" + "- \"班上两极分化严重怎么办?\"\n" + "- \"家长群里如何回复投诉?\"" + offline_note + ) + + subj_resp = self._subject_advice(msg, subject, grade) + if subj_resp: + return subj_resp + offline_note + + if any(k in msg for k in ["备课", "教学设计", "教案", "教学环节", "教学目标", "怎么设计", "如何设计", "设计一节", "设计这节", "课时方案"]): + return self._chat_lesson_prep(ctx) + offline_note + if any(k in msg for k in ["怎么讲", "如何讲", "怎么解释", "如何解释", "讲清楚", "讲不清", "学生不懂", "听不懂", "理解不了", "突破难点", "重难点", "算理", "为什么这样", "原理"]): + return self._chat_explain(ctx) + offline_note + if any(k in msg for k in ["导入", "开场", "引入", "情境", "激发兴趣", "吸引"]): + return self._chat_intro(ctx) + offline_note + if any(k in msg for k in ["课堂活动", "互动活动", "小组活动", "合作学习", "游戏", "动手", "探究", "角色扮演", "情境表演"]): + return self._chat_activity(ctx) + offline_note + if any(k in msg for k in ["提问", "追问", "问题设计", "提问技巧", "课堂提问"]): + return self._chat_questioning(ctx) + offline_note + if any(k in msg for k in ["纪律", "管纪律", "课堂管理", "课堂常规", "调皮", "捣乱", "走神", "注意力", "不专心", "讲话", "吵闹", "坐不住"]): + return self._chat_management(ctx) + offline_note + if any(k in msg for k in ["学困生", "后进生", "学困", "待优生", "跟不上", "两极分化", "差距大", "分层", "差异化", "因材施教", "培优补差"]): + return self._chat_differentiation(ctx) + offline_note + if any(k in msg for k in ["家长", "家校", "家访", "家长会", "家长群", "沟通家长", "投诉"]): + return self._chat_parents(ctx) + offline_note + if any(k in msg for k in ["作业", "布置作业", "评价", "批改", "反馈", "形成性评价", "作业量", "作业设计"]): + return self._chat_assessment(ctx) + offline_note + if any(k in msg for k in ["不想学", "没兴趣", "没动力", "厌学", "积极性", "不想听", "敷衍", "内驱力", "学习动机"]): + return self._chat_motivation(ctx) + offline_note + if any(k in msg for k in ["复习", "备考", "期末", "期中", "考试", "复习课", "冲刺", "考点", "应试"]): + return self._chat_review(ctx) + offline_note + if any(k in msg for k in ["新教师", "新手", "刚入职", "实习", "第一次上课", "新老师", "刚上班"]): + return self._chat_newteacher(ctx) + offline_note + + return self._chat_general(msg, ctx) + offline_note + + def _subject_advice(self, msg: str, subject: str, grade: str) -> str: + """识别学科相关提问,返回学科特色建议;无匹配返回空串。""" + is_math = any(k in msg for k in ["数学", "算理", "算式", "方程", "函数", "几何", "分数", "小数", "乘法", "除法", "加减法", "应用题"]) + is_chinese = any(k in msg for k in ["语文", "课文", "识字", "写字", "阅读理解", "作文", "古诗词", "古诗", "文言文", "拼音", "草船借箭", "荷塘月色"]) + is_english = any(k in msg for k in ["英语", "单词", "语法", "口语", "听力", "时态", "从句", "because"]) + is_physics = any(k in msg for k in ["物理", "力学", "电学", "电路", "牛顿", "压强", "浮力", "光学"]) + if is_math: + return ( + "【数学教学建议】\n\n" + "一、算理优于算法\n" + "数学核心是让学生懂\"为什么\",而非记\"怎么做\"。讲新运算先用实物/画图/数轴把算理可视化(如分数乘法用面积模型),再给法则。\n\n" + "二、三讲三练结构\n" + "- 讲例题(边讲边写思维过程)→ 练模仿题\n" + "- 讲变式(改一个条件/数据)→ 练变式题\n" + "- 讲错题(学生典型错误当反面教材)→ 练易错题\n\n" + "三、让错误可见\n" + "数学最怕假装会。多用板演、白板小测、手势即时暴露思维。错题不只对答案,让学生讲\"我当时怎么想的\"。\n\n" + "四、应用题三部曲\n" + "读题(圈关键信息)→ 画图/列表(文字转成关系)→ 列式(再算)。低年级卡第一步,高年级卡第二步。\n\n" + "告诉我具体课题(如\"分数除法\"\"平行四边形面积\"),我给更细的活动设计。" + ) + if is_chinese: + return ( + "【语文教学建议】\n\n" + "一、朗读是地基\n" + "低中年级把朗读做扎实:教师范读→跟读→指名读→齐读,注意停顿、重音、语气。朗读到位,理解自然跟上。\n\n" + "二、课文\"三问法\"\n" + "- 写了什么?(整体感知)\n" + "- 怎么写的?(品词析句、写法)\n" + "- 为什么这样写?(情感、主旨)\n\n" + "三、作文从\"有话写\"到\"写得好\"\n" + "先解决没素材:多搞观察课、活动作文。再解决写不具体:训练动作分解(把\"他跑了\"拆成 5 个动作)。最后才润色文采。\n\n" + "四、识字/字词\n" + "字理识字(讲字源)、归类识字(同偏旁)、语境识字(放词句里记),比机械抄写有效。\n\n" + "告诉我具体课文或课型,我给针对性设计。" + ) + if is_english: + return ( + "【英语教学建议】\n\n" + "一、输入先行(i+1 原则)\n" + "新语言点先大量听/看→理解,再要求说/写。用 TPR、图片、情境让学生先懂意思,不要一上来就机械跟读。\n\n" + "二、词不离句,句不离境\n" + "单词放进有意义的句子里记,句子放进情境里用。避免单词—中文翻译的孤立记忆,那样学生只会背不会用。\n\n" + "三、PPP 模式\n" + "Presentation(呈现)→ Practice(控制练习)→ Production(自由产出)。第三步最常被省略,却最关键——要给真实任务去用语言。\n\n" + "四、让沉默期变短\n" + "不爱开口的学生:先做非语言回应(指、选、画),再重复性输出(跟读),最后创造性输出。降低开口焦虑。\n\n" + "告诉我具体单元/课型,我给细化方案。" + ) + if is_physics: + return ( + "【物理教学建议】\n\n" + "一、实验是物理的灵魂\n" + "能做实验绝不只讲实验。演示实验要让现象出乎意料(引发认知冲突),分组实验让每个人动手。器材不足用生活物品替代(矿泉水瓶测压强)。\n\n" + "二、从现象到规律四步\n" + "观察现象 → 提出问题 → 猜想/设计实验 → 得出规律。规律不要直接给,让学生发现。\n\n" + "三、建模与数学化\n" + "讲公式时一定讲清每个字母的物理意义和单位,强调公式是规律的语言,而非要背的符号。\n\n" + "四、典型误区预防\n" + "力与运动(惯性误解)、电流方向、浮力沉浮条件……把这些高频误区编成判断题课前测,对症讲解。" + ) + return "" + + def _chat_lesson_prep(self, ctx: str) -> str: + return ( + f"关于备课,建议你按\"目标—学情—活动—评价\"四步设计{ctx}:\n\n" + "一、明确可检验的教学目标(最关键)\n" + "用\"学生能做什么 + 什么条件下 + 达到什么程度\"来写。例:学生能在 5 分钟内独立完成 3 道两位数乘法,正确率≥80%。\n" + "避免\"让学生理解…\"\"培养…能力\"这类无法检测的目标。一节课 2-3 个核心目标即可。\n\n" + "二、诊断学情\n" + "这节课学生已有什么基础?常见误区是什么?课前用 2 道前测题摸底,或翻看上节课作业的典型错误。\n\n" + "三、设计主干活动(建议 3 环节)\n" + "- 导入(3-5 分钟):旧知、生活情境或认知冲突引入。\n" + "- 新知建构(15-20 分钟):不要直接给结论,设计让学生发现的活动——观察、操作、讨论、归纳。\n" + "- 巩固迁移(10-15 分钟):模仿→变式→应用,难度阶梯上升。\n\n" + "四、嵌入评价(防止假装听懂)\n" + "每个环节末尾设一个检测点:提问、小测、板演、同伴互评,让你随时知道学生学到哪了。\n\n" + "时间提醒:教师讲授总时长控制在 15 分钟内,把时间还给学生练与思。\n\n" + "告诉我具体课题和年级,我给完整的环节脚本与活动设计。" + ) + + def _chat_explain(self, ctx: str) -> str: + return ( + f"讲清一个难点,核心是搭台阶 + 可视化 + 证伪误区{ctx}:\n\n" + "一、先找到学生的卡点\n" + "学生不懂通常卡在某一个具体环节,不是整体不懂。问自己:这个知识依赖哪个旧知?哪个概念是转折点?把卡点定位到最小单元。\n\n" + "二、搭台阶(最近发展区)\n" + "把难点拆成 3-4 个递进小问题,每个学生能答上来,最终自己推出结论。例:讲分数除法→先复习分数意义→再问除以一个数=乘它倒数为什么成立→用面积图验证。\n\n" + "三、可视化(多通道呈现)\n" + "能画图就画图,能摆实物就摆实物。抽象概念配具象表征(数轴、面积模型、流程图、表格),让学生看见关系。\n\n" + "四、用错误反证(认知冲突)\n" + "先抛一个看起来对其实是错的判断,让多数学生踩坑,再一起分析为什么错——比直接讲正确结论印象深 3 倍。\n\n" + "五、让学生复述/教别人\n" + "讲完别问懂了吗(学生都会点头),而让 1-2 个学生用自己的话复述,或讲给同桌听。能讲清楚才是真懂。\n\n" + "把具体知识点告诉我,我帮你拆台阶、设计冲突问题。" + ) + + def _chat_intro(self, ctx: str) -> str: + return ( + f"好的导入能在 3-5 分钟抓住学生,常用 5 种方式{ctx}:\n\n" + "1. 情境导入:用学生熟悉的生活场景切入(买菜算账、校园事件、新闻热点),让知识有用。\n" + "2. 冲突导入:抛一个反直觉的现象或问题(如:1千克铁和1千克棉花谁重?),引发好奇。\n" + "3. 悬念导入:讲半截故事/留个谜,答案藏在今天的内容里。\n" + "4. 旧知导入:从上节课内容自然延伸,温故知新。\n" + "5. 操作导入:动手做个小实验/小游戏,身体先参与,注意力立刻集中。\n\n" + "避坑:导入别超过 5 分钟,别为热闹而热闹——导入必须直接服务于本课目标。\n\n" + "告诉我课题,我帮你设计 1-2 个具体导入脚本。" + ) + + def _chat_activity(self, ctx: str) -> str: + return ( + f"设计有效课堂活动,关键是任务清晰 + 人人有事做 + 及时反馈{ctx}:\n\n" + "常用活动类型\n" + "- 小组讨论:4 人一组,问题要有争议性/开放性,讨论前给 1 分钟独立思考,避免搭便车。\n" + "- 角色扮演:语文/英语/历史特别适合,提前给角色卡和台词框架。\n" + "- 操作探究:数学/科学课用学具、实验,让学生做出结论。\n" + "- 游戏竞赛:抢答、卡片配对、知识闯关,适合巩固环节,注意控制节奏。\n" + "- 同伴互教:会的学生教不会的,教的人反而学得更深。\n\n" + "让活动不失控的 3 个原则\n" + "1. 活动前讲清规则和产出(讨论结束每组派代表说 1 条),黑板上挂任务。\n" + "2. 活动中教师巡视、记录、点拨,不站在讲台。\n" + "3. 活动后必须汇报/展示/点评,否则学生会觉得讨论了没用。\n\n" + "告诉我学科和课型,我帮你设计一个具体活动。" + ) + + def _chat_questioning(self, ctx: str) -> str: + return ( + f"高质量课堂提问,记住三秒原则 + 追问 + 全员应答{ctx}:\n\n" + "一、提问前的设计\n" + "备课时写下 3-5 个核心问题,分三类:\n" + "- 记忆型(是什么)→ 唤醒旧知\n" + "- 理解型(为什么/怎么说)→ 促思考\n" + "- 应用/评价型(如果…会怎样/你同意吗)→ 促迁移\n" + "一节好课,后两类要占多数。\n\n" + "二、提问技巧\n" + "- 先问后叫人:先抛问题,停 3-5 秒(让所有人想),再随机点名。\n" + "- 追问:学生答完,追问你怎么想到的?还有别的可能吗?把思维引向深处。\n" + "- 不轻易否定:错误答案也是资源,追问这个想法哪里有问题,让全班分析。\n" + "- 全员应答:用白板、手势、答题卡、随机抽签,让每个学生被迫回应。\n\n" + "避坑:别只问对不对/是不是;别一问完立刻自己答。\n\n" + "把要讲的课题告诉我,我帮你设计一组阶梯式提问。" + ) + + def _chat_management(self, ctx: str) -> str: + return ( + f"课堂纪律靠规则前置 + 节奏控制 + 关系先行,而非吼叫{ctx}:\n\n" + "一、预防优于纠正\n" + "- 开学第一周把课堂常规立清楚(举手发言、不插嘴、安静信号),坚持执行。\n" + "- 用固定的安静信号(拍手节奏、举手倒数、手势),比喊安静有效。\n\n" + "二、走神/讲话:用教学手段拉回,而非批评\n" + "- 突然停顿 3 秒(全班会安静看你)。\n" + "- 把走神学生的名字编进例题(假设小明买了 3 个本子…),温和提醒。\n" + "- 走下讲台站到他旁边继续讲,距离本身就是管理。\n\n" + "三、调皮/对抗:私下处理,保护自尊\n" + "- 课堂上不当众发作(易激化),课后单独谈。\n" + "- 谈话用我观察到…+ 我担心…+ 我希望…句式,而非指责。\n" + "- 找到行为背后的需求(求关注?听不懂?家庭问题?),对症下药。\n\n" + "四、根本:让课足够有趣 + 师生关系好\n" + "纪律问题 80% 来自课堂太无聊或学生不喜欢老师。把课上精彩、多关心学生,纪律自然好转。\n\n" + "具体场景(哪个年级、什么行为)告诉我,我给针对性策略。" + ) + + def _chat_differentiation(self, ctx: str) -> str: + return ( + f"应对两极分化/学困生,核心是分层不减负、抓两头带中间{ctx}:\n\n" + "一、分层不分班(课内分层)\n" + "- 目标分层:同一节课,学困生只要达成基础目标,学优生有挑战任务。\n" + "- 任务分层:布置 A(基础)/B(提高)/C(拓展)三档作业,学生选做或指定。\n" + "- 提问分层:简单问题给学困生(建立信心),难题给学优生。\n\n" + "二、补差要抓前置基础\n" + "学困生卡住往往是旧知识漏洞,不是这节课听不懂。诊断他缺哪块基础,用课前 10 分钟或课后针对性补。\n\n" + "三、同伴互助(最省力)\n" + "小老师制:1 个学优生帮 1 个学困生,捆绑考核(两人都进步才表扬)。教的人学得更深,被教的人压力小、敢问。\n\n" + "四、保护学困生自尊\n" + "- 永远不当众羞辱(这么简单都不会是毒药)。\n" + "- 抓住任何小进步公开表扬,建立我能学的信念。\n" + "- 错题私下反馈,不公布排名。\n\n" + "五、培优要给空间\n" + "学优生最怕吃不饱被打发。给自主探究任务、当小老师、参加竞赛,让能量有出口。\n\n" + "具体学科和年级告诉我,我帮你设计分层任务清单。" + ) + + def _chat_parents(self, ctx: str) -> str: + return ( + f"家校沟通关键是先共情 + 讲事实 + 给方案,而非告状{ctx}:\n\n" + "一、沟通前心态\n" + "家长不是对手,是同盟。即使投诉的家长,本质也是为孩子好。先听他说完,别急着辩解。\n\n" + "二、报问题的标准话术(三明治法)\n" + "1. 先说一个孩子的优点/进步(让家长放下防备)。\n" + "2. 客观陈述问题(只讲事实,不带评价:最近三次作业有两次没交,而非这孩子太懒)。\n" + "3. 表达关心 + 给具体方案(我担心影响他后续学习,咱们一起想想办法,我建议…您看行吗?)。\n\n" + "三、家长群沟通禁忌\n" + "- 不在群里点名批评个别学生(保护隐私)。\n" + "- 不在群里跟家长争论(私聊处理)。\n" + "- 通知类信息简洁清晰,避免长语音。\n" + "- 投诉先回应已收到,X 点前给您答复,别晾着。\n\n" + "四、家访/约谈\n" + "- 提前预约,不突袭。\n" + "- 先表扬再谈问题,结束时达成 1-2 个可执行小目标。\n" + "- 记录谈话要点,下次跟进。\n\n" + "具体场景(投诉什么/孩子什么问题)告诉我,我帮你写一段沟通话术。" + ) + + def _chat_assessment(self, ctx: str) -> str: + return ( + f"作业与评价的核心是少而精 + 即时反馈 + 促学而非排名{ctx}:\n\n" + "一、作业设计原则\n" + "- 少而精:3 道有思维含量的题 > 30 道机械重复。\n" + "- 分层:基础题全员必做,挑战题选做,避免学困生抄、学优生闲。\n" + "- 形式多样:除书面外,加口头、实践、项目作业(如用本周学的统计调查家里一周用电量)。\n" + "- 必批必改必反馈:不批的作业等于没布置;批改后要讲评典型错题。\n\n" + "二、形成性评价(重过程)\n" + "不只看期末考试。日常用:课堂提问记录、小测、作品集、同伴互评,让评价贯穿学习全过程。\n\n" + "三、反馈的艺术\n" + "- 具体:第二步符号写反了 > 粗心。\n" + "- 可改进:指出下一步怎么做,而非只判对错。\n" + "- 及时:当天/次日反馈,隔一周就失效。\n" + "- 成长导向:比上次进步了比 85 分更有激励作用。\n\n" + "具体学科和题型告诉我,我帮你设计一份分层作业。" + ) + + def _chat_motivation(self, ctx: str) -> str: + return ( + f"激发学习动机,从胜任感 + 自主感 + 归属感入手{ctx}:\n\n" + "一、胜任感:让他尝到成功的甜头\n" + "没兴趣的学生,往往因为长期学不会而放弃。给他够得着的小目标,让他体验我会了——一次小测进步、一道题做对,立刻肯定。信心是兴趣的前提。\n\n" + "二、自主感:给他选择权\n" + "人对被命令的事天然抗拒。给有限选择:今天作业 A 还是 B?小组讨论你想当记录员还是汇报员?参与感提升投入度。\n\n" + "三、归属感:让他感到被看见\n" + "记住每个学生的名字和一件小事,课堂上让每个人都有发言机会。学生因为喜欢这个老师而喜欢这门课是真实存在的。\n\n" + "四、让知识有用 + 有趣\n" + "- 联系生活/热点/学生关心的事,让知识活起来。\n" + "- 用游戏、竞赛、悬念,让过程有乐趣。\n" + "- 慎用外在奖励:物质奖励/加分短期有效,长期会削弱内驱力。多用成长反馈、公开认可。\n\n" + "五、排查外部原因\n" + "突然厌学,先了解:家庭变故?同伴关系?沉迷手机?身体/心理问题?对症才能解。\n\n" + "具体是哪个学生/什么表现告诉我,我给针对性方案。" + ) + + def _chat_review(self, ctx: str) -> str: + return ( + f"复习备考要结构化 + 错题驱动 + 模拟实战,而非题海{ctx}:\n\n" + "一、先建知识结构(脑图)\n" + "复习不是把新课重讲一遍。先带学生画整章/整册知识脑图,理清脉络,让零散知识串成网。结构清晰,提取才快。\n\n" + "二、错题驱动(最高效)\n" + "把全学期高频错题/易错点整理成专题,针对性突破。会的题不重复刷,时间留给真问题。建议每个学生建错题本,定期重做。\n\n" + "三、三轮复习节奏\n" + "- 第一轮:单元过关,扫清知识盲点(重基础)。\n" + "- 第二轮:专题整合,跨章节串联(重综合)。\n" + "- 第三轮:模拟实战,限时训练(重应试与心态)。\n\n" + "四、模拟考试要仿真\n" + "限时、独立、评分标准对齐正式考试。考后不只对答案,要分析:哪类题失分?知识漏洞还是应试失误?制定针对性补救。\n\n" + "五、别忘了心态\n" + "考前焦虑的学生很多。适当减压,强调尽力就好,别把分数和人格绑定。\n\n" + "具体学科和学段告诉我,我帮你列复习专题清单。" + ) + + def _chat_newteacher(self, ctx: str) -> str: + return ( + f"新手教师上路,先稳住课堂 + 关系 + 心态三件事{ctx}:\n\n" + "一、第一年别追求花哨,先求稳\n" + "- 把常规立住(纪律、作业、发言规则),课堂乱什么都做不好。\n" + "- 备课写详案(每环节说什么、问什么、学生可能怎么答),别只写框架。\n" + "- 多听老教师的课,模仿是最好的学习。\n\n" + "二、师生关系:严慈相济\n" + "- 开学先立威(规则严、说到做到),再慢慢施恩(关心、幽默、认可)。顺序反了很难管。\n" + "- 记住每个学生名字,课后多聊天。被看见的学生才会配合你。\n" + "- 永远不当众发火/羞辱学生,一次就可能毁掉整学期关系。\n\n" + "三、教学:少讲多练,别贪多\n" + "- 一节课 2-3 个核心点足够,讲透练透比走马观花强。\n" + "- 教师讲授控制在 15 分钟内,剩下时间给学生。\n" + "- 每节课留 5 分钟检测,知道自己教得怎么样。\n\n" + "四、心态:别和名师比,和自己比\n" + "- 第一年上不好很正常,三年才成型,五年才出风格。\n" + "- 别把所有问题归咎于自己(学生基础、家庭都是变量)。\n" + "- 找一个愿意带你/听你课的师傅,成长快 3 倍。\n\n" + "具体困惑(管不住班?备课没底?家长沟通?)告诉我,我针对性支招。" + ) + + def _chat_general(self, msg: str, ctx: str) -> str: + return ( + f"我收到了你的问题。为了让建议更精准,先按问题—对象—目标帮你理一理{ctx}:\n\n" + "一、你想解决的核心问题是什么?\n" + "比如:备课没思路?某个知识点学生听不懂?课堂纪律乱?学生没兴趣?家长难沟通?复习效率低?\n\n" + "二、针对的对象和情境?\n" + "学科、年级、班级特点(人数、两极分化程度)、这节课/这个学生的具体情况。\n\n" + "三、你期望达成的目标?\n" + "想要一份完整教案?一个活动设计?一段沟通话术?一个分层作业?还是某个具体问题的解决思路?\n\n" + "把这些信息补充给我,我能给出可直接用的方案。你也可以直接试试这些常见问题:\n" + "- 如何讲清[某知识点]?\n" + "- 设计一节[某课题]的导入/活动\n" + "- 班上两极分化怎么办?\n" + "- 学生上课走神怎么管理?\n" + "- 如何回复家长的投诉?" + ) + + def _fallback_mindmap(self, topic: str, subject: str) -> dict: + topic = self._repair_text(topic) + title = topic[:24] or "思维导图" + for keys, nodes in self._mindmap_kb(): + if any(k in topic for k in keys): + return {"title": title, "nodes": nodes} + return {"title": title, "nodes": self._mindmap_subject_template(subject)} + + # ---- 主题感知思维导图知识库 ---- + _MM_COLORS = ["#00a870", "#2563eb", "#d97706", "#db2777", "#7c3aed", "#0891b2"] + + def _mm_nodes(self, *branches): + out = [] + for i, (title, kids) in enumerate(branches): + out.append({"title": title, "color": self._MM_COLORS[i % len(self._MM_COLORS)], + "children": [{"title": k} for k in kids]}) + return out + + def _mindmap_kb(self): + mm = self._mm_nodes + # 历史 + if False: + pass + kb = [ + (["朝代", "历代", "王朝", "中国古代史"], mm( + ("先秦文明", ["夏商周更替", "春秋五霸", "战国七雄"]), + ("秦汉大一统", ["秦始皇统一", "汉武帝强盛", "丝绸之路"]), + ("隋唐盛世", ["隋朝开科", "贞观之治", "开元盛世"]), + ("宋元变革", ["宋代理学", "元朝行省制", "四大发明"]), + )), + (["丝绸之路"], mm( + ("陆上丝路", ["张骞出使西域", "长安出发", "途经河西走廊"]), + ("海上丝路", ["泉州广州港口", "瓷器丝绸外销", "郑和下西洋"]), + ("贸易商品", ["丝绸", "瓷器", "茶叶", "香料"]), + ("文化交流", ["佛教东传", "四大发明西传", "中外互通"]), + )), + (["工业革命"], mm( + ("第一次工业革命", ["蒸汽机", "纺织业机械化", "铁路交通"]), + ("第二次工业革命", ["电力", "内燃机", "化学工业"]), + ("社会影响", ["城市化", "阶级变化", "殖民扩张"]), + ("科技进步", ["生产力飞跃", "世界市场形成", "环境污染"]), + )), + (["抗日战争", "抗战"], mm( + ("局部抗战", ["九一八事变", "东北沦陷", "义勇军抗争"]), + ("全面抗战", ["七七事变", "正面战场", "敌后战场"]), + ("重大战役", ["台儿庄", "百团大战", "平型关"]), + ("伟大胜利", ["统一战线", "世界反法西斯", "日本投降"]), + )), + (["文艺复兴"], mm( + ("兴起背景", ["意大利城邦", "资本主义萌芽", "人文主义"]), + ("代表人物", ["但丁", "达芬奇", "莎士比亚"]), + ("核心思想", ["以人为本", "个性解放", "反对神权"]), + ("深远影响", ["思想解放", "科学革命", "宗教改革"]), + )), + (["辛亥革命"], mm( + ("革命背景", ["清末危机", "民族资本主义", "革命思想传播"]), + ("革命过程", ["武昌起义", "各省响应", "建立民国"]), + ("历史意义", ["推翻帝制", "民主共和", "思想解放"]), + ("局限性", ["革命不彻底", "军阀割据", "任务未完成"]), + )), + # 地理 + (["气候", "气候类型", "气温降水"], mm( + ("影响因子", ["纬度", "海陆", "地形", "洋流"]), + ("主要类型", ["热带雨林", "季风气候", "温带大陆性", "地中海气候"]), + ("降水规律", ["赤道多雨", "副热带少雨", "温带增多"]), + ("中国气候", ["季风显著", "雨热同期", "气候复杂多样"]), + )), + (["地形", "地貌", "地势"], mm( + ("基本地形", ["山地", "平原", "高原", "盆地", "丘陵"]), + ("中国地势", ["西高东低", "阶梯分布", "大河东流"]), + ("外力作用", ["流水侵蚀", "风力沉积", "冰川作用"]), + ("典型地貌", ["喀斯特", "丹霞", "黄土高原", "冲积平原"]), + )), + (["河流", "水系", "长江", "黄河"], mm( + ("长江", ["发源青藏", "流经十一省", "入东海", "黄金水道"]), + ("黄河", ["发源巴颜喀拉", "泥沙含量大", "地上河", "入渤海"]), + ("河流作用", ["供水灌溉", "航运发电", "塑造平原"]), + ("治理保护", ["防洪堤坝", "水土保持", "生态修复"]), + )), + (["地球", "地球运动", "自转公转"], mm( + ("自转", ["绕轴旋转", "昼夜交替", "时差"]), + ("公转", ["绕日运行", "四季更替", "五带划分"]), + ("黄赤交角", ["23.5度", "太阳直射点移动", "昼夜长短变化"]), + ("地理意义", ["正午太阳高度", "节气", "气候形成"]), + )), + # 生物 + (["细胞"], mm( + ("细胞结构", ["细胞膜", "细胞质", "细胞核", "细胞壁(植物)"]), + ("细胞器", ["线粒体", "叶绿体", "核糖体", "液泡"]), + ("细胞分裂", ["有丝分裂", "减数分裂", "无丝分裂"]), + ("细胞分化", ["形态变化", "功能特化", "形成组织"]), + )), + (["光合作用"], mm( + ("反应条件", ["光照", "叶绿体", "适宜温度"]), + ("原料产物", ["二氧化碳", "水", "氧气", "有机物"]), + ("两个阶段", ["光反应", "暗反应", "能量转化"]), + ("重要意义", ["制造有机物", "释放氧气", "碳氧平衡"]), + )), + (["生态系统"], mm( + ("组成成分", ["非生物物质", "生产者", "消费者", "分解者"]), + ("食物链网", ["捕食关系", "能量单向", "物质循环"]), + ("能量流动", ["太阳能输入", "逐级递减", "10%-20%传递"]), + ("生态平衡", ["自我调节", "稳定性", "人类影响"]), + )), + (["遗传", "基因", "DNA"], mm( + ("遗传物质", ["DNA", "基因", "染色体"]), + ("基本规律", ["分离定律", "自由组合", "显隐性"]), + ("遗传变异", ["基因突变", "基因重组", "染色体变异"]), + ("应用拓展", ["育种", "遗传咨询", "基因工程"]), + )), + (["人体", "消化", "呼吸", "循环"], mm( + ("消化系统", ["口腔胃小肠", "消化酶", "吸收营养"]), + ("呼吸系统", ["呼吸道", "肺泡", "气体交换"]), + ("循环系统", ["心脏", "血管", "血液循环"]), + ("神经系统", ["大脑", "神经传导", "反射调节"]), + )), + # 化学 + (["元素周期表", "元素"], mm( + ("结构规律", ["周期", "族", "原子序数", "递变规律"]), + ("主族元素", ["碱金属", "卤素", "氧族", "氮族"]), + ("周期变化", ["原子半径递减", "金属性减弱", "非金属性增强"]), + ("应用价值", ["预测性质", "寻找新材料", "指导分类"]), + )), + (["化学反应"], mm( + ("反应类型", ["化合", "分解", "置换", "复分解"]), + ("反应条件", ["加热", "点燃", "催化", "通电"]), + ("能量变化", ["放热反应", "吸热反应", "化学能转化"]), + ("方程式", ["质量守恒", "配平", "符号规范"]), + )), + (["酸碱盐", "酸碱"], mm( + ("常见酸", ["盐酸", "硫酸", "硝酸", "醋酸"]), + ("常见碱", ["氢氧化钠", "氢氧化钙", "氨水"]), + ("酸碱反应", ["中和反应", "生成盐和水", "pH变化"]), + ("盐的性质", ["溶解性", "复分解", "焰色反应"]), + )), + (["原子", "分子", "物质构成"], mm( + ("原子结构", ["原子核", "质子", "中子", "核外电子"]), + ("分子构成", ["共价键", "分子间作用力", "分子特性"]), + ("离子化合物", ["得失电子", "离子键", "晶体结构"]), + ("物质分类", ["单质", "化合物", "混合物"]), + )), + # 物理 + (["力学", "力", "牛顿"], mm( + ("常见的力", ["重力", "弹力", "摩擦力", "浮力"]), + ("牛顿三定律", ["惯性定律", "加速度定律", "作用反作用"]), + ("压强", ["固体压强", "液体压强", "大气压强"]), + ("简单机械", ["杠杆", "滑轮", "斜面", "功的原理"]), + )), + (["电学", "电流", "电路", "欧姆"], mm( + ("基本物理量", ["电流", "电压", "电阻", "电功率"]), + ("欧姆定律", ["I=U/R", "串并联电阻", "电压电流关系"]), + ("电路连接", ["串联", "并联", "混联", "短路断路"]), + ("电与磁", ["电流磁效应", "电磁感应", "电动机发电机"]), + )), + (["光学", "光", "反射折射"], mm( + ("光的传播", ["直线传播", "光速", "影子小孔成像"]), + ("光的反射", ["反射定律", "镜面反射", "漫反射"]), + ("光的折射", ["折射定律", "全反射", "色散"]), + ("透镜成像", ["凸透镜", "凹透镜", "成像规律", "应用"]), + )), + (["运动", "速度", "加速度"], mm( + ("运动描述", ["参照物", "路程位移", "速度"]), + ("匀速运动", ["速度恒定", "s=vt", "图像分析"]), + ("变速运动", ["加速度", "v=v0+at", "匀变速直线"]), + ("相对运动", ["相对速度", "相遇追及", "实际应用"]), + )), + # 数学 + (["函数"], mm( + ("函数概念", ["定义域", "值域", "对应关系"]), + ("基本初等函数", ["一次函数", "二次函数", "反比例函数"]), + ("指数对数", ["指数函数", "对数函数", "运算法则"]), + ("函数性质", ["单调性", "奇偶性", "周期性"]), + )), + (["几何", "三角形", "圆"], mm( + ("线与角", ["点线面", "相交平行", "角的关系"]), + ("三角形", ["内角和", "全等相似", "勾股定理"]), + ("四边形", ["平行四边形", "矩形菱形", "梯形"]), + ("圆", ["圆的性质", "切线", "弧长扇形"]), + )), + (["方程", "一元", "二元", "不等式"], mm( + ("一元一次", ["等式性质", "求解步骤", "应用题"]), + ("方程组", ["二元一次", "代入消元", "加减消元"]), + ("一元二次", ["因式分解", "公式法", "韦达定理"]), + ("不等式", ["性质", "一元一次不等式", "不等式组"]), + )), + (["概率", "统计"], mm( + ("数据收集", ["普查抽样", "频数频率", "统计图表"]), + ("数据特征", ["平均数", "中位数众数", "方差极差"]), + ("概率初步", ["事件分类", "古典概型", "频率估计", "树状图列举"]), + ("应用", ["决策分析", "风险评估", "生活实例"]), + )), + # 语文 + (["古诗", "古诗词", "唐诗宋词"], mm( + ("发展脉络", ["诗经楚辞", "唐诗", "宋词", "元曲"]), + ("代表诗人", ["李白杜甫", "白居易", "苏轼李清照"]), + ("常见题材", ["山水田园", "边塞", "咏物言志", "送别"]), + ("鉴赏方法", ["意象意境", "表现手法", "炼字", "情感主旨"]), + )), + (["记叙文"], mm( + ("六要素", ["时间地点", "人物", "起因经过结果"]), + ("结构层次", ["开头引入", "主体展开", "结尾点题"]), + ("表达方式", ["叙述", "描写", "议论抒情"]), + ("写作技巧", ["详略得当", "线索贯穿", "细节描写"]), + )), + (["议论文"], mm( + ("三要素", ["论点", "论据", "论证"]), + ("论点确立", ["中心论点", "分论点", "鲜明准确"]), + ("论证方法", ["举例论证", "道理论证", "对比比喻"]), + ("结构思路", ["提出问题", "分析问题", "解决问题"]), + )), + # 英语 + (["时态"], mm( + ("一般时态", ["一般现在", "一般过去", "一般将来"]), + ("进行时", ["现在进行", "过去进行", "be+doing"]), + ("完成时", ["现在完成", "过去完成", "have+done"]), + ("时间状语", ["always/often", "yesterday", "already/yet"]), + )), + (["从句"], mm( + ("名词性从句", ["主语从句", "宾语从句", "表语从句"]), + ("定语从句", ["关系代词", "关系副词", "限制非限制"]), + ("状语从句", ["时间原因", "条件让步", "目的结果"]), + ("用法要点", ["连接词", "语序", "时态呼应"]), + )), + ] + return kb + + def _mindmap_subject_template(self, subject: str) -> list: + subject = (subject or "").strip() + mm = self._mm_nodes + templates = { + "数学": mm( + ("概念理解", ["定义", "性质", "判定条件"]), + ("公式法则", ["基本公式", "运算法则", "推导过程"]), + ("典型例题", ["基础题", "应用题", "易错题"]), + ("知识应用", ["实际问题", "跨学科", "拓展提升"]), + ), + "物理": mm( + ("物理概念", ["定义", "物理意义", "单位"]), + ("规律定律", ["适用条件", "公式表达", "实验基础"]), + ("实验探究", ["实验器材", "操作步骤", "数据处理"]), + ("生活应用", ["现象解释", "技术应用", "综合计算"]), + ), + "化学": mm( + ("物质组成", ["元素", "结构", "性质"]), + ("变化规律", ["反应类型", "反应条件", "实验现象"]), + ("化学计算", ["化学式", "方程式", "质量计算"]), + ("实际应用", ["材料能源", "环境保护", "生命健康"]), + ), + "生物": mm( + ("生命现象", ["结构基础", "生理功能", "生命活动"]), + ("核心概念", ["定义", "原理", "机制"]), + ("实验观察", ["方法", "现象", "结论"]), + ("联系应用", ["生产实践", "健康生活", "生态保护"]), + ), + "历史": mm( + ("时代背景", ["政治", "经济", "文化"]), + ("重要事件", ["时间", "人物", "过程"]), + ("深远影响", ["制度变革", "思想文化", "社会发展"]), + ("历史启示", ["经验教训", "规律认识", "现实意义"]), + ), + "地理": mm( + ("空间分布", ["位置", "范围", "界线"]), + ("自然要素", ["地形气候", "水文土壤", "植被"]), + ("人文要素", ["人口城市", "农业工业", "交通"]), + ("人地关系", ["资源利用", "环境问题", "可持续发展"]), + ), + "语文": mm( + ("基础知识", ["字词", "语句", "修辞"]), + ("文本理解", ["内容", "结构", "主旨"]), + ("表达技巧", ["描写方法", "表现手法", "语言特色"]), + ("拓展运用", ["写作借鉴", "迁移阅读", "文化传承"]), + ), + "英语": mm( + ("词汇积累", ["核心单词", "短语搭配", "词形变化"]), + ("语法要点", ["句型结构", "时态语态", "从句"]), + ("功能话题", ["日常交际", "话题表达", "文化背景"]), + ("技能训练", ["听说", "读写", "综合运用"]), + ), + } + if subject in templates: + return templates[subject] + return mm( + ("核心概念", ["定义", "特征", "分类"]), + ("知识结构", ["基本原理", "重要规律", "内在联系"]), + ("重点方法", ["解题思路", "分析方法", "易错提示"]), + ("应用拓展", ["生活实例", "学科关联", "拓展提升"]), + ) + + + async def generate_animation(self, prompt: str, anim_type: str = "general") -> dict: + prompt = self._repair_text(prompt) + system_prompt = f"""你是一位教学动画设计专家。根据教师描述,生成教学动画的HTML页面。 + +重要规则: +- 所有HTML属性必须用单引号,如 style='color:red;' onclick='fn()' +- 不要使用" + ) + if qtype == "fill_blank": + return ( + "" + "

    " + stem + "

    " + + "" + + "" + + "

    " + + "" + ) + return "

    " + stem + "

    " + + def _fallback_lesson_plan(self, title, subject, grade, objectives, duration): + t = title or "大单元教案" + subj = (subject or "").strip() + prof = self._lesson_plan_profile(subj) + new_dur = max(10, duration - 20) + practice_dur = max(8, duration // 4) + return { + "title": t, + "objectives": objectives or prof["objectives"](t), + "key_points": prof["key_points"](t), + "difficulties": prof["difficulties"](t), + "phases": [ + {"name": "导入", "duration": 5, "activities": [prof["intro"](t)], "teacher_actions": ["创设情境,激发兴趣。", "提出核心问题。"], "student_actions": ["观察思考。", "联系已有经验。"], "resources": prof["intro_resources"]}, + {"name": "新授", "duration": new_dur, "activities": prof["new_activities"](t), "teacher_actions": prof["new_teacher"], "student_actions": prof["new_student"], "resources": prof["new_resources"]}, + {"name": "练习巩固", "duration": practice_dur, "activities": prof["practice"](t), "teacher_actions": ["巡视指导,个别辅导。"], "student_actions": prof["practice_student"], "resources": prof["practice_resources"]}, + {"name": "总结", "duration": 5, "activities": [prof["summary"](t)], "teacher_actions": ["梳理知识脉络,提炼方法。"], "student_actions": ["回顾反思,记录收获。"], "resources": ["板书", "思维导图"]}, + ], + "homework": prof["homework"](t), + "reflection": f"根据课堂反馈与学生掌握情况,调整「{t}」后续教学的深度、广度与练习分层策略。", + } + + def _lesson_plan_profile(self, subject: str) -> dict: + s = subject + profiles = { + "数学": { + "objectives": lambda t: [f"理解「{t}」的概念内涵与几何或代数意义", f"掌握「{t}」的基本公式与运算法则", "能运用所学知识分析并解决实际问题"], + "key_points": lambda t: [f"「{t}」的定义与核心性质", "公式推导与规范书写"], + "difficulties": lambda t: [f"「{t}」的灵活运用与变形", "数形结合思想的建立"], + "intro": lambda t: f"借助生活实例或已有知识,引出「{t}」的研究必要性。", + "new_activities": lambda t: [f"通过探究活动归纳「{t}」的概念与性质。", "推导公式并分析适用条件。"], + "new_teacher": ["引导探究,板书推导过程。", "组织小组讨论。"], + "new_student": ["动手操作或画图探究。", "小组合作归纳结论。"], + "intro_resources": ["课件", "实物教具", "几何画板"], + "new_resources": ["学案", "探究任务单", "动态演示"], + "practice": lambda t: ["基础题巩固概念。", "变式训练提升能力。", "实际问题应用。"], + "practice_student": ["独立完成,规范步骤。", "同桌互批,错题订正。"], + "practice_resources": ["分层练习", "答题卡"], + "summary": lambda t: f"梳理「{t}」的知识结构图,提炼解题方法与易错点。", + "homework": lambda t: [f"完成「{t}」分层作业(基础+提升)。", "整理本节易错题集。"], + }, + "物理": { + "objectives": lambda t: [f"理解「{t}」的物理意义与适用条件", "掌握相关公式并能进行简单计算", "经历实验探究过程,培养科学思维"], + "key_points": lambda t: [f"「{t}」的概念建立与公式表达", "实验现象的观察与分析"], + "difficulties": lambda t: [f"「{t}」物理模型的构建", "实验方案的设计与数据分析"], + "intro": lambda t: f"通过演示实验或生活现象,引发对「{t}」的思考。", + "new_activities": lambda t: ["分组实验探究规律。", "分析实验数据,归纳结论。"], + "new_teacher": ["演示关键实验,强调安全。", "指导小组实验,引导分析。"], + "new_student": ["分组实验,记录数据。", "分析数据,得出结论。"], + "intro_resources": ["演示器材", "课件", "视频"], + "new_resources": ["学生实验器材", "数据记录表"], + "practice": lambda t: ["基础概念辨析。", "公式应用计算。", "实验数据分析题。"], + "practice_student": ["独立解题,规范作图。", "小组互查实验报告。"], + "practice_resources": ["练习卷", "实验报告单"], + "summary": lambda t: f"构建「{t}」的知识网络,强调实验方法与物理思想。", + "homework": lambda t: [f"完成「{t}」课后练习。", "撰写实验探究报告。"], + }, + } + # 化学/生物共用理科探究模板 + sci = { + "objectives": lambda t: [f"理解「{t}」的基本概念与原理", "掌握核心知识并能解释相关现象", "培养实验观察与科学探究能力"], + "key_points": lambda t: [f"「{t}」的核心概念与特征", "实验现象的观察与解释"], + "difficulties": lambda t: [f"「{t}」微观机制的理解", "知识在真实情境中的迁移应用"], + "intro": lambda t: f"通过实验现象或生活实例,引出「{t}」的探究问题。", + "new_activities": lambda t: ["实验观察与操作。", "小组讨论归纳知识要点。"], + "new_teacher": ["组织实验,强调规范操作。", "引导分析现象背后的原理。"], + "new_student": ["动手实验,如实记录。", "讨论交流,形成结论。"], + "intro_resources": ["实验器材", "课件", "标本/模型"], + "new_resources": ["实验材料", "学习单"], + "practice": lambda t: ["概念辨析与判断。", "现象解释与应用。"], + "practice_student": ["独立完成练习。", "小组互评实验记录。"], + "practice_resources": ["练习卡", "实验记录册"], + "summary": lambda t: f"梳理「{t}」的知识体系,强调科学方法与探究思路。", + "homework": lambda t: [f"完成「{t}」巩固练习。", "预习下一节内容并完成预习单。"], + } + for k in ("化学", "生物"): + profiles[k] = sci + profiles["语文"] = { + "objectives": lambda t: [f"品味「{t}」的语言特色与表达技巧", "理解文章主旨与作者情感", "学习并运用相关的写作方法"], + "key_points": lambda t: [f"「{t}」的文本内容与结构梳理", "关键语句的赏析与理解"], + "difficulties": lambda t: [f"「{t}」深层意蕴与作者情感的体悟", "写作手法在表达中的迁移运用"], + "intro": lambda t: f"借助背景介绍或朗读导入,唤起对「{t}」的阅读期待。", + "new_activities": lambda t: ["初读感知,整体把握文意。", "精读品味,赏析关键语段。"], + "new_teacher": ["范读指导,组织品析。", "点拨赏析方法。"], + "new_student": ["朗读体会,圈点批注。", "小组交流赏析心得。"], + "intro_resources": ["课件", "背景资料", "音频朗读"], + "new_resources": ["文本", "批注学习单"], + "practice": lambda t: ["仿写练习。", "片段赏析与表达。"], + "practice_student": ["独立完成仿写。", "分享交流,互评互改。"], + "practice_resources": ["仿写学习单", "评价量表"], + "summary": lambda t: f"总结「{t}」的写法与主旨,提炼可借鉴的表达技巧。", + "homework": lambda t: [f"围绕「{t}」完成读写结合小练笔。", "积累本文好词好句。"], + } + profiles["英语"] = { + "objectives": lambda t: [f"掌握「{t}」相关核心词汇与句型", "能在真实情境中运用所学进行交流", "提升听、说、读、写综合语言能力"], + "key_points": lambda t: [f"「{t}」的核心词汇与目标句型", "语言功能的得体运用"], + "difficulties": lambda t: [f"「{t}」相关语法结构的正确使用", "在真实交际中的灵活表达"], + "intro": lambda t: f"通过歌曲、游戏或情境对话导入「{t}」话题。", + "new_activities": lambda t: ["词汇与句型学习。", "情境对话操练与角色扮演。"], + "new_teacher": ["创设情境,示范句型。", "组织pair work与group work。"], + "new_student": ["跟读模仿,记忆词汇。", "结对操练,大胆表达。"], + "intro_resources": ["课件", "音频视频", "单词卡"], + "new_resources": ["对话任务单", "角色卡片"], + "practice": lambda t: ["词汇句型巩固。", "情境交际任务。"], + "practice_student": ["完成听力与填空。", "小组表演对话。"], + "practice_resources": ["练习单", "评价表"], + "summary": lambda t: f"回顾「{t}」核心语言知识,梳理交际策略。", + "homework": lambda t: [f"完成「{t}」听读作业。", "用目标句型写一段对话。"], + } + profiles["历史"] = { + "objectives": lambda t: [f"了解「{t}」的基本史实与发展脉络", "分析历史事件的因果关系与影响", "培养历史思维与家国情怀"], + "key_points": lambda t: [f"「{t}」的关键事件、人物与时间", "历史发展规律与时代特征"], + "difficulties": lambda t: [f"对「{t}」历史现象的多角度分析", "史论结合,以史为鉴"], + "intro": lambda t: f"通过史料、图片或视频导入「{t}」的时代背景。", + "new_activities": lambda t: ["梳理时间线与重大事件。", "研读史料,分析因果关系。"], + "new_teacher": ["提供史料,引导分析。", "组织讨论,启发思考。"], + "new_student": ["阅读史料,做时间轴。", "小组讨论,发表见解。"], + "intro_resources": ["课件", "历史地图", "史料摘录"], + "new_resources": ["史料学习单", "时间轴模板"], + "practice": lambda t: ["史实梳理填空。", "材料分析题。"], + "practice_student": ["独立完成基础题。", "小组研讨材料题。"], + "practice_resources": ["练习卷", "材料研读单"], + "summary": lambda t: f"构建「{t}」的知识框架,总结历史启示。", + "homework": lambda t: [f"完成「{t}」巩固练习。", "撰写一段历史小短评。"], + } + profiles["地理"] = { + "objectives": lambda t: [f"认识「{t}」的空间分布与基本特征", "理解各地理要素的相互关系", "树立人地协调与可持续发展观念"], + "key_points": lambda t: [f"「{t}」的位置、分布与主要特征", "自然与人文地理要素的相互作用"], + "difficulties": lambda t: [f"「{t}」空间格局的综合分析", "读图分析与地理推理"], + "intro": lambda t: f"借助地图、卫星图或景观图导入「{t}」。", + "new_activities": lambda t: ["读图分析空间分布。", "小组探究地理要素关系。"], + "new_teacher": ["指导读图方法。", "组织探究活动。"], + "new_student": ["读图标注,提取信息。", "小组合作,归纳特征。"], + "intro_resources": ["课件", "地图", "景观图片"], + "new_resources": ["空白地图", "探究任务单"], + "practice": lambda t: ["读图填图训练。", "综合分析题。"], + "practice_student": ["独立完成读图题。", "小组研讨分析题。"], + "practice_resources": ["练习图册", "分析学习单"], + "summary": lambda t: f"整理「{t}」知识结构,强调人地关系与可持续发展。", + "homework": lambda t: [f"完成「{t}」读图与练习。", "调查身边的地理现象。"], + } + if s in profiles: + return profiles[s] + # 通用模板 + return { + "objectives": lambda t: [f"理解「{t}」的核心内容与基本要求", "掌握关键知识与基本方法", "培养分析思维与合作探究能力"], + "key_points": lambda t: [f"「{t}」的核心知识建构", "方法与技能的训练"], + "difficulties": lambda t: [f"「{t}」的深入理解与灵活运用", "知识的迁移与综合"], + "intro": lambda t: f"创设情境,引出「{t}」的学习主题。", + "new_activities": lambda t: [f"围绕「{t}」进行讲解与互动探究。"], + "new_teacher": ["讲解引导,组织探究。"], + "new_student": ["主动思考,合作探究。"], + "intro_resources": ["课件", "情境素材"], + "new_resources": ["学习单", "探究材料"], + "practice": lambda t: ["分层练习巩固。", "互评与订正。"], + "practice_student": ["独立完成,规范作答。"], + "practice_resources": ["练习卡"], + "summary": lambda t: f"梳理「{t}」知识结构,提炼重点方法。", + "homework": lambda t: [f"完成「{t}」巩固作业。", "预习下节内容。"], + } + + def _fallback_essay_grade(self, essay_text, total_score): + text = essay_text or "" + char_count = len(text) + import re as _re2 + sentences = [s.strip() for s in _re2.split(r"[。!?]", text) if s.strip()] + sent_count = len(sentences) + has_dialog = "“" in text or "说" in text + has_detail = any(w in text for w in ("有一次", "记得", "那天", "当时", "忽然", "只见")) + avg_sent = char_count / max(sent_count, 1) + score_ratio = 0.82 if avg_sent > 25 else 0.74 + score = max(1, int(total_score * score_ratio)) + annotations = [] + if has_detail: + for kw in ("有一次", "记得", "那天", "当时"): + pos = text.find(kw) + if pos >= 0: + snippet = text[max(0,pos-3):pos+12] + annotations.append({"text": snippet, "comment": "这里用具体事例展开,增加了文章的真实感。", "type": "good"}) + break + if char_count < 100: + annotations.append({"text": text[:20], "comment": "文章字数偏少,建议增加具体描写。", "type": "error"}) + strengths = [] + weaknesses = [] + suggestions = [] + if has_detail: + strengths.append("能用具体事例展开叙述,增加了文章的感染力。") + else: + weaknesses.append("缺乏具体事例和细节描写,内容较空洞。") + suggestions.append("增加一到两个具体事例,展开细节描写。") + if has_dialog: + strengths.append("运用了对话描写,使人物形象更加鲜活。") + else: + suggestions.append("可以加入对话描写,让人物更鲜活。") + if sent_count <= 2: + weaknesses.append("句子偏少,结构较单谬。") + suggestions.append("增加句子数量,丰富文章层次。") + if avg_sent > 40: + weaknesses.append("部分句子较长,建议适当断句。") + suggestions.append("将过长的句子拆分为短句,提高可读性。") + content_ratio = 0.32 if has_detail else 0.25 + content_s = int(total_score * content_ratio) + structure_s = int(total_score * 0.22) + language_s = int(total_score * 0.18) + writing_s = score - content_s - structure_s - language_s + if not strengths: + strengths = ["主题明确,有基本的表达。"] + if not weaknesses: + weaknesses = ["可以在细节上进一步丰富。"] + if not suggestions: + suggestions = ["继续保持认真观察和真诚表达。"] + level = "优秀" if score_ratio > 0.80 else ("良好" if score_ratio > 0.72 else "中等") + return { + "total_score": score, + "scores": { + "content": {"score": content_s, "max": int(total_score * 0.3), "comment": "内容较为充实。" if has_detail else "内容基本完整,建议增加细节。"}, + "structure": {"score": structure_s, "max": int(total_score * 0.25), "comment": "结构较清晰。" if sent_count > 2 else "结构较为单谬,建议增加层次。"}, + "language": {"score": language_s, "max": int(total_score * 0.25), "comment": "表达较通顺。" if avg_sent <= 40 else "部分句子较长,可适当断句。"}, + "writing": {"score": writing_s, "max": int(total_score * 0.2), "comment": "书写规范尚可。"}, + }, + "overall_comment": "作文围绕主题展开," + ("能用具体事例进行叙述," if has_detail else "但缺乏具体细节,") + "表达有一定的真实感。" + ("建议继续加强细节描写和语言表现力,让文章更有感染力。" if not has_detail else "建议在语言精炼和结构层次上进一步提升。"), + "strengths": strengths, + "weaknesses": weaknesses, + "suggestions": suggestions, + "annotations": annotations, + "level": level, + } + + def _fallback_exam(self, subject, grade, knowledge_points, difficulty, question_types, count, total_score): + total = max(1, min(count or 10, 20)) + each = max(1, total_score // max(total, 1)) + subj = subject or "综合" + kp = "、".join(knowledge_points) if knowledge_points else "基础知识" + bank = self._question_bank(kp, subj) + if not bank: + bank = self._generic_question_bank(kp, subj) + questions = [] + for idx in range(total): + q = dict(bank[idx % len(bank)]) + q["id"] = idx + 1 + q["type"] = q.get("type", (question_types or ["choice"])[0]) + q["score"] = each + q["difficulty"] = difficulty if isinstance(difficulty, str) else ["easy", "medium", "hard"][idx % 3] + q["content"] = q.pop("question", None) or q.get("content", "") + q["analysis"] = q.get("explanation", q.get("analysis", "")) + if "explanation" in q: + q.pop("explanation", None) + if "html" in q: + q.pop("html", None) + questions.append(q) + return {"title": f"{subj}{grade or ''}智能试卷", "questions": questions} + + def _courseware_category(self, prompt: str) -> str: + prompt = self._repair_text(prompt) + if any(k in prompt for k in ["圆柱", "圆柱的体积", "体积公式", "底面积", "高"]): + return "cylinder" + return "general" + + def _courseware_shell(self, title: str, subtitle: str, body: str, footer_left: str = "") -> str: + title_html = html_lib.escape(title) + subtitle_html = html_lib.escape(subtitle) + footer_html = html_lib.escape(footer_left) + return f"""
    + +
    +

    {title_html}

    {subtitle_html}

    +
    AI生成
    +
    +
    {body}
    + +
    """ + + def _fallback_courseware(self, prompt: str, subject: str, grade: str, page_count: int) -> dict: + prompt = self._repair_text(prompt) + if self._courseware_category(prompt) == "cylinder": + return self._fallback_cylinder_courseware(prompt, subject, grade, page_count) + return self._fallback_general_courseware(prompt, subject, grade, page_count) + + def _normalize_courseware_result(self, result: dict, prompt: str, subject: str, grade: str, page_count: int, aspect_ratio: str) -> dict | None: + if not isinstance(result, dict): + return None + pages = result.get("pages") + if not isinstance(pages, list) or len(pages) < 5: + return None + normalized_pages = [] + for page in pages: + if not isinstance(page, dict): + continue + content = page.get("content") + if not isinstance(content, str): + continue + cleaned = content.replace("100vh", "100%").replace("100vw", "100%") + if not cleaned.strip().lower().startswith("{cleaned}" + normalized_pages.append({ + "type": page.get("type") or "content", + "title": self._repair_text(str(page.get("title") or "")), + "content": cleaned, + "notes": self._repair_text(str(page.get("notes") or "")), + }) + if len(normalized_pages) < 5: + return None + return { + "title": self._repair_text(str(result.get("title") or prompt[:24] or "课件")), + "summary": self._repair_text(str(result.get("summary") or "")), + "tags": [self._repair_text(str(tag)) for tag in (result.get("tags") or []) if str(tag).strip()], + "pages": normalized_pages[:max(5, min(page_count, len(normalized_pages)))], + "aspect_ratio": aspect_ratio, + "subject": subject, + "grade": grade, + } + + def _courseware_topic(self, prompt: str) -> str: + prompt = self._repair_text(prompt) + text = prompt.lower() + if any(k in prompt for k in ["地球公转", "四季", "春分", "夏至", "秋分", "冬至", "昼夜长短", "太阳直射"]) or any(k in text for k in ["orbit", "season"]): + return "orbit" + if any(k in prompt for k in ["圆柱", "体积", "底面积", "圆锥", "长方体"]): + return "cylinder" + return "general" + + def _courseware_shell_clean(self, title: str, subtitle: str, body: str, footer_left: str = "") -> str: + title_html = html_lib.escape(title) + subtitle_html = html_lib.escape(subtitle) + footer_html = html_lib.escape(footer_left) + return ( + "
    " + "" + "" + "

    " + title_html + "

    " + subtitle_html + "

    AI生成
    " + "
    " + + body + + "
    " + "" + "
    " + ) + + def _fallback_cylinder_courseware(self, prompt: str, subject: str, grade: str, page_count: int) -> dict: + prompt = self._repair_text(prompt) + title = "圆柱的体积(第一课时)" + pages = [ + { + "type": "title", + "title": "封面", + "content": self._courseware_shell_clean( + title, + f"{subject or '数学'} · {grade or '六年级'}", + """ +
    +
    课堂导入
    +
    +
    +
    数学 · 形体与测量
    +

    圆柱的体积

    +

    围绕“底面积 × 高”展开探究,通过观察、切分、比较和例题,把抽象公式变成可理解、可操作的课堂体验。

    +
    + 层次清楚 + 页面可切换 + 支持课堂互动 +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +""", + "封面页", + ), + "notes": "封面", + }, + { + "type": "content", + "title": "知识回顾", + "content": self._courseware_shell_clean( + "知识回顾", + "先把旧知识拉回来,再进入新课探究", + """ +
    +
    + + + +
    +
    +
    +
    旧知 1
    +

    长方体体积公式

    +

    体积和底面积、高有关。先看“底面铺了多少”,再看“竖起来有多高”。

    +
    + + +
    +
    +""", + "点击卡片切换旧知", + ), + "notes": "知识回顾", + }, + { + "type": "interactive", + "title": "引入新知", + "content": self._courseware_shell_clean( + "引入新知", + "先看形状和高度,再让学生猜一猜体积和什么有关", + """ +
    +
    +
    情境导入
    +
    +
    +
    +
    +
    +
    课堂提问
    +

    你觉得圆柱体积和什么有关?

    +
    +
    A. 只和底面形状有关
    +
    B. 和底面积、高都有关系
    +
    C. 只和颜色有关
    +
    +
    课堂结论:体积计算要抓住“底面积 × 高”这条主线。
    +
    +
    +""", + "学生先猜想,再看图验证", + ), + "notes": "引入", + }, + { + "type": "content", + "title": "公式探究", + "content": self._courseware_shell_clean( + "公式探究", + "用切、拼、比的方式,把圆柱体积想清楚", + """ +
    +
    + + + +
    +
    +
    +
    切分观察
    +

    把圆柱切成很多薄片

    +

    圆柱越像被分成很多很薄的小层,每一层越接近一个圆形小面。层数越多,拼出的形状就越接近长方体。

    +
    + + +
    +
    +""", + "切、拼、比,逐步导出公式", + ), + "notes": "公式探究", + }, + { + "type": "interactive", + "title": "例题讲解", + "content": self._courseware_shell_clean( + "例题讲解", + "把公式落到计算里,按步骤说清楚", + """ +
    +
    +
    例题
    +

    一个圆柱的底面积是 28.26 cm²,高是 10 cm,体积是多少?

    +
    + + + +
    +
    +
    +
    已知
    +
    S = 28.26 cm²,h = 10 cm
    +
    + + +
    +
    +
    +
    解题提醒
    +
    圆柱体积公式中的 S 是底面积,不是底面周长。单位也要对应写成立方单位。
    +
    写答语时要带上单位,例如 cm³、m³。
    +
    +
    +""", + "按步骤理解计算过程", + ), + "notes": "例题", + }, + { + "type": "exercise", + "title": "巩固练习", + "content": self._courseware_shell_clean( + "巩固练习", + "点击选项,立即查看判断结果", + """ +
    +
    +
    选择题
    +

    圆柱体积公式正确的是哪一个?

    +
    + + + +
    +
    +
    +
    +
    反馈
    +
    A 不是正确答案。圆柱体积不是把周长直接相乘。
    +
    + + +
    +
    +""", + "选择答案后直接反馈", + ), + "notes": "练习", + }, + { + "type": "summary", + "title": "总结", + "content": self._courseware_shell_clean( + "总结与作业", + "把公式、方法和易错点一次收好", + """ +
    +
    +
    本课总结
    +
    +
    1. 圆柱体积公式:V = S × h
    +
    2. 核心方法:切、拼、转化成近似长方体
    +
    3. 易错提醒:S 是底面积,不是底面周长
    +
    +
    +
    +
    课后任务
    +
    1. 完成课本练习。
    2. 找一个生活中的圆柱体,量一量底面和高,试着算体积。
    + + +
    +
    +""", + "总结方法并布置作业", + ), + "notes": "总结", + }, + ] + total = max(5, min(page_count or 7, len(pages))) + return {"title": title, "summary": "围绕底面积与高探究圆柱体积", "tags": ["圆柱", "体积", "互动课件"], "pages": pages[:total]} + + def _fallback_orbit_courseware(self, prompt: str, subject: str, grade: str, page_count: int) -> dict: + prompt = self._repair_text(prompt) + title = "地球公转与四季变化" + pages = [ + { + "type": "title", + "title": "封面", + "content": self._courseware_shell_clean( + title, + f"{subject or '科学'} · {grade or '六年级'}", + """ +
    +
    情境导入
    +
    +
    +
    科学 · 地球与宇宙
    +

    地球公转与四季变化

    +

    通过轨道、倾角和阳光照射方向的变化,把春夏秋冬的形成过程讲清楚、看明白、能点击。

    +
    + 轨道示意 + 四季按钮 + 课堂可演示 +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +""", + "封面页", + ), + "notes": "封面", + }, + { + "type": "content", + "title": "知识回顾", + "content": self._courseware_shell_clean( + "知识回顾", + "先把轨道、地轴倾斜和太阳光照这三个要点记住", + """ +
    +
    +
    要点 1
    +

    地球在公转

    +

    地球绕太阳转一圈,需要大约一年。

    +
    +
    +
    要点 2
    +

    地轴是倾斜的

    +

    地轴倾斜让同一地区在不同时间受到的阳光不同。

    +
    +
    +
    要点 3
    +

    太阳光照变化

    +

    阳光照射角度和昼夜长短,会随着公转位置变化。

    +
    +
    +""", + "三个关键概念先对齐", + ), + "notes": "知识回顾", + }, + { + "type": "interactive", + "title": "四季探究", + "content": self._courseware_shell_clean( + "四季探究", + "点击春夏秋冬,观察光照和气候变化", + """ +
    +
    + + + + +
    +
    +
    +
    春季
    +

    阳光开始变强,气温慢慢回升

    +

    春季时太阳直射位置逐渐北移,北半球白天变长,天气回暖,植物也开始生长。

    +
    + + + +
    +
    + +""", + "四季按钮可直接切换", + ), + "notes": "四季探究", + }, + { + "type": "content", + "title": "现象解释", + "content": self._courseware_shell_clean( + "现象解释", + "同样是太阳,为什么不同季节感觉不一样?", + """ +
    +
    +
    原因 1
    +

    阳光直射角度不同

    +

    角度越直,单位面积接收到的热量越多;角度越斜,热量越分散。

    +
    +
    +
    原因 2
    +

    白昼长短不同

    +

    白天越长,太阳照射时间越久,地面吸收热量也更多。

    +
    +
    +""", + "从光照与时间两个角度解释", + ), + "notes": "现象解释", + }, + { + "type": "summary", + "title": "总结", + "content": self._courseware_shell_clean( + "总结与作业", + "把公转、地轴倾斜和四季变化连起来理解", + """ +
    +
    +
    本课总结
    +
    +
    1. 地球围绕太阳公转,形成一年四季的周期变化。
    +
    2. 地轴倾斜,让不同季节接受的阳光不同。
    +
    3. 阳光直射角度和白昼长短,决定了冷热变化。
    +
    +
    +
    +
    课后任务
    +
    1. 画一张地球绕太阳公转示意图。
    2. 说一说春夏秋冬各自最明显的特点。
    + + +
    +
    +""", + "把四季原因说完整", + ), + "notes": "总结", + }, + ] + total = max(5, min(page_count or 6, len(pages))) + return {"title": title, "summary": "围绕地球公转与太阳光照变化解释四季形成", "tags": ["地球公转", "四季", "科学探究"], "pages": pages[:total]} + + def _fallback_general_courseware(self, prompt: str, subject: str, grade: str, page_count: int) -> dict: + prompt = self._repair_text(prompt) + title = prompt[:24] or "互动课件" + pages = [ + { + "type": "title", + "title": "封面", + "content": self._courseware_shell_clean( + title, + f"{subject or '综合'} · {grade or '课堂教学'}", + """ +
    +
    课堂导入
    +
    +
    +
    知识生成 · 互动呈现
    +

    """ + html_lib.escape(title) + """

    +

    用清爽、分层、可点击的页面组织课堂内容,适合投屏展示和逐步讲解。

    +
    + 层次清楚 + 页面可切换 + 支持课堂互动 +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +""", + "封面页", + ), + "notes": "封面", + }, + { + "type": "content", + "title": "核心概念", + "content": self._courseware_shell_clean( + "核心概念", + "先把问题拆开,再逐步组织内容", + """ +
    +
    +
    第 1 点
    +

    要讲什么

    +

    把主题拆成 2 到 3 个核心问题,再分别展开。

    +
    +
    +
    第 2 点
    +

    怎么讲

    +

    先观察,再归纳,最后用例子验证。

    +
    +
    +
    第 3 点
    +

    怎么练

    +

    用点击反馈、小练习和总结页收尾。

    +
    +
    +""", + "搭好课堂结构", + ), + "notes": "核心概念", + }, + { + "type": "interactive", + "title": "互动探究", + "content": self._courseware_shell_clean( + "互动探究", + "点击左侧选项,右侧显示不同内容", + """ +
    +
    + + + +
    +
    +
    +
    观察
    +

    先看现象,再找规律

    +

    围绕图片、数据或情境,先让学生说出“看见了什么”,再引导他们说“为什么会这样”。

    +
    + + +
    +
    +""", + "交互区可直接切换", + ), + "notes": "互动探究", + }, + { + "type": "exercise", + "title": "课堂练习", + "content": self._courseware_shell_clean( + "课堂练习", + "选择一个答案,马上看反馈", + """ +
    +
    +
    练习题
    +

    下面哪一种说法最符合今天的主题?

    +
    + + + +
    +
    +
    +
    +
    反馈
    +
    A 不完整。课堂结构不能只靠一个结论支撑。
    +
    + + +
    +
    +""", + "选择后看反馈", + ), + "notes": "课堂练习", + }, + { + "type": "summary", + "title": "总结", + "content": self._courseware_shell_clean( + "总结与作业", + "把今天的重点提炼成一页", + """ +
    +
    +
    本课总结
    +
    +
    1. 先观察,再分析,再总结。
    +
    2. 把复杂内容拆成若干清楚的部分。
    +
    3. 用点击互动提高课堂参与感。
    +
    +
    +
    +
    课后任务
    +
    1. 把本课内容整理成 3 条要点。
    2. 试着为一个新的知识点设计 1 个互动问题。
    + + +
    +
    +""", + "整理成可复述的结论", + ), + "notes": "总结", + }, + ] + total = max(5, min(page_count or 5, len(pages))) + return {"title": title, "summary": "把主题拆分成观察、分析、总结的互动课件", "tags": ["互动课件", "可点击", "课堂讲解"], "pages": pages[:total]} + + async def generate_courseware(self, prompt: str, subject: str = "", grade: str = "", page_count: int = 8, aspect_ratio: str = "16:9") -> dict: + prompt = self._repair_text(prompt) + topic = self._courseware_topic(prompt) + if topic == "cylinder": + return self._fallback_cylinder_courseware(prompt, subject or "数学", grade or "六年级", page_count) + if topic == "orbit": + return self._fallback_orbit_courseware(prompt, subject or "科学", grade or "六年级", page_count) + + try: + result = await self._chat_json([ + {"role": "system", "content": "请生成多页互动课件 JSON,包含 title、summary、tags、pages。每页 content 必须是可直接渲染的
    HTML 片段,页面结构清爽,适合教室投屏。"}, + {"role": "user", "content": f"学科:{subject or '未指定'}\n年级:{grade or '未指定'}\n页数要求:{page_count}\n\n教师需求:{prompt}"}, + ], temperature=0.45, max_tokens=12000) + normalized = self._normalize_courseware_result(result, prompt, subject, grade, page_count, aspect_ratio) + if normalized: + return normalized + except Exception: + pass + return self._fallback_general_courseware(prompt, subject, grade, page_count) + diff --git a/backend/services/audit.py b/backend/services/audit.py new file mode 100644 index 0000000..561eb2c --- /dev/null +++ b/backend/services/audit.py @@ -0,0 +1,96 @@ +"""安全审计日志服务。 + +提供 log_action() 用于在任意路由中记录安全相关事件。日志写入数据库 audit_logs 表, +同时输出到 logger,便于后续接入文件/集中式日志收集。 +""" +from __future__ import annotations + +import logging +from typing import Any, Optional + +from fastapi import Request +from sqlalchemy.orm import Session + +from models.audit import AuditLog +from models.user import User + +logger = logging.getLogger(__name__) + + +def _user_label(user: Optional[User]) -> str: + """返回用户可读标识:优先 username,其次 phone,最后 id。""" + if user is None: + return "" + for attr in ("username", "phone", "name"): + val = getattr(user, attr, None) + if val: + return str(val) + return str(getattr(user, "id", "")) + + +def _client_ip(request: Optional[Request]) -> str: + if request is None: + return "" + try: + fwd = request.headers.get("x-forwarded-for") + if fwd: + return fwd.split(",")[0].strip()[:64] + client = getattr(request, "client", None) + if client and client.host: + return client.host[:64] + except Exception: + pass + return "" + + +def _user_agent(request: Optional[Request]) -> str: + if request is None: + return "" + try: + return (request.headers.get("user-agent") or "")[:255] + except Exception: + return "" + + +def log_action( + db: Session, + *, + action: str, + user: Optional[User] = None, + request: Optional[Request] = None, + target_type: str = "", + target_id: Any = "", + detail: str = "", + status: str = "success", +) -> None: + """记录一条审计日志。失败不影响主流程。""" + try: + log = AuditLog( + user_id=user.id if user else None, + username=_user_label(user), + action=action[:64], + target_type=target_type[:32] if target_type else "", + target_id=str(target_id)[:64] if target_id else "", + detail=detail[:2000] if detail else "", + ip=_client_ip(request), + user_agent=_user_agent(request), + status=status[:16] if status else "success", + ) + db.add(log) + db.commit() + except Exception as exc: # pragma: no cover - 日志失败不应中断业务 + logger.warning("audit log failed: %s", exc) + try: + db.rollback() + except Exception: + pass + # 同时输出到日志通道 + logger.info( + "audit action=%s user=%s target=%s/%s status=%s ip=%s", + action, + _user_label(user), + target_type, + target_id, + status, + _client_ip(request), + ) diff --git a/backend/services/auth.py b/backend/services/auth.py new file mode 100644 index 0000000..ccb31a9 --- /dev/null +++ b/backend/services/auth.py @@ -0,0 +1,87 @@ +import logging +from datetime import datetime, timedelta, timezone +from jose import jwt, JWTError +import bcrypt +from fastapi import Depends, HTTPException, status +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials +from sqlalchemy.orm import Session +from config import get_settings +from database import get_db +from models.user import User + +logger = logging.getLogger(__name__) +security = HTTPBearer() +optional_security = HTTPBearer(auto_error=False) +settings = get_settings() + + +def get_password_hash(password: str) -> str: + salt = bcrypt.gensalt() + hashed = bcrypt.hashpw(password.encode("utf-8"), salt) + return hashed.decode("utf-8") + + +def verify_password(plain_password: str, hashed_password: str) -> bool: + return bcrypt.checkpw(plain_password.encode("utf-8"), hashed_password.encode("utf-8")) + + +def create_access_token(user_id: int) -> str: + expire = datetime.now(timezone.utc) + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) + payload = {"sub": str(user_id), "exp": expire, "type": "access"} + token = jwt.encode(payload, settings.SECRET_KEY, algorithm=settings.ALGORITHM) + logger.info(f"Created token for user {user_id}") + return token + + +def create_refresh_token(user_id: int) -> str: + expire = datetime.now(timezone.utc) + timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS) + payload = {"sub": str(user_id), "exp": expire, "type": "refresh"} + return jwt.encode(payload, settings.SECRET_KEY, algorithm=settings.ALGORITHM) + + +def get_current_user( + credentials: HTTPAuthorizationCredentials = Depends(security), + db: Session = Depends(get_db), +) -> User: + token = credentials.credentials + try: + payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]) + user_id = int(payload.get("sub")) + logger.info(f"Token decoded for user {user_id}") + except JWTError as e: + logger.error(f"JWT decode error: {e}") + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="无效的认证凭证") + except (TypeError, ValueError) as e: + logger.error(f"Invalid sub claim: {e}") + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="无效的认证凭证") + + user = db.query(User).filter(User.id == user_id).first() + if user is None: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="用户不存在") + if not user.is_active: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="账号已被禁用") + return user + + +def get_optional_current_user( + credentials: HTTPAuthorizationCredentials | None = Depends(optional_security), + db: Session = Depends(get_db), +) -> User | None: + if credentials is None: + return None + try: + payload = jwt.decode(credentials.credentials, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]) + user_id = int(payload.get("sub")) + except (JWTError, TypeError, ValueError): + return None + + user = db.query(User).filter(User.id == user_id).first() + if user is None or not user.is_active: + return None + return user + + +def get_admin_user(user: User = Depends(get_current_user)) -> User: + if user.role != "admin": + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="需要管理员权限") + return user diff --git a/backend/services/credits.py b/backend/services/credits.py new file mode 100644 index 0000000..5799b8f --- /dev/null +++ b/backend/services/credits.py @@ -0,0 +1,103 @@ +from fastapi import HTTPException +from sqlalchemy.orm import Session + +from models.credit import CreditAccount, CreditTransaction +from models.user import User + +DEFAULT_CREDIT_BALANCE = 130 +DAILY_CHECKIN_CREDITS = 10 + +CREDIT_COSTS = { + "courseware_generate": 8, + "animation_generate": 6, + "exercise_generate": 4, + "lesson_plan_generate": 5, + "exam_generate": 5, + "essay_grade": 2, + "chat_generate": 1, + "material_parse": 1, +} + + +def ensure_credit_account(db: Session, user: User) -> CreditAccount: + account = db.query(CreditAccount).filter(CreditAccount.user_id == user.id).first() + if account: + return account + account = CreditAccount( + user_id=user.id, + balance=DEFAULT_CREDIT_BALANCE, + total_granted=DEFAULT_CREDIT_BALANCE, + total_used=0, + ) + db.add(account) + db.flush() + return account + + +def spend_credits(db: Session, user: User, action: str, description: str = "") -> CreditAccount: + cost = CREDIT_COSTS.get(action, 1) + account = ensure_credit_account(db, user) + if account.balance < cost: + raise HTTPException(status_code=402, detail=f"积分不足,本次需要 {cost} 分,当前剩余 {account.balance} 分") + account.balance -= cost + account.total_used += cost + db.add(CreditTransaction( + user_id=user.id, + amount=-cost, + action=action, + description=description, + balance_after=account.balance, + )) + db.flush() + return account + + +def credits_payload(account: CreditAccount, action: str) -> dict: + return { + "balance": account.balance, + "cost": CREDIT_COSTS.get(action, 1), + "action": action, + } + + +def grant_credits(db: Session, user: User, amount: int, action: str = "grant", description: str = "") -> CreditAccount: + if amount <= 0: + raise HTTPException(status_code=400, detail="积分数量必须大于 0") + account = ensure_credit_account(db, user) + account.balance += amount + account.total_granted += amount + db.add(CreditTransaction( + user_id=user.id, + amount=amount, + action=action, + description=description, + balance_after=account.balance, + )) + db.flush() + return account + + +def has_credit_transaction_today(db: Session, user: User, action: str) -> bool: + from datetime import datetime, time + + today = datetime.now().date() + start = datetime.combine(today, time.min) + end = datetime.combine(today, time.max) + return db.query(CreditTransaction).filter( + CreditTransaction.user_id == user.id, + CreditTransaction.action == action, + CreditTransaction.created_at >= start, + CreditTransaction.created_at <= end, + ).first() is not None + + +def grant_daily_checkin(db: Session, user: User) -> CreditAccount: + if has_credit_transaction_today(db, user, "daily_checkin"): + raise HTTPException(status_code=409, detail="今日积分已领取") + return grant_credits( + db, + user, + DAILY_CHECKIN_CREDITS, + action="daily_checkin", + description="每日登录领取积分", + ) diff --git a/backend/services/limiter.py b/backend/services/limiter.py new file mode 100644 index 0000000..38404a8 --- /dev/null +++ b/backend/services/limiter.py @@ -0,0 +1,4 @@ +from slowapi import Limiter +from slowapi.util import get_remote_address + +limiter = Limiter(key_func=get_remote_address) diff --git a/backend/services/password_policy.py b/backend/services/password_policy.py new file mode 100644 index 0000000..a6e99df --- /dev/null +++ b/backend/services/password_policy.py @@ -0,0 +1,27 @@ +"""密码强度校验策略:拒绝常见弱密码,要求字母+数字组合。""" +import re + +WEAK_PATTERNS = [ + "123456", "123456789", "password", "111111", "000000", "888888", + "abc123", "qwerty", "654321", "12345678", "1234567", "admin", +] + +CONSECUTIVE = ["0123456789", "9876543210", "abcdef", "qwerty", "asdfgh"] + + +def validate_password_strength(password: str) -> str | None: + """返回错误提示;通过则返回 None。要求至少 6 位且含字母与数字。""" + if not password or len(password) < 6: + return "密码至少需要 6 个字符" + low = password.lower() + if low in WEAK_PATTERNS: + return "密码过于简单,请使用更复杂的密码" + if any(seq in low for seq in CONSECUTIVE): + return "密码包含连续字符,请更换" + has_letter = bool(re.search(r"[a-zA-Z]", password)) + has_digit = bool(re.search(r"\d", password)) + if not has_letter: + return "密码必须包含字母" + if not has_digit: + return "密码必须包含数字" + return None diff --git a/backend/services/upload_security.py b/backend/services/upload_security.py new file mode 100644 index 0000000..8fc9a96 --- /dev/null +++ b/backend/services/upload_security.py @@ -0,0 +1,130 @@ +"""文件上传安全校验。 + +提供 validate_upload() 对上传文件做: +1. 文件名清洗(去除路径、危险字符、控制长度) +2. 扩展名白名单校验 +3. MIME 与扩展名一致性校验 +4. 文件魔数(magic bytes)校验,防止伪装 +5. 文件大小校验 +""" +from __future__ import annotations + +import re +from dataclasses import dataclass +from typing import Optional + +from fastapi import HTTPException, UploadFile + + +# 允许的扩展名 → 期望的 MIME 前缀(; 前部分) +ALLOWED_TYPES: dict[str, str] = { + "txt": "text/", + "md": "text/", + "csv": "text/", + "json": "application/json", + "docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "pdf": "application/pdf", + "xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "xls": "application/vnd.ms-excel", + "png": "image/png", + "jpg": "image/jpeg", + "jpeg": "image/jpeg", + "gif": "image/gif", + "webp": "image/webp", + "bmp": "image/bmp", +} + +# 魔数签名(前若干字节) +MAGIC_SIGNATURES: dict[str, list[bytes]] = { + "pdf": [b"%PDF-"], + "png": [b"\x89PNG\r\n\x1a\n"], + "jpg": [b"\xff\xd8\xff"], + "jpeg": [b"\xff\xd8\xff"], + "gif": [b"GIF87a", b"GIF89a"], + "bmp": [b"BM"], + "webp": [b"RIFF"], + # Office 文件均为 ZIP 容器(PK\x03\x04) + "docx": [b"PK\x03\x04"], + "pptx": [b"PK\x03\x04"], + "xlsx": [b"PK\x03\x04"], + "xls": [b"\xd0\xcf\x11\xe0"], # OLE 复合文档 +} + +MAX_FILENAME_LEN = 200 + + +@dataclass +class UploadCheck: + filename: str + suffix: str + media_type: str + size: int + + +_FILENAME_BAD = re.compile("[\x00-\x1f<>:\"/\\|?*]") + + +def sanitize_filename(name: str) -> str: + """清洗文件名:去路径、去控制/危险字符、限长。""" + # 仅保留文件名部分 + name = re.split(r"[\\/]", name)[-1].strip() + name = _FILENAME_BAD.sub("_", name) + name = name.strip(". ") + if not name: + name = "upload" + if len(name) > MAX_FILENAME_LEN: + stem, dot, ext = name.rpartition(".") + if dot: + name = stem[: MAX_FILENAME_LEN - len(ext) - 1] + "." + ext + else: + name = name[:MAX_FILENAME_LEN] + return name + + +def _check_magic(suffix: str, head: bytes) -> bool: + sigs = MAGIC_SIGNATURES.get(suffix) + if not sigs: + return True # 无签名要求(文本类)默认放行 + return any(head.startswith(sig) for sig in sigs) + + +def validate_upload( + file: UploadFile, + *, + content: bytes, + max_size: Optional[int] = None, +) -> UploadCheck: + """校验上传文件,失败抛出 HTTPException。 + + 参数 content 为已读取的字节内容(用于魔数校验)。 + """ + if max_size is not None and len(content) > max_size: + raise HTTPException(status_code=413, detail=f"文件不能超过 {max_size // (1024 * 1024)}MB") + if not content: + raise HTTPException(status_code=400, detail="上传文件为空") + + raw_name = file.filename or "material" + filename = sanitize_filename(raw_name) + suffix = filename.rsplit(".", 1)[-1].lower() if "." in filename else "" + media_type = (file.content_type or "").split(";")[0].strip().lower() + + if suffix not in ALLOWED_TYPES: + raise HTTPException( + status_code=415, + detail="暂不支持该文件类型,请上传 txt、md、csv、json、docx、pptx、pdf、xlsx 或图片", + ) + + # MIME 一致性:图片/office/pdf 必须匹配;文本类型放宽 + expected_mime = ALLOWED_TYPES[suffix] + if expected_mime != "text/" and media_type and not media_type.startswith(expected_mime.split("/")[0]): + # 宽松:只校验主类型一致(image/* application/* 等) + if media_type.split("/")[0] != expected_mime.split("/")[0]: + raise HTTPException(status_code=415, detail="文件类型与扩展名不一致") + + # 魔数校验 + head = content[:16] + if not _check_magic(suffix, head): + raise HTTPException(status_code=415, detail="文件内容与声明类型不符") + + return UploadCheck(filename=filename, suffix=suffix, media_type=media_type or expected_mime, size=len(content)) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..1a44459 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,57 @@ +version: '3.8' + +services: + backend: + build: + context: ./backend + dockerfile: Dockerfile + ports: + - "8000:8000" + environment: + - DATABASE_URL=postgresql://jiaoyu:jiaoyu123@db:5432/jiaoyu + - REDIS_URL=redis://redis:6379/0 + - AI_API_KEY=${AI_API_KEY} + - AI_API_BASE=${AI_API_BASE:-https://api.openai.com/v1} + - AI_MODEL=${AI_MODEL:-gpt-4o} + - SECRET_KEY=${SECRET_KEY:-change-me-in-production} + - CORS_ORIGINS=["http://localhost","http://localhost:5173"] + volumes: + - ./uploads:/app/uploads + depends_on: + - db + - redis + restart: unless-stopped + + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + ports: + - "80:80" + depends_on: + - backend + restart: unless-stopped + + db: + image: postgres:16-alpine + environment: + POSTGRES_DB: jiaoyu + POSTGRES_USER: jiaoyu + POSTGRES_PASSWORD: jiaoyu123 + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - "5432:5432" + restart: unless-stopped + + redis: + image: redis:7-alpine + ports: + - "6379:6379" + volumes: + - redis_data:/data + restart: unless-stopped + +volumes: + postgres_data: + redis_data: diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000..123446c --- /dev/null +++ b/docs/api.md @@ -0,0 +1,109 @@ +# API 接口文档 + +Base URL: `http://localhost:8000` + +## 认证模块 + +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | /api/auth/register | 教师注册 | +| POST | /api/auth/login | 教师登录 | +| GET | /api/auth/me | 获取个人信息 | +| PUT | /api/auth/me | 更新个人信息 | + +## 课件模块 + +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | /api/coursewares/ | 创建课件 | +| GET | /api/coursewares/ | 课件列表(支持筛选) | +| GET | /api/coursewares/{id} | 课件详情 | +| PUT | /api/coursewares/{id} | 更新课件 | +| DELETE | /api/coursewares/{id} | 删除课件 | +| POST | /api/coursewares/ai-generate | AI生成课件 | + +## 教学动画模块 + +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | /api/animations/ | 创建动画 | +| GET | /api/animations/ | 动画列表 | +| GET | /api/animations/{id} | 动画详情 | +| PUT | /api/animations/{id} | 更新动画 | +| DELETE | /api/animations/{id} | 删除动画 | +| POST | /api/animations/ai-generate | AI生成动画 | + +## 互动练习模块 + +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | /api/exercises/ | 创建练习 | +| GET | /api/exercises/ | 练习列表 | +| GET | /api/exercises/{id} | 练习详情 | +| DELETE | /api/exercises/{id} | 删除练习 | +| POST | /api/exercises/ai-generate | AI生成练习 | +| POST | /api/exercises/{id}/attempt | 提交答题 | + +## 教案模块 + +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | /api/lesson-plans/ | 创建教案 | +| GET | /api/lesson-plans/ | 教案列表 | +| GET | /api/lesson-plans/{id} | 教案详情 | +| DELETE | /api/lesson-plans/{id} | 删除教案 | +| POST | /api/lesson-plans/ai-generate | AI生成教案 | + +## AI能力模块 + +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | /api/ai/essay-grade | 作文批改 | +| POST | /api/ai/exam-generate | AI命题 | + +## 资源库模块 + +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | /api/resources/ | 上传资源 | +| GET | /api/resources/ | 资源列表 | +| GET | /api/resources/{id} | 资源详情 | +| POST | /api/resources/{id}/like | 点赞 | +| DELETE | /api/resources/{id} | 删除资源 | + +## 教师社区模块 + +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | /api/community/posts | 发布帖子 | +| GET | /api/community/posts | 帖子列表 | +| GET | /api/community/posts/{id} | 帖子详情 | +| POST | /api/community/posts/{id}/like | 点赞帖子 | +| DELETE | /api/community/posts/{id} | 删除帖子 | +| GET | /api/community/posts/{id}/comments | 评论列表 | +| POST | /api/community/posts/{id}/comments | 发表评论 | + +## 认证方式 + +所有需要认证的接口请在请求头中携带 JWT Token: + +``` +Authorization: Bearer +``` + +## 通用响应格式 + +成功响应: +```json +{ + "success": true, + "data": { ... } +} +``` + +错误响应: +```json +{ + "detail": "错误信息描述" +} +``` diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..3a13200 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,202 @@ +# 系统架构设计 + +## 整体架构 + +``` +┌─────────────────────────────────────────────────────────┐ +│ Nginx 反向代理 │ +├────────────────────────┬────────────────────────────────┤ +│ 前端 (Vue 3 SPA) │ 静态资源 / CDN │ +├────────────────────────┴────────────────────────────────┤ +│ API Gateway (FastAPI) │ +├──────────┬──────────┬──────────┬──────────┬─────────────┤ +│ 认证模块 │ 课件模块 │ AI模块 │ 课堂模块 │ 社区模块 │ +├──────────┴──────────┴──────────┴──────────┴─────────────┤ +│ 服务层 (Business Logic) │ +│ ┌─────────┐ ┌──────────┐ ┌─────────┐ ┌──────────────┐ │ +│ │AI服务 │ │课件引擎 │ │动画引擎 │ │ 练习生成引擎 │ │ +│ └─────────┘ └──────────┘ └─────────┘ └──────────────┘ │ +├─────────────────────────────────────────────────────────┤ +│ 数据层 (Data Layer) │ +│ ┌──────────┐ ┌──────────┐ ┌─────────────────────┐ │ +│ │PostgreSQL │ │ Redis │ │ 文件存储 (OSS) │ │ +│ └──────────┘ └──────────┘ └─────────────────────┘ │ +└─────────────────────────────────────────────────────────┘ +``` + +## 模块划分 + +### 1. 用户与认证模块 (auth) + +- 教师注册/登录 (手机号 + 验证码) +- JWT Token 认证 +- 角色权限管理 (教师/管理员) +- 个人信息管理 + +### 2. AI课件生成模块 (courseware) + +**核心流程:** +``` +教师输入 → AI理解意图 → 结构化课件大纲 → 逐页生成内容 → 互动元素注入 → 预览/编辑 → 发布 +``` + +**子功能:** +- 文本输入 → 课件生成 +- 语音输入 → 转写 → 课件生成 +- 上传教材/文档 → 内容提取 → 课件生成 +- 课件模板管理 +- 课件在线编辑 (富文本 + 互动组件) + +### 3. 教学动画模块 (animation) + +**动画类型:** +- 数学: 函数图像动画、几何变换、公式推导 +- 物理: 力学模拟、电路实验、光学实验 +- 化学: 分子结构、化学反应过程 +- 通用: 流程图动画、时间轴动画 + +**技术实现:** +- 基于 Canvas/SVG 的动画引擎 +- AI生成动画参数配置 (JSON Schema) +- 动画模板库 + 自定义动画 + +### 4. 互动练习模块 (exercise) + +**练习类型:** +- 游戏化练习: 贪吃蛇(单词)、消消乐(配对)、闯关 +- 选择题 / 判断题 / 填空题 +- 拖拽排序 / 连线匹配 +- 口语跟读 (语音识别) +- AI自适应出题 + +**生成流程:** +``` +知识点/单词表 → AI分析 → 选择游戏模板 → 生成题目 → 预览 → 发布 +``` + +### 5. 教案生成模块 (lesson-plan) + +- 根据课程标准和知识点生成教案 +- 教案模板: 导入→新授→练习→总结→作业 +- 支持自定义教案结构 +- 教案与课件/练习关联 + +### 6. AI命题模块 (exam) + +- 按知识点、难度、题型智能组卷 +- 题型支持: 选择、填空、判断、简答、计算 +- 题目去重与相似度检测 +- 自动生成答案与解析 + +### 7. 作文批改模块 (essay) + +- 多维度评分: 内容、结构、语言、书写 +- 逐段/逐句批注 +- 优秀范文推荐 +- 常见错误统计分析 + +### 8. 课堂互动模块 (classroom) + +- 在线授课 (WebRTC) +- 实时白板 (Canvas) +- 计时器 / 随机点名 +- 课堂答题器 (实时统计) +- 课堂评价 + +### 9. 资源库模块 (resource) + +- 个人资源管理 +- 课件/动画/教案收藏 +- 标签分类与搜索 +- 资源版本管理 + +### 10. 教师社区模块 (community) + +- 资源发布与分享 +- 经验交流帖 +- 点赞/评论/收藏 +- 关注体系 + +## 数据库设计 (核心表) + +```sql +-- 用户表 +users: id, phone, name, avatar, subject, school, role, created_at + +-- 课件表 +coursewares: id, user_id, title, subject, grade, description, + content(JSON), status, version, created_at, updated_at + +-- 动画表 +animations: id, user_id, title, type, config(JSON), + thumbnail, status, created_at + +-- 练习表 +exercises: id, user_id, title, type, questions(JSON), + subject, knowledge_points, created_at + +-- 教案表 +lesson_plans: id, user_id, title, subject, grade, content(JSON), + objectives, key_points, created_at + +-- 试卷表 +exams: id, user_id, title, subject, questions(JSON), + answers(JSON), duration, total_score, created_at + +-- 资源表 +resources: id, user_id, type, title, content_ref, + tags, downloads, likes, status, created_at + +-- 社区帖子表 +posts: id, user_id, title, content, type, tags, + views, likes, comments_count, created_at + +-- 评论表 +comments: id, post_id, user_id, content, created_at +``` + +## AI服务架构 + +``` +┌──────────────────────────────────────┐ +│ AI Service Layer │ +├──────────────────────────────────────┤ +│ │ +│ ┌────────────┐ ┌────────────────┐ │ +│ │ Prompt模板 │ │ 教育知识库 │ │ +│ │ 管理 │ │ (RAG) │ │ +│ └────────────┘ └────────────────┘ │ +│ │ +│ ┌────────────────────────────────┐ │ +│ │ LLM API 调用层 │ │ +│ │ (OpenAI / 自研模型 / 本地模型) │ │ +│ └────────────────────────────────┘ │ +│ │ +│ ┌────────────────────────────────┐ │ +│ │ 输出处理与验证 │ │ +│ │ (JSON Schema验证/安全过滤) │ │ +│ └────────────────────────────────┘ │ +│ │ +└──────────────────────────────────────┘ +``` + +### Prompt 工程 + +每种AI功能对应一套精心设计的Prompt模板: + +1. **课件生成Prompt**: 输入教学意图 → 输出结构化课件JSON +2. **动画生成Prompt**: 输入知识点 → 输出动画参数配置 +3. **练习生成Prompt**: 输入知识点/单词 → 输出游戏化练习数据 +4. **教案生成Prompt**: 输入课程信息 → 输出结构化教案 +5. **命题Prompt**: 输入知识点/难度 → 输出题目+答案+解析 +6. **批改Prompt**: 输入作文 → 输出评分+批注+建议 + +## 安全设计 + +- JWT认证 + Refresh Token +- API限流 (Redis) +- 输入校验 (Pydantic) +- XSS/CSRF防护 +- 文件上传安全检查 +- 数据库参数化查询 +- 敏感信息加密存储 diff --git a/docs/platform-parity-roadmap.md b/docs/platform-parity-roadmap.md new file mode 100644 index 0000000..87e5964 --- /dev/null +++ b/docs/platform-parity-roadmap.md @@ -0,0 +1,83 @@ +# 同类 AI 教学平台功能路线 + +本项目目标是实现同类 AI 教学创作平台能力,功能逻辑对标,但品牌、文案、视觉素材和生成模板保持原创。 + +## 已落地 + +| 能力 | 当前实现 | +| --- | --- | +| 创作广场 | 首页资源卡片、分类筛选、快速创作入口 | +| 一句话生成互动课件 | `/courseware/create`,AI 生成多页 HTML 互动课件 | +| 教学动画 | `/animation`,AI 生成 HTML/CSS 教学动画,支持数学/物理/化学/生物/地理/通用 | +| 教学游戏/互动练习 | `/exercise`,8种题型,互动答题(选择/填空)、即时反馈、计分、答案解析 | +| AI 教案 | `/lesson-plan`,按课题、学科、年级、目标生成教案 | +| AI 命题 | `/exam`,按学科、知识点、题型、难度生成试卷 | +| 作文批改 | `/essay-grade`,作文评分、评语、优缺点和修改建议 | +| 模板社区/资源库 | `/community`、`/resources`,资源展示、发布、点赞、评论;站内通知系统(评论/收藏自动触发、头部铃铛未读计数、一键已读) | +| 课堂工具/数据回收 | `/classroom`,投票、问答、测验、签到、反馈,支持提交统计 | +| 多模态入口雏形 | 首页支持文本材料上传、图片描述注入、浏览器语音识别 | +| 成果导出 | 课件/动画类 HTML 下载,试卷/教案 Word 下载 + 打印 PDF(浏览器原生打印对话框,可另存为 PDF) | +| 材料智能解析 | 后端解析 docx、pptx、pdf、xlsx/txt/md/csv/json,图片经 AI 视觉 OCR 提取文字 | +| 数据回收学生端 | `/classroom/join` 公开提交页、投放码/链接、投放大屏、实时统计、Excel 导出 | +| 会员/积分体系 | 生成扣费、每日签到、积分流水、余额实时同步 | +| 课件编辑器页面管理 | 添加/复制/移动/删除页面,页面标题、类型与备注编辑 | +| 课件编辑器撤销重做 | 历史栈撤销/重做,Ctrl+Z / Ctrl+Shift+Z 快捷键 | +| 管理后台 | 运营看板、用户管理(启停/角色/发积分)、资源审核(上下架) | +| 数据库迁移 | Alembic 初始化,autogenerate + batch 模式,初始迁移已 stamp | +| 接口限流 | slowapi 限流:登录/注册、AI 生成、材料解析接口 | +| 账号安全 | 修改密码(校验旧密码/禁止新旧相同/密码强度策略/审计日志)+ 找回密码(手机号验证码重置、限流、验证码 5 分钟过期)+ 头像上传(图片白名单/魔数校验/5MB) | +| 令牌刷新 | access_token 过期自动用 refresh_token 静默刷新(单飞去重,避免并发 401 重复刷新),刷新失败才跳登录;登录/注册同时存储 refresh_token | +| AI 思维导图 | /mindmap,AI 生成层次化知识思维导图,交互式可折叠树、分支配色、展开/折叠、HTML 导出、保存到我的导图(CRUD + 耗积分);后端 mind_maps 表与路由、AIService.generate_mindmap + fallback | +| 动画模板库 | 36 个参数化学科动画模板:二次函数、正弦波、斜抛运动、简谐振动、分子运动、细胞分裂、太阳系公转、昼夜四季、几何变换、串并联电路、食物链能量流动、水循环、历史时间线、朝代更替轮、诗词意境、分数可视化、光的折射与全反射、酸碱中和滴定、光合作用过程、概率模拟(大数定律)、单词卡片翻转记忆(英语词汇)、英语时态时间轴(英语语法)、汉字笔顺动画(语文识字)、钢琴键盘与简谱(音乐)、三原色混色实验(美术)、二进制拨码开关(信息技术)、拼音四声调对比(语文)、元素周期表交互、化学键与分子结构、pH酸碱指示剂(化学拓展);覆盖数学/物理/化学(5)/生物/地理/历史/语文/英语/音乐/美术/信息技术/通用 12 学科;TemplateGallery 组件接入动画生成页,免积分即用即改 | +| 游戏模板库 | 20 个参数化教学游戏模板:知识贪吃蛇、翻牌记忆配对、排序拖拽、冒险闯关问答、抢答打地鼠、填词连连看、转盘答题、找词游戏、知识飞行棋、知识合成 2048、打字竞速、猜词游戏(猜字母拼单词)、判断对错速答(限时连对奖励)、选词填空(Cloze 拖放)、算术竞速(四则限时);TemplateGallery 接入练习生成页,模板自动映射题型;并修复贪吃蛇/转盘/找词三个旧模板的脚本语法错误 | +| 课件编辑器增强 | 6 套主题模板(简洁/暖橙/海洋/森林/深夜/日落)一键换肤 + 模板插入按钮(TemplateGallery 复用);保留页面增删改/撤销重做 | +| 品牌替换 | 汉字/拼音/CSS 前缀全量切换为自有品牌 zj-(前端样式与后端生成模板),源码与 AI 生成内容(课件/动画/教案等)经全工作区扫描 + 多类型生成验证零残留 | +| 模板插值修复 | 修复 strokeOrder/4 个新模板的反引号字符串插值 bug:原 `var X=\"+name+\"` 写法在浏览器里被解析为字面字符串 `\"+name+\"` 而非变量值,导致参数实际未生效(仅靠 `\|\| 默认值` 兜底掩盖);统一改为正确的 `${name}` 模板插值,参数现在真正传入 IIFE | +| AI 实时生成可用 | 修复核心瓶颈:`_chat` 读取超时 8s→120s(推理模型需 20-90s 思考)、OCR 30s→90s;空内容守卫(reasoning 模型思考耗尽预算时抛 `AI_PROVIDER_EMPTY_RESPONSE` 触发兜底);`max_tokens` 默认 4096→8192(防复杂生成 JSON 截断),chat_teaching 2048→8192、mindmap 4096→8192;端到端验证:练习(勾股定理几何意义,92s,5题)+试卷(一元二次方程,64s,6题)均产出高质量真实内容,无截断、无兜底模板 | +| JWT 密钥安全 | 修复高危漏洞:SECRET_KEY 原硬编码为 `change-me-in-production` 默认值,源码泄露即可伪造任意用户/管理员 token。改为 pydantic field_validator 启动校验——生产模式(DEBUG=False)遇到弱/默认/空/短(<32)密钥直接 RuntimeError 拒绝启动;开发模式自动生成临时密钥并警告;.env 已配置 64 字符随机强密钥。端到端验证:旧密钥伪造 token 返回 401 | +| 生产安全 | 上传安全校验(扩展名白名单/MIME 一致性/魔数检测/文件名清洗/大小限制)+ 审计日志覆盖登录/注册/材料解析/发布资源/管理员发积分/改用户/资源审核,admin 可查询 + AuditLog 表与 Alembic 迁移 | + + +| 练习/命题兜底题库扩展 | _exercise_kb() 从 14 个主题扩展到 35 个,覆盖历史(朝代/丝路/工业革命/抗战/文艺复兴/辛亥革命)、地理(气候/地形/河流/地球运动)、生物(细胞/光合/生态/遗传/人体)、化学(元素/反应/酸碱盐/原子)、物理(力学/电学/光学/运动)、数学(几何三角形/函数/方程/一元二次/二次函数/概率统计)、语文(荷塘月色/古诗词/记叙文/议论文)、英语(时态/从句);修复核心缺口:用户请求「三角形内角和」时不再退回无关算术,而是生成三角形内角和(180°)、直角三角形判定、勾股定理等主题相关题目 | +| AI 熔断器全局共享 | _ai_unavailable_until 从实例变量改为类变量,8 个路由模块的 AIService 实例共享一个熔断器;首次 AI 超时后,所有端点的后续调用立即走兜底(0.2s),不再各自独立等待 10s 超时(5 端点连续调用从 ~50s 降至 ~11s) | + +## 端到端验证覆盖(双账号真实链路) + +以下链路均经真实 HTTP 调用端到端验证通过: + +| 链路 | 验证结果 | +| --- | --- | +| 认证积分 | 注册201/登录/me初始130积分/签到+10/140 通过 | +| AI生成8端点 | 课件/动画/教案/练习/思维导图/命题/作文批改全200,内容质量已核验 | +| 社交链路 | A发资源→B收藏通知→B下载→B发帖→A评论→A点赞双向通知→全局搜索7维度 通过 | +| 收藏状态 | 跨账号is_favorited正确/收藏列表同步/取消重收准确 通过 | +| 课堂数据回收 | 创建投票→4学生公开提交→分析统计(选项50%/掌握度/教学建议)→Excel导出 通过 | +| 课件分享 | 生成token→公开访问→浏览数递增→我的分享列表→撤销后404 通过 | +| 旧品牌清除 | 源码0残留+所有生成类型产出0残留(课件原117处旧前缀全部清零) 通过 | +| 平台完整性审计 | 24视图无空壳(最小366行)、17路由+~125个前端API端点与后端对齐、3处TODO标记均为UI文案非未实现、找回密码/分享/通知/搜索等关键链路前后端完整;Playwright真实浏览器端到端验证:登录→首页(H1正确)→课件/动画/练习页加载→动画生成API 200→0控制台错误→无旧品牌残留 | +| AI 兜底质量增强 | 修复 `backend/services/ai_service.py` 中 `_exercise_kb` 重复定义导致的 SyntaxError(`""书名""` 双引号嵌套解析失败,删除冗余副本,保留已用《》/「」的正确版本);思维导图/练习/教案 fallback 升级为学科+主题感知题库(30+ 高频主题:朝代/丝绸之路/光合作用/细胞/元素周期表/酸碱盐/欧姆定律/重力/一元二次方程/二次函数/荷塘月色/古诗词/时态/气候),冷门主题回退通用题库无回归;时态关键词扩充(现在时/过去时/进行时/完成时)、朝代关键词扩充(唐/汉/宋/明/清/秦);HTTP + Playwright 端到端验证:一元二次方程→x²-5x+6=0、光合作用→叶绿体、欧姆定律→I=U/R、荷塘月色→朱自清、唐朝→朝代题库、0 控制台错误 | + + +| 练习标题去重 | 修复 `_fallback_exercise` 兜底标题把整段 prompt 当主题再拼「练习」导致「…练习练习」重复:改为正则去尾(练习题/练习/题目/试题/习题/题库/选择题/填空题/判断题/应用题/解答题/计算题/题 等)后再拼;已端到端验证 prompt「二次函数练习」→ 标题「二次函数练习」(原「二次函数练习练习」),3 题正常返回;全工作区旧品牌残留复核:源码+数据库+node_modules 字节级扫描 0 命中;16 个主视图浏览器遍历 0 控制台错误,登录/课件生成链路运行时健康 | + + +| 练习作答追踪(attempt) | 后端修复 POST `/{id}/attempt`(原忽略路径参数 exercise_id,改为显式赋值并校验练习存在);新增 GET `/{id}/attempts` 供教师查询作答记录;schema 新增 ExerciseAttemptOut、ExerciseAttemptCreate 去掉冗余 exercise_id;前端新增 submitExerciseAttempt/getExerciseAttempts API + Exercise.vue 自动提交(答完全部题目或重置时触发,仅对已保存练习生效)+ 预览时加载并展示「已有 N 人完成 · 平均正确率 M%」徽章;端到端验证:创建练习→提交2条作答(2/3, 3/3)→GET列表返回2条完整记录 | +| 全链路深度审计 | 品牌复核(源码+DB+node_modules 字节级 0 残留);87前端API路径全部有对应后端端点;16主视图浏览器遍历 0 控制台错误;资源CRUD/课堂回收/社交/AI对话/签到/Profile统计 各链路端到端 HTTP 验证通过;TypeScript 零编译错误 | + + +| 管理后台致命 Bug 修复 | admin.py 缺少 `from services.audit import log_action` 导入,导致 update_user / grant_user_credits / moderate_resource 三个端点全部 500(NameError)。修复后端到端验证:发放积分 200、启用/禁用用户 200、资源上下架 200,审计日志正确记录;全路由文件 AST 扫描确认无其他缺失导入 | +| 全功能端到端审计 | 七大生成流(课件/动画/练习/教案/思维导图/命题/作文) × 全生命周期(生成→保存→列表→详情→导出→改编→删除);课件分享链路(token→公开访问→列表→撤销→404);社交链路(发帖→评论→点赞→收藏→搜索5维度);AI对话(创建→发消息→历史加载→删除);课堂数据回收(创建→发布→4学生公开提交→分析统计→Excel导出);密码安全(改密/旧密码校验/新旧不同/强度策略);材料解析(文件上传→解析→入库);管理后台(看板15指标/用户CRUD/资源审核/审计日志50条) | + +## 待完善(持续打磨) + +| 能力 | 建议实现 | +| --- | --- | +| 模板库持续扩展 | 持续补充学科/题型覆盖(当前 36 动画 + 20 游戏),打磨生成与模板质量 | +| 安全增强 | 余项:病毒扫描集成;✅ JWT 密钥安全已落地(SECRET_KEY 启动校验:生产模式拒绝弱/默认/空密钥,开发模式生成临时密钥并警告;当前 .env 已配置 64 字符强密钥;旧默认密钥伪造的 token 端到端验证被拒);密码强度策略已覆盖注册/改密/重置,刷新令牌限流 30/min 已落地 | + +## 开发优先级 + +1. 材料解析、数据回收学生端、积分体系和管理后台已落地,持续打磨生成质量。 +2. ✅ 课件编辑器组件化(主题模板 + 模板插入)已落地。 +3. ✅ 动画/游戏模板库(36 + 20)已落地,覆盖数学/物理/化学/生物/地理/历史/语文/英语/通用多学科场景;并修复 3 个旧游戏模板的脚本错误。 +4. ✅ 生产安全(上传校验 + 审计日志 + 修改密码)已落地,持续扩展审计覆盖与安全增强。 diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..174a0f6 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,12 @@ +FROM node:20-alpine AS build +WORKDIR /app +COPY package*.json ./ +RUN npm install +COPY . . +RUN npm run build + +FROM nginx:alpine +COPY --from=build /app/dist /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/_audit.cjs b/frontend/_audit.cjs new file mode 100644 index 0000000..0fee104 --- /dev/null +++ b/frontend/_audit.cjs @@ -0,0 +1,76 @@ +const { chromium } = require("playwright-core"); + +(async () => { + const browser = await chromium.launch({ + executablePath: "C:/Program Files/Google/Chrome/Application/chrome.exe", + args: ["--disable-blink-features=AutomationControlled"], + }); + const page = await browser.newPage({ viewport: { width: 1440, height: 900 } }); + const errors = []; + page.on("console", msg => { + if (msg.type() === "error") errors.push(`CONSOLE: ${msg.text()}`); + }); + page.on("pageerror", err => errors.push(`PAGEERROR: ${err.message}`)); + + const BASE = "http://127.0.0.1:5175"; + + // Login first via API + const loginResp = await page.request.post("http://127.0.0.1:8010/api/auth/login", { + data: { phone: "13943441149", password: "Test1234" }, + headers: { "Content-Type": "application/json" }, + }); + const loginData = await loginResp.json(); + const token = loginData.access_token; + + // Inject token into localStorage + await page.goto(BASE + "/login", { waitUntil: "networkidle" }); + await page.evaluate(t => { + localStorage.setItem("token", t); + localStorage.setItem("access_token", t); + localStorage.setItem("zj_token", t); + }, token); + + const routes = [ + "/", "/courseware", "/courseware/create", "/animation", "/exercise", + "/lesson-plan", "/exam", "/essay-grade", "/mindmap", "/materials", + "/resources", "/community", "/classroom", "/assistant", "/profile", + "/admin", "/search", + ]; + + for (const route of routes) { + const routeErrors = []; + page.removeAllListeners("console"); + page.removeAllListeners("pageerror"); + page.on("console", msg => { if (msg.type() === "error") routeErrors.push(msg.text()); }); + page.on("pageerror", err => routeErrors.push(err.message)); + + try { + await page.goto(BASE + route, { waitUntil: "networkidle", timeout: 15000 }); + await page.waitForTimeout(1500); + + // Check for empty/blank pages + const bodyText = await page.evaluate(() => document.body.innerText.trim()); + const isEmpty = bodyText.length < 10; + const hasRouterView = await page.evaluate(() => { + const app = document.querySelector("#app"); + return app && app.children.length > 0; + }); + + // Filter expected errors (401/403 for admin views) + const realErrors = routeErrors.filter(e => + !e.includes("401") && !e.includes("403") && + !e.includes("Failed to fetch") && + !e.includes("NetworkError") + ); + + const status = realErrors.length === 0 && !isEmpty && hasRouterView ? "OK" : "CHECK"; + const errSummary = realErrors.length > 0 ? ` ERR:[${realErrors.slice(0,2).join(" | ").substring(0,100)}]` : ""; + const emptyFlag = isEmpty ? " BLANK" : ""; + console.log(`${status} ${route.padEnd(20)} ${bodyText.length}chars${emptyFlag}${errSummary}`); + } catch (e) { + console.log(`FAIL ${route.padEnd(20)} ${e.message.substring(0, 80)}`); + } + } + + await browser.close(); +})(); diff --git a/frontend/_audit.mjs b/frontend/_audit.mjs new file mode 100644 index 0000000..266ae86 --- /dev/null +++ b/frontend/_audit.mjs @@ -0,0 +1,58 @@ +import { chromium } from 'playwright-core'; +import fs from 'fs'; +import path from 'path'; + +const loginRes = await fetch('http://127.0.0.1:8010/api/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ phone: '13943441149', password: 'Test1234' }) +}); +const loginData = await loginRes.json(); +const accessToken = loginData.access_token; +const refreshToken = loginData.refresh_token; + +const browser = await chromium.launch({ + channel: 'chrome', headless: true, + executablePath: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe' +}); +const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } }); +await ctx.addInitScript(([at, rt]) => { + localStorage.setItem('access_token', at); + localStorage.setItem('refresh_token', rt); +}, [accessToken, refreshToken]); +const page = await ctx.newPage(); +const errors = []; +page.on('console', m => { if (m.type() === 'error') errors.push(m.text()); }); +page.on('pageerror', e => errors.push('PAGEERROR: ' + e.message)); + +const shotDir = path.resolve('audit_shots'); +if (!fs.existsSync(shotDir)) fs.mkdirSync(shotDir); + +const pages = [ + { name: 'home', path: '/home' }, + { name: 'courseware_list', path: '/courseware' }, + { name: 'courseware_create', path: '/courseware/create' }, + { name: 'animation', path: '/animation' }, + { name: 'exercise', path: '/exercise' }, + { name: 'exam', path: '/exam' }, + { name: 'lesson_plan', path: '/lesson-plan' }, + { name: 'mindmap', path: '/mindmap' }, + { name: 'essay_grade', path: '/essay-grade' }, + { name: 'materials', path: '/materials' }, + { name: 'profile', path: '/profile' }, + { name: 'classroom', path: '/classroom' }, + { name: 'assistant', path: '/assistant' }, + { name: 'admin', path: '/admin' }, +]; +for (const p of pages) { + await page.goto('http://127.0.0.1:5175' + p.path, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {}); + await page.waitForTimeout(1500); + await page.screenshot({ path: path.join(shotDir, p.name + '.png'), fullPage: false }); + const url = page.url(); + const redirected = url.includes('login'); + const h = await page.textContent('h1, h2').catch(() => ''); + console.log('[' + p.name + '] ' + (redirected ? 'REDIRECT_LOGIN' : url.split('/').slice(-2).join('/')) + ' h="' + (h||'').trim().substring(0,30) + '"'); +} +console.log('ERRORS:', errors.length); +errors.slice(0,10).forEach(e => console.log(' ', e.substring(0,140))); +await browser.close(); diff --git a/frontend/_audit2.cjs b/frontend/_audit2.cjs new file mode 100644 index 0000000..5862ffc --- /dev/null +++ b/frontend/_audit2.cjs @@ -0,0 +1,73 @@ +const { chromium } = require("playwright-core"); + +(async () => { + const browser = await chromium.launch({ + executablePath: "C:/Program Files/Google/Chrome/Application/chrome.exe", + args: ["--disable-blink-features=AutomationControlled"], + }); + const page = await browser.newPage({ viewport: { width: 1440, height: 900 } }); + const errors = []; + page.on("console", msg => { if (msg.type() === "error") errors.push(msg.text()); }); + page.on("pageerror", err => errors.push(err.message)); + + const BASE = "http://127.0.0.1:5175"; + const loginResp = await page.request.post("http://127.0.0.1:8010/api/auth/login", { + data: { phone: "13943441149", password: "Test1234" }, + }); + const token = (await loginResp.json()).access_token; + + await page.goto(BASE + "/login", { waitUntil: "networkidle" }); + await page.evaluate(t => { localStorage.setItem("token", t); localStorage.setItem("access_token", t); localStorage.setItem("zj_token", t); }, token); + + // === TEST 1: Animation template gallery === + console.log("=== ANIMATION TEMPLATES ==="); + await page.goto(BASE + "/animation", { waitUntil: "networkidle" }); + await page.waitForTimeout(2000); + const tplButtons = await page.$$(".tpl-btn"); + console.log("template buttons found:", tplButtons.length); + + // Click first template + if (tplButtons.length > 0) { + await tplButtons[0].click(); + await page.waitForTimeout(2000); + const previewIframe = await page.$(".tg-preview-wrap iframe, .preview-wrap iframe, iframe"); + if (previewIframe) { + console.log("preview iframe: FOUND"); + } else { + console.log("preview iframe: NOT FOUND"); + const allIframes = await page.$$("iframe"); + console.log("total iframes:", allIframes.length); + } + const cards = await page.$$(".tg-card, .result-card"); + console.log("result cards:", cards.length); + } + + // === TEST 2: Exercise template gallery === + console.log("\n=== EXERCISE TEMPLATES ==="); + await page.goto(BASE + "/exercise", { waitUntil: "networkidle" }); + await page.waitForTimeout(2000); + const exTplBtns = await page.$$(".tpl-btn"); + console.log("exercise template buttons:", exTplBtns.length); + + // === TEST 3: Courseware create page === + console.log("\n=== COURSEWARE CREATE ==="); + await page.goto(BASE + "/courseware/create", { waitUntil: "networkidle" }); + await page.waitForTimeout(1500); + const textareas = await page.$$("textarea, input[type=text], .el-input__inner"); + console.log("input fields:", textareas.length); + const genButtons = await page.$$("button:has-text('生成'), button:has-text('创建'), .gen-btn"); + console.log("generate buttons:", genButtons.length); + + // === TEST 4: Classroom === + console.log("\n=== CLASSROOM ==="); + await page.goto(BASE + "/classroom", { waitUntil: "networkidle" }); + await page.waitForTimeout(1500); + const classroomText = await page.evaluate(() => document.body.innerText.substring(0, 500)); + console.log("classroom has content:", classroomText.length > 50); + + const realErrors = errors.filter(e => !e.includes("401") && !e.includes("403") && !e.includes("Failed to fetch")); + console.log("\n=== TOTAL ERRORS (filtered):", realErrors.length, "==="); + realErrors.slice(0, 5).forEach(e => console.log(" ", e.substring(0, 120))); + + await browser.close(); +})(); diff --git a/frontend/_audit3.cjs b/frontend/_audit3.cjs new file mode 100644 index 0000000..52db8a9 --- /dev/null +++ b/frontend/_audit3.cjs @@ -0,0 +1,74 @@ +const { chromium } = require("playwright-core"); + +(async () => { + const browser = await chromium.launch({ + executablePath: "C:/Program Files/Google/Chrome/Application/chrome.exe", + args: ["--disable-blink-features=AutomationControlled"], + }); + const page = await browser.newPage({ viewport: { width: 1440, height: 900 } }); + const errors = []; + page.on("console", msg => { if (msg.type() === "error") errors.push(msg.text()); }); + page.on("pageerror", err => errors.push(err.message)); + + const BASE = "http://127.0.0.1:5175"; + const loginResp = await page.request.post("http://127.0.0.1:8010/api/auth/login", { + data: { phone: "13943441149", password: "Test1234" }, + }); + const token = (await loginResp.json()).access_token; + await page.goto(BASE + "/login", { waitUntil: "networkidle" }); + await page.evaluate(t => { localStorage.setItem("token", t); localStorage.setItem("access_token", t); localStorage.setItem("zj_token", t); }, token); + + // Get existing courseware list + const cwResp = await page.request.get("http://127.0.0.1:8010/api/coursewares", { + headers: { Authorization: `Bearer ${token}` }, + }); + const coursewares = await cwResp.json(); + console.log("existing coursewares:", coursewares.length); + if (coursewares.length > 0) { + console.log("first courseware id:", coursewares[0].id, "title:", coursewares[0].title?.substring(0, 30)); + } + + // Navigate to courseware list and check if preview works + console.log("\n=== COURSEWARE LIST ==="); + await page.goto(BASE + "/courseware", { waitUntil: "networkidle" }); + await page.waitForTimeout(2000); + + // Count cards + const cards = await page.$$(".courseware-card, .cw-card, .el-card"); + console.log("courseware cards on page:", cards.length); + + // Check for action buttons + const previewBtns = await page.$$("button:has-text('预览'), button:has-text('查看'), a:has-text('预览')"); + console.log("preview buttons:", previewBtns.length); + + // Navigate to first courseware preview if exists + if (coursewares.length > 0) { + console.log("\n=== COURSEWARE PREVIEW ==="); + await page.goto(`${BASE}/courseware/${coursewares[0].id}/preview`, { waitUntil: "networkidle" }); + await page.waitForTimeout(3000); + + // Check if slides rendered + const iframes = await page.$$("iframe"); + console.log("iframes:", iframes.length); + + const slideContent = await page.evaluate(() => { + const slide = document.querySelector(".slide-container, .preview-slide, .cw-slide"); + return slide ? "slide found" : "no slide element"; + }); + console.log("slide element:", slideContent); + + // Check page navigation + const navBtns = await page.$$(".page-nav, .nav-btn, button:has-text('下一页'), button:has-text('上一页')"); + console.log("nav buttons:", navBtns.length); + + // Take screenshot + await page.screenshot({ path: "_cw_preview.png" }); + console.log("screenshot saved"); + } + + const realErrors = errors.filter(e => !e.includes("401") && !e.includes("403") && !e.includes("Failed to fetch")); + console.log("\nerrors (filtered):", realErrors.length); + realErrors.slice(0, 3).forEach(e => console.log(" ", e.substring(0,120))); + + await browser.close(); +})(); diff --git a/frontend/_audit4.cjs b/frontend/_audit4.cjs new file mode 100644 index 0000000..972474a --- /dev/null +++ b/frontend/_audit4.cjs @@ -0,0 +1,66 @@ +const { chromium } = require("playwright-core"); +const fs = require("fs"); + +(async () => { + const cwId = fs.readFileSync("D:/AI/jiaoyu/_cw_id.txt","utf8").trim(); + console.log("testing courseware preview for id:", cwId); + + const browser = await chromium.launch({ + executablePath: "C:/Program Files/Google/Chrome/Application/chrome.exe", + args: ["--disable-blink-features=AutomationControlled"], + }); + const page = await browser.newPage({ viewport: { width: 1440, height: 900 } }); + const errors = []; + page.on("console", msg => { if (msg.type() === "error") errors.push(msg.text()); }); + page.on("pageerror", err => errors.push(err.message)); + + const BASE = "http://127.0.0.1:5175"; + const loginResp = await page.request.post("http://127.0.0.1:8010/api/auth/login", { + data: { phone: "13943441149", password: "Test1234" }, + }); + const token = (await loginResp.json()).access_token; + await page.goto(BASE + "/login", { waitUntil: "networkidle" }); + await page.evaluate(t => { localStorage.setItem("token", t); localStorage.setItem("access_token", t); localStorage.setItem("zj_token", t); }, token); + + // Go to preview + await page.goto(`${BASE}/courseware/${cwId}/preview`, { waitUntil: "networkidle", timeout: 20000 }); + await page.waitForTimeout(3000); + + // Check rendering + const iframes = await page.$$("iframe"); + console.log("iframes:", iframes.length); + + const bodyText = await page.evaluate(() => document.body.innerText.substring(0, 200)); + console.log("body text preview:", bodyText.substring(0, 120)); + + // Check for slide/navigation elements + const allDivs = await page.$$("#app div"); + console.log("total divs:", allDivs.length); + + // Check for specific preview elements + const slideEls = await page.$$("[class*='slide'], [class*='preview'], [class*='page-nav'], [class*='cw-']"); + console.log("slide/preview elements:", slideEls.length); + + // Look for iframe content + for (let i = 0; i < Math.min(iframes.length, 3); i++) { + try { + const frame = iframes[i].contentFrame(); + if (frame) { + const content = await frame.evaluate(() => document.body ? document.body.innerText.substring(0, 60) : "empty"); + console.log(`iframe[${i}] content:`, content); + } + } catch(e) { + console.log(`iframe[${i}] error:`, e.message.substring(0, 60)); + } + } + + // Screenshot + await page.screenshot({ path: "_cw_preview.png" }); + console.log("screenshot saved"); + + const realErrors = errors.filter(e => !e.includes("401") && !e.includes("403") && !e.includes("Failed to fetch")); + console.log("\nerrors (filtered):", realErrors.length); + realErrors.slice(0, 3).forEach(e => console.log(" ", e.substring(0,120))); + + await browser.close(); +})(); diff --git a/frontend/_audit5.cjs b/frontend/_audit5.cjs new file mode 100644 index 0000000..6ba6f6a --- /dev/null +++ b/frontend/_audit5.cjs @@ -0,0 +1,67 @@ +const { chromium } = require("playwright-core"); +const fs = require("fs"); + +(async () => { + const cwId = fs.readFileSync("D:/AI/jiaoyu/_cw_id.txt","utf8").trim(); + console.log("testing courseware preview at /preview/" + cwId); + + const browser = await chromium.launch({ + executablePath: "C:/Program Files/Google/Chrome/Application/chrome.exe", + args: ["--disable-blink-features=AutomationControlled"], + }); + const page = await browser.newPage({ viewport: { width: 1440, height: 900 } }); + const errors = []; + page.on("console", msg => { if (msg.type() === "error") errors.push(msg.text()); }); + page.on("pageerror", err => errors.push(err.message)); + + const BASE = "http://127.0.0.1:5175"; + const loginResp = await page.request.post("http://127.0.0.1:8010/api/auth/login", { + data: { phone: "13943441149", password: "Test1234" }, + }); + const token = (await loginResp.json()).access_token; + await page.goto(BASE + "/login", { waitUntil: "networkidle" }); + await page.evaluate(t => { localStorage.setItem("token", t); localStorage.setItem("access_token", t); localStorage.setItem("zj_token", t); }, token); + + // Correct URL: /preview/:id + await page.goto(`${BASE}/preview/${cwId}`, { waitUntil: "networkidle", timeout: 20000 }); + await page.waitForTimeout(4000); + + const iframes = await page.$$("iframe"); + console.log("iframes:", iframes.length); + + // Check iframe content + for (let i = 0; i < Math.min(iframes.length, 2); i++) { + try { + const frame = iframes[i].contentFrame(); + if (frame) { + const html = await frame.evaluate(() => document.documentElement.outerHTML.substring(0, 200)); + console.log(`iframe[${i}] html:`, html.substring(0, 150)); + } + } catch(e) { + console.log(`iframe[${i}] error:`, e.message.substring(0, 80)); + } + } + + // Check page navigation + const navBtns = await page.$$("button"); + const navTexts = []; + for (const btn of navBtns) { + const text = await btn.textContent(); + if (text.trim()) navTexts.push(text.trim().substring(0, 20)); + } + console.log("buttons:", navTexts.slice(0, 10)); + + // Page counter + const bodyText = await page.evaluate(() => document.body.innerText); + const pageMatch = bodyText.match(/(\d+)\s*\/\s*(\d+)/); + if (pageMatch) console.log("page counter:", pageMatch[0]); + + await page.screenshot({ path: "_cw_preview2.png" }); + console.log("screenshot saved"); + + const realErrors = errors.filter(e => !e.includes("401") && !e.includes("403") && !e.includes("Failed to fetch")); + console.log("\nerrors (filtered):", realErrors.length); + realErrors.slice(0, 3).forEach(e => console.log(" ", e.substring(0,120))); + + await browser.close(); +})(); diff --git a/frontend/_audit6.cjs b/frontend/_audit6.cjs new file mode 100644 index 0000000..9e93f31 --- /dev/null +++ b/frontend/_audit6.cjs @@ -0,0 +1,55 @@ +const { chromium } = require("playwright-core"); +const fs = require("fs"); + +(async () => { + const cwId = fs.readFileSync("D:/AI/jiaoyu/_cw_id.txt","utf8").trim(); + const browser = await chromium.launch({ + executablePath: "C:/Program Files/Google/Chrome/Application/chrome.exe", + args: ["--disable-blink-features=AutomationControlled"], + }); + const page = await browser.newPage({ viewport: { width: 1440, height: 900 } }); + const errors = []; + page.on("console", msg => { if (msg.type() === "error") errors.push(msg.text()); }); + page.on("pageerror", err => errors.push(err.message)); + + const BASE = "http://127.0.0.1:5175"; + const loginResp = await page.request.post("http://127.0.0.1:8010/api/auth/login", { + data: { phone: "13943441149", password: "Test1234" }, + }); + const token = (await loginResp.json()).access_token; + await page.goto(BASE + "/login", { waitUntil: "networkidle" }); + await page.evaluate(t => { localStorage.setItem("token", t); localStorage.setItem("access_token", t); localStorage.setItem("zj_token", t); }, token); + + await page.goto(`${BASE}/preview/${cwId}`, { waitUntil: "networkidle", timeout: 20000 }); + await page.waitForTimeout(4000); + + const iframes = await page.$$("iframe"); + console.log("iframes:", iframes.length); + + // Check main slide iframe content via srcdoc + const slideIframe = await page.$(".slide-raw-iframe"); + if (slideIframe) { + const srcdoc = await slideIframe.getAttribute("srcdoc"); + console.log("slide srcdoc length:", srcdoc ? srcdoc.length : 0); + if (srcdoc) console.log("srcdoc has content:", srcdoc.length > 100); + } else { + console.log("no .slide-raw-iframe found"); + } + + // Check page counter + const pageCounter = await page.$eval(".bottom-page-info, .ov-page", el => el.textContent).catch(() => "none"); + console.log("page counter:", pageCounter); + + // Check thumbnails + const thumbs = await page.$$(".thumb-raw-iframe, .thumb-scaler"); + console.log("thumbnails:", thumbs.length); + + await page.screenshot({ path: "_cw_preview3.png" }); + console.log("screenshot saved"); + + const realErrors = errors.filter(e => !e.includes("401") && !e.includes("403") && !e.includes("Failed to fetch")); + console.log("errors (filtered):", realErrors.length); + realErrors.slice(0, 3).forEach(e => console.log(" ", e.substring(0,120))); + + await browser.close(); +})(); diff --git a/frontend/_br_test.cjs b/frontend/_br_test.cjs new file mode 100644 index 0000000..6d8c0c1 --- /dev/null +++ b/frontend/_br_test.cjs @@ -0,0 +1,35 @@ +const { chromium } = require('playwright-core'); +(async () => { + const browser = await chromium.launch({ channel: 'chrome', headless: true, executablePath: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe' }); + const page = await browser.newPage({ viewport: { width: 1400, height: 900 } }); + const errors = []; + page.on('console', m => { if (m.type() === 'error') errors.push(m.text()); }); + page.on('pageerror', e => errors.push('PAGEERR: ' + e.message)); + // 登录 + await page.goto('http://127.0.0.1:5173/login', { waitUntil: 'networkidle' }); + await page.waitForTimeout(800); + await page.fill('input[placeholder="请输入手机号"]', '13943441149'); + await page.fill('input[type="password"]', 'Test1234'); + await page.click('button.gradient-btn'); + await page.waitForTimeout(3000); + const url1 = page.url(); + console.log('登录后URL:', url1); + // 思维导图 + await page.goto('http://127.0.0.1:5173/mindmap', { waitUntil: 'networkidle' }); + await page.waitForTimeout(1000); + await page.fill('input[placeholder*="光合作用"]', '中国历史朝代'); + await page.waitForTimeout(400); + await page.click('button:has-text("生成导图")'); + console.log('已点生成导图'); + await page.waitForTimeout(9000); + await page.screenshot({ path: 'D:\\AI\\jiaoyu\\_shot_mindmap.png', fullPage: true }); + const bodyText = await page.evaluate(() => document.body.innerText); + console.log('含先秦文明:', bodyText.includes('先秦文明')); + console.log('含秦汉大一统:', bodyText.includes('秦汉大一统')); + console.log('含隋唐盛世:', bodyText.includes('隋唐盛世')); + console.log('含概念定义(旧通用):', bodyText.includes('概念定义')); + console.log('含知识结构(旧通用):', bodyText.includes('知识结构')); + console.log('错误数:', errors.length); + if (errors.length) console.log('错误:', errors.slice(0,5).join(' | ')); + await browser.close(); +})(); diff --git a/frontend/_dbg.cjs b/frontend/_dbg.cjs new file mode 100644 index 0000000..872378c --- /dev/null +++ b/frontend/_dbg.cjs @@ -0,0 +1,22 @@ +const { chromium } = require('playwright-core'); +(async () => { + const CHROME = String.raw`C:\Program Files\Google\Chrome\Application\chrome.exe`; + const browser = await chromium.launch({ executablePath: CHROME, headless: true }); + const page = await browser.newPage(); + const errs = []; + page.on('console', m => errs.push(m.type()+': '+m.text())); + page.on('pageerror', e => errs.push('PAGEERR: '+e.message)); + const base = 'http://127.0.0.1:8010'; + const r = await fetch(base + '/api/auth/login', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({phone:'13943441149',password:'Test1234'}) }); + const tok = (await r.json()).data?.access_token; + await page.goto('http://127.0.0.1:5175/login'); + await page.evaluate(t => { localStorage.setItem('access_token', t); localStorage.setItem('refresh_token','r'); }, tok); + await page.goto('http://127.0.0.1:5175/animation'); + await page.waitForTimeout(3000); + console.log('URL:', page.url()); + console.log('has tpl-btn:', await page.locator('.tpl-btn').count()); + console.log('body text (first 200):', (await page.locator('body').innerText()).slice(0,200)); + console.log('errors:', errs.slice(0,8)); + await page.screenshot({ path: '../audit_shots/_debug_page.png' }); + await browser.close(); +})(); diff --git a/frontend/_debug_prob.cjs b/frontend/_debug_prob.cjs new file mode 100644 index 0000000..8aecd3c --- /dev/null +++ b/frontend/_debug_prob.cjs @@ -0,0 +1,29 @@ +const { chromium } = require('playwright-core'); +(async () => { + const CHROME = String.raw`C:\Program Files\Google\Chrome\Application\chrome.exe`; + const browser = await chromium.launch({ executablePath: CHROME, headless: true }); + const page = await browser.newPage(); + page.on('pageerror', e => console.log('PAGEERROR:', e.message, '\nSTACK:', e.stack)); + page.on('console', m => { if (m.type()==='error') console.log('CONSOLE_ERR:', m.text()); }); + // Load the module and render probability template + await page.goto('http://127.0.0.1:5175/animation'); + // we need the template render output; use the gallery instead but isolate + await page.waitForTimeout(1500); + // inject token + const base = 'http://127.0.0.1:8010'; + const r = await fetch(base + '/api/auth/login', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({phone:'13943441149',password:'Test1234'}) }); + const tok = (await r.json()).data?.access_token; + await page.evaluate(t => { localStorage.setItem('access_token', t); localStorage.setItem('refresh_token','r'); }, tok); + await page.goto('http://127.0.0.1:5175/animation'); + await page.waitForTimeout(2000); + // open gallery and jump directly to probability card + await page.locator('.tpl-btn').first().click(); + await page.waitForTimeout(1200); + // search for probability + await page.locator('.tg-search input').fill('概率'); + await page.waitForTimeout(600); + await page.locator('.tg-card').first().click(); + await page.waitForTimeout(2000); + console.log('done'); + await browser.close(); +})(); diff --git a/frontend/_diag.mjs b/frontend/_diag.mjs new file mode 100644 index 0000000..272dac9 --- /dev/null +++ b/frontend/_diag.mjs @@ -0,0 +1,46 @@ +import { chromium } from 'playwright-core'; +import fs from 'fs'; +import path from 'path'; + +const loginRes = await fetch('http://127.0.0.1:8010/api/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ phone: '13943441149', password: 'Test1234' }) +}); +const loginData = await loginRes.json(); +const browser = await chromium.launch({ + channel: 'chrome', headless: true, + executablePath: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe' +}); +const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } }); +await ctx.addInitScript(([at, rt]) => { + localStorage.setItem('access_token', at); + localStorage.setItem('refresh_token', rt); +}, [loginData.access_token, loginData.refresh_token]); +const page = await ctx.newPage(); +const errors = []; +page.on('console', m => { if (m.type() === 'error') errors.push(m.text()); }); +page.on('pageerror', e => errors.push('PAGEERROR: ' + e.message)); + +// Go to home and wait longer +await page.goto('http://127.0.0.1:5175/home', { waitUntil: 'networkidle', timeout: 20000 }).catch(e => console.log('goto err', e.message)); +await page.waitForTimeout(4000); + +// Diagnose +const info = await page.evaluate(() => { + const body = document.body; + return { + bodyTextLen: body.innerText.length, + bodyTextHead: body.innerText.substring(0, 300), + url: location.href, + hasH1: !!document.querySelector('h1'), + h1Text: document.querySelector('h1')?.textContent || '(none)', + mainContent: document.querySelector('.home-page, .home-container, .hero, main, #app')?.innerHTML?.length || 0, + childCount: document.querySelector('#app')?.children?.length || 0, + }; +}); +console.log(JSON.stringify(info, null, 2)); +await page.screenshot({ path: path.resolve('audit_shots/home2.png') }); +console.log('ERRORS:', errors.length); +errors.slice(0,8).forEach(e => console.log(' ', e.substring(0,160))); +await browser.close(); diff --git a/frontend/_diag2.mjs b/frontend/_diag2.mjs new file mode 100644 index 0000000..fa8c537 --- /dev/null +++ b/frontend/_diag2.mjs @@ -0,0 +1,15 @@ +import { chromium } from 'playwright-core'; +import fs from 'fs'; +import path from 'path'; +const loginRes = await fetch('http://127.0.0.1:8010/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ phone: '13943441149', password: 'Test1234' }) }); +const loginData = await loginRes.json(); +const browser = await chromium.launch({ channel: 'chrome', headless: true, executablePath: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe' }); +const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } }); +await ctx.addInitScript(([at, rt]) => { localStorage.setItem('access_token', at); localStorage.setItem('refresh_token', rt); }, [loginData.access_token, loginData.refresh_token]); +const page = await ctx.newPage(); +await page.goto('http://127.0.0.1:5175/', { waitUntil: 'networkidle', timeout: 20000 }).catch(e => console.log('goto err', e.message)); +await page.waitForTimeout(4000); +const info = await page.evaluate(() => ({ bodyTextLen: document.body.innerText.length, bodyTextHead: document.body.innerText.substring(0, 200), url: location.href, h1Text: document.querySelector('h1')?.textContent || '(none)', childCount: document.querySelector('#app')?.children?.length || 0 })); +console.log(JSON.stringify(info, null, 2)); +await page.screenshot({ path: path.resolve('audit_shots/home_root.png') }); +await browser.close(); diff --git a/frontend/_final_tpl.cjs b/frontend/_final_tpl.cjs new file mode 100644 index 0000000..a4a7340 --- /dev/null +++ b/frontend/_final_tpl.cjs @@ -0,0 +1,43 @@ +const { chromium } = require('playwright-core'); +(async () => { + const CHROME = String.raw`C:\Program Files\Google\Chrome\Application\chrome.exe`; + const browser = await chromium.launch({ executablePath: CHROME, headless: true }); + const page = await browser.newPage({ viewport: { width: 1366, height: 850 } }); + const errors = [], warnings = []; + page.on('pageerror', e => errors.push(e.message)); + page.on('console', m => { if (m.type()==='error') errors.push(m.text()); if (m.type()==='warning' && m.text().includes('resolve component')) warnings.push(m.text()); }); + const base = 'http://127.0.0.1:8010'; + const r = await fetch(base + '/api/auth/login', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({phone:'13943441149',password:'Test1234'}) }); + const j = await r.json(); + const tok = j.access_token; + await page.goto('http://127.0.0.1:5175/login'); + await page.evaluate(t => { localStorage.setItem('access_token', t); localStorage.setItem('refresh_token','r'); }, tok); + await page.goto('http://127.0.0.1:5175/animation'); + await page.waitForTimeout(2500); + await page.locator('.tpl-btn').first().click(); + await page.waitForTimeout(1500); + const cards = await page.locator('.tg-card').count(); + const ours = ['勾股定理','单位圆','排序算法','DNA','牛顿第二','板块构造']; + const results = []; + for (let i = 0; i < cards; i++) { + await page.locator('.tg-card').nth(i).click(); + await page.waitForTimeout(800); + const name = (await page.locator('.tg-main-head h3').textContent().catch(()=> '') || '').trim(); + if (ours.some(k => name.includes(k))) { + const frame = page.frameLocator('.tg-preview-wrap iframe').first(); + let ok = false, iconOk = false; + try { ok = (await frame.locator('body').innerHTML({ timeout: 3000 }).catch(()=> '')).length > 200; } catch(e) {} + // check the card icon rendered (svg present) + try { iconOk = await page.locator('.tg-card').nth(i).locator('svg').count() > 0; } catch(e) {} + results.push({ name, ok, iconOk }); + } + } + await page.screenshot({ path: '../audit_shots/_tpl_final2.png' }); + console.log('TOTAL_CARDS:', cards); + console.log('CONSOLE_ERRORS:', errors.length, 'ICON_WARNINGS:', warnings.length); + if (errors.length) console.log('ERROR_SAMPLES:', errors.slice(0,5)); + if (warnings.length) console.log('WARN_SAMPLES:', warnings.slice(0,3)); + console.log('NEW_TEMPLATES:', JSON.stringify(results)); + console.log('ALL_NEW_OK:', results.length === 6 && results.every(r => r.ok && r.iconOk)); + await browser.close(); +})(); diff --git a/frontend/_find_bad_games.cjs b/frontend/_find_bad_games.cjs new file mode 100644 index 0000000..476b6a3 --- /dev/null +++ b/frontend/_find_bad_games.cjs @@ -0,0 +1,30 @@ +const { chromium } = require('playwright-core'); +(async () => { + const CHROME = String.raw`C:\Program Files\Google\Chrome\Application\chrome.exe`; + const browser = await chromium.launch({ executablePath: CHROME, headless: true }); + const page = await browser.newPage({ viewport: { width: 1366, height: 850 } }); + const base = 'http://127.0.0.1:8010'; + const r = await fetch(base + '/api/auth/login', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({phone:'13943441149',password:'Test1234'}) }); + const tok = (await r.json()).access_token; + await page.goto('http://127.0.0.1:5175/login'); + await page.evaluate(t => { localStorage.setItem('access_token', t); localStorage.setItem('refresh_token','r'); }, tok); + await page.goto('http://127.0.0.1:5175/exercise'); + await page.waitForTimeout(2500); + await page.locator('.tpl-btn').first().click(); + await page.waitForTimeout(1500); + const cards = await page.locator('.tg-card').count(); + const ours = ['知识分类','序列记忆','成语接龙','定时炸弹','入门数独']; + const bad = []; + for (let i = 0; i < cards; i++) { + const errs = []; + const handler = e => errs.push(e.message); + page.on('pageerror', handler); + await page.locator('.tg-card').nth(i).click(); + await page.waitForTimeout(1000); + const name = (await page.locator('.tg-main-head h3').textContent().catch(()=> '') || '').trim(); + page.off('pageerror', handler); + if (errs.length) bad.push({ i, name, errs: errs.slice(0,2) }); + } + console.log('BAD_GAMES:', JSON.stringify(bad, null, 2)); + await browser.close(); +})(); diff --git a/frontend/_find_bad_tpl.cjs b/frontend/_find_bad_tpl.cjs new file mode 100644 index 0000000..0e30dd2 --- /dev/null +++ b/frontend/_find_bad_tpl.cjs @@ -0,0 +1,32 @@ +const { chromium } = require('playwright-core'); +const fs = require('fs'); +(async () => { + const CHROME = String.raw`C:\Program Files\Google\Chrome\Application\chrome.exe`; + const browser = await chromium.launch({ executablePath: CHROME, headless: true }); + const page = await browser.newPage({ viewport: { width: 1366, height: 850 } }); + const base = 'http://127.0.0.1:8010'; + const loginRes = await fetch(base + '/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ phone: '13943441149', password: 'Test1234' }) }); + const loginData = await loginRes.json(); + const token = loginData.data?.access_token || loginData.access_token; + await page.goto('http://127.0.0.1:5175/login'); + await page.evaluate(t => { localStorage.setItem('access_token', t); localStorage.setItem('refresh_token', 'r'); }, token); + await page.goto('http://127.0.0.1:5175/animation'); + await page.waitForTimeout(1800); + await page.locator('.tpl-btn').first().click(); + await page.waitForTimeout(1200); + const cards = await page.locator('.tg-card').count(); + const bad = []; + for (let i = 0; i < cards; i++) { + let beforeErr = 0; + const errs = []; + const handler = e => errs.push(e.message); + page.on('pageerror', handler); + await page.locator('.tg-card').nth(i).click(); + await page.waitForTimeout(700); + const name = (await page.locator('.tg-main-head h3').textContent().catch(()=> '') || '').trim(); + page.off('pageerror', handler); + if (errs.length) bad.push({ i, name, errs: errs.slice(0,2) }); + } + console.log('BAD_TEMPLATES:', JSON.stringify(bad, null, 2)); + await browser.close(); +})(); diff --git a/frontend/_iso_prob.cjs b/frontend/_iso_prob.cjs new file mode 100644 index 0000000..b2c7651 --- /dev/null +++ b/frontend/_iso_prob.cjs @@ -0,0 +1,36 @@ +const { chromium } = require('playwright-core'); +const fs = require('fs'); +(async () => { + const CHROME = String.raw`C:\Program Files\Google\Chrome\Application\chrome.exe`; + const src = fs.readFileSync('src/utils/animationTemplates.ts','utf8'); + const start = src.indexOf('const probability'); + const end = src.indexOf('// ===== 21', start); + const block = src.slice(start, end); + const m = block.match(/return SHELL\('概率模拟',\s*'',\s*`([\s\S]*?)`\s*\);\s*\}/); + if (!m) { console.log('no match'); process.exit(1); } + let script = m[1]; + script = script.split('${typ}').join('coin'); + script = script.split('${per}').join('20'); + const html = '
    '; + fs.writeFileSync('../audit_shots/_prob_isolated.html', html); + const browser = await chromium.launch({ executablePath: CHROME, headless: true }); + const page = await browser.newPage(); + page.on('pageerror', e => console.log('PAGEERROR:', e.message)); + page.on('console', msg => { if(msg.type()==='error') console.log('CONSOLE:', msg.text()); }); + await page.goto('file:///D:/AI/jiaoyu/audit_shots/_prob_isolated.html'); + await page.waitForTimeout(1500); + await page.screenshot({ path: '../audit_shots/_prob_isolated.png' }); + console.log('isolated test done'); + await browser.close(); +})(); diff --git a/frontend/_race_test.cjs b/frontend/_race_test.cjs new file mode 100644 index 0000000..cd646f5 --- /dev/null +++ b/frontend/_race_test.cjs @@ -0,0 +1,43 @@ +const { chromium } = require('playwright-core'); +(async () => { + const CHROME = String.raw`C:\Program Files\Google\Chrome\Application\chrome.exe`; + const browser = await chromium.launch({ executablePath: CHROME, headless: true }); + const page = await browser.newPage({ viewport: { width: 1366, height: 850 } }); + const errors = []; + page.on('pageerror', e => errors.push(e.message)); + page.on('console', m => { if (m.type()==='error') errors.push(m.text()); }); + const base = 'http://127.0.0.1:8010'; + const r = await fetch(base + '/api/auth/login', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({phone:'13943441149',password:'Test1234'}) }); + const tok = (await r.json()).access_token; + await page.goto('http://127.0.0.1:5175/login'); + await page.evaluate(t => { localStorage.setItem('access_token', t); localStorage.setItem('refresh_token','r'); }, tok); + await page.goto('http://127.0.0.1:5175/animation'); + await page.waitForTimeout(2500); + await page.locator('.tpl-btn').first().click(); + await page.waitForTimeout(1500); + // Click a setInterval template (solar-system), wait, then click probability + await page.locator('.tg-search input').fill('行星'); + await page.waitForTimeout(800); + await page.locator('.tg-card').first().click(); + await page.waitForTimeout(3000); // let its interval run + errors.length = 0; // reset + await page.locator('.tg-search input').fill('概率'); + await page.waitForTimeout(800); + await page.locator('.tg-card').first().click(); + await page.waitForTimeout(2500); + console.log('ERRORS_AFTER_SWITCH:', errors.length); + if (errors.length) console.log('ERRORS:', errors.slice(0,5)); + // also test: DNA (mine, uses setInterval) then probability + errors.length = 0; + await page.locator('.tg-search input').fill('DNA'); + await page.waitForTimeout(800); + await page.locator('.tg-card').first().click(); + await page.waitForTimeout(3000); + await page.locator('.tg-search input').fill('概率'); + await page.waitForTimeout(800); + await page.locator('.tg-card').first().click(); + await page.waitForTimeout(2500); + console.log('ERRORS_AFTER_DNA_TO_PROB:', errors.length); + if (errors.length) console.log('ERRORS:', errors.slice(0,5)); + await browser.close(); +})(); diff --git a/frontend/_screens.cjs b/frontend/_screens.cjs new file mode 100644 index 0000000..fc88708 --- /dev/null +++ b/frontend/_screens.cjs @@ -0,0 +1,67 @@ +const { chromium } = require('playwright-core'); +const fs = require('fs'); +const path = require('path'); + +const SHOTS = 'D:/AI/jiaoyu/_shots'; +if (!fs.existsSync(SHOTS)) fs.mkdirSync(SHOTS, { recursive: true }); + +(async () => { + const browser = await chromium.launch({ channel: 'chrome', headless: true, executablePath: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe' }); + const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } }); + const page = await ctx.newPage(); + const allErrors = []; + page.on('pageerror', e => allErrors.push('PAGE: ' + e.message)); + page.on('console', m => { if (m.type() === 'error') { const t = m.text(); if (!t.includes('favicon') && !t.includes('404')) allErrors.push('CON: ' + t.slice(0,150)); } }); + + // Login + const loginResp = await page.request.post('http://localhost:8000/api/auth/login', { + data: { phone: '13943441149', password: 'Test1234' } + }); + const { access_token } = await loginResp.json(); + + const pages = [ + { name: 'home', url: 'http://localhost:5174/', wait: 2500 }, + { name: 'courseware-create', url: 'http://localhost:5174/courseware/create', wait: 2000 }, + { name: 'animation', url: 'http://localhost:5174/animation', wait: 2000 }, + { name: 'exercise', url: 'http://localhost:5174/exercise', wait: 2000 }, + { name: 'lesson-plan', url: 'http://localhost:5174/lesson-plan', wait: 2000 }, + { name: 'exam', url: 'http://localhost:5174/exam', wait: 2000 }, + { name: 'essay-grade', url: 'http://localhost:5174/essay-grade', wait: 2000 }, + { name: 'mindmap', url: 'http://localhost:5174/mindmap', wait: 2000 }, + { name: 'resources', url: 'http://localhost:5174/resources', wait: 2000 }, + { name: 'community', url: 'http://localhost:5174/community', wait: 2000 }, + { name: 'classroom', url: 'http://localhost:5174/classroom', wait: 2000 }, + { name: 'materials', url: 'http://localhost:5174/materials', wait: 2000 }, + { name: 'profile', url: 'http://localhost:5174/profile', wait: 2000 }, + { name: 'search', url: 'http://localhost:5174/search?q=数学', wait: 2000 }, + { name: 'assistant', url: 'http://localhost:5174/assistant', wait: 2000 }, + ]; + + const results = []; + for (const p of pages) { + const errorsBefore = allErrors.length; + await page.goto(p.url, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {}); + // Inject token on first navigation + if (p.name === 'home') { + await page.evaluate((t) => { localStorage.setItem('access_token', t); localStorage.setItem('refresh_token', t); }, access_token); + await page.reload({ waitUntil: 'networkidle' }); + } + await page.waitForTimeout(p.wait); + await page.screenshot({ path: path.join(SHOTS, p.name + '.png'), fullPage: false }); + const bodyLen = await page.evaluate(() => document.body.innerText.length); + const h1 = await page.locator('h1, h2, .page-title, .tpl-title').first().textContent().catch(() => ''); + const newErrors = allErrors.slice(errorsBefore); + results.push({ name: p.name, bodyLen, title: h1.trim().slice(0,40), errors: newErrors.length }); + console.log('[' + p.name + '] bodyLen=' + bodyLen + ' title="' + h1.trim().slice(0,35) + '" errors=' + newErrors.length); + if (newErrors.length) newErrors.slice(0,2).forEach(e => console.log(' ' + e)); + } + + console.log('\n=== Summary ==='); + console.log('Total pages:', results.length); + console.log('Total errors:', allErrors.length); + const small = results.filter(r => r.bodyLen < 200); + console.log('Pages with thin content (<200 chars):', small.length); + small.forEach(r => console.log(' ' + r.name + ': ' + r.bodyLen + ' chars')); + + await browser.close(); +})().catch(e => { console.error('FATAL', e.message); process.exit(1); }); \ No newline at end of file diff --git a/frontend/_smoke.mjs b/frontend/_smoke.mjs new file mode 100644 index 0000000..bc2a6fc --- /dev/null +++ b/frontend/_smoke.mjs @@ -0,0 +1,34 @@ +import { chromium } from 'playwright-core'; +const browser = await chromium.launch({ + channel: 'chrome', headless: true, + executablePath: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe' +}); +const page = await browser.newPage({ viewport: { width: 1280, height: 800 } }); +const errors = []; +page.on('console', m => { if (m.type() === 'error') errors.push(m.text()); }); +page.on('pageerror', e => errors.push('PAGEERROR: ' + e.message)); + +// Login +await page.goto('http://127.0.0.1:5175/login', { waitUntil: 'networkidle' }); +await page.fill('input[placeholder*="手机"], input[type="tel"]', '13943441149'); +await page.fill('input[type="password"]', 'Test1234'); +await page.click('button:has-text("登录")'); +await page.waitForURL(/\/(home|courseware|exercise)/, { timeout: 8000 }).catch(() => {}); +await page.waitForTimeout(1500); + +// Visit AI generation pages, check they render +const pages = ['/exercise', '/exam', '/animation', '/lesson-plan', '/mindmap', '/essay-grade', '/courseware/create']; +for (const p of pages) { + await page.goto('http://127.0.0.1:5175' + p, { waitUntil: 'networkidle' }).catch(() => {}); + await page.waitForTimeout(800); + const h = await page.textContent('h1, h2').catch(() => '(none)'); + console.log(p, '->', h?.trim()?.substring(0, 40)); +} + +// Check exercise loading tip text exists in DOM (even if hidden) +const tipExists = await page.evaluate(() => document.body.innerHTML.includes('模型深度推理中')); +console.log('TIP_TEXT_PRESENT:', tipExists); + +console.log('CONSOLE_ERRORS:', errors.length); +errors.slice(0, 8).forEach(e => console.log(' ERR:', e.substring(0, 120))); +await browser.close(); diff --git a/frontend/_test_idiom.cjs b/frontend/_test_idiom.cjs new file mode 100644 index 0000000..e6f9ba1 --- /dev/null +++ b/frontend/_test_idiom.cjs @@ -0,0 +1,30 @@ +const { chromium } = require('playwright-core'); +(async () => { + const CHROME = String.raw`C:\Program Files\Google\Chrome\Application\chrome.exe`; + const browser = await chromium.launch({ executablePath: CHROME, headless: true }); + const page = await browser.newPage({ viewport: { width: 1366, height: 850 } }); + const errs = []; + page.on('pageerror', e => errs.push(e.message)); + const base = 'http://127.0.0.1:8010'; + const r = await fetch(base + '/api/auth/login', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({phone:'13943441149',password:'Test1234'}) }); + const tok = (await r.json()).access_token; + await page.goto('http://127.0.0.1:5175/login'); + await page.evaluate(t => { localStorage.setItem('access_token', t); localStorage.setItem('refresh_token','r'); }, tok); + await page.goto('http://127.0.0.1:5175/exercise'); + await page.waitForTimeout(2500); + await page.locator('.tpl-btn').first().click(); + await page.waitForTimeout(1500); + await page.locator('.tg-search input').fill('成语接龙'); + await page.waitForTimeout(800); + await page.locator('.tg-card').first().click(); + await page.waitForTimeout(2000); + // type and play + const frame = page.frameLocator('.tg-preview-wrap iframe').first(); + await frame.locator('#inp').fill('一心一意'); + await page.keyboard.press('Enter'); + await page.waitForTimeout(2500); + const chainCount = await frame.locator('#list > div').count().catch(()=>0); + console.log('ERRORS:', errs.length, 'CHAIN_ITEMS:', chainCount); + if (errs.length) console.log('ERR:', errs.slice(0,3)); + await browser.close(); +})(); diff --git a/frontend/_verify_404.cjs b/frontend/_verify_404.cjs new file mode 100644 index 0000000..589a49e --- /dev/null +++ b/frontend/_verify_404.cjs @@ -0,0 +1,55 @@ +const { chromium } = require('playwright-core'); +(async () => { + const CHROME = String.raw`C:\Program Files\Google\Chrome\Application\chrome.exe`; + const browser = await chromium.launch({ executablePath: CHROME, headless: true }); + const page = await browser.newPage({ viewport: { width: 1280, height: 800 } }); + const errs = []; + page.on('pageerror', e => errs.push(e.message)); + page.on('console', m => { if (m.type()==='error') errs.push(m.text()); }); + + // 1) Verify SEO meta tags on a normal page + await page.goto('http://127.0.0.1:5175/login'); + await page.waitForTimeout(2000); + const desc = await page.locator('meta[name="description"]').getAttribute('content').catch(()=> ''); + const ogTitle = await page.locator('meta[property="og:title"]').getAttribute('content').catch(()=> ''); + const themeColor = await page.locator('meta[name="theme-color"]').getAttribute('content').catch(()=> ''); + const noscript = await page.locator('noscript').count(); + console.log('META description:', desc ? desc.slice(0,50)+'...' : 'MISSING'); + console.log('META og:title:', ogTitle || 'MISSING'); + console.log('META theme-color:', themeColor || 'MISSING'); + console.log('noscript fallback:', noscript > 0 ? 'PRESENT' : 'MISSING'); + + // 2) Test shallow 404 (within AppLayout) + await page.goto('http://127.0.0.1:5175/nonexistent-page'); + await page.waitForTimeout(1500); + const title404 = (await page.locator('.nf-title').textContent().catch(()=> '')) || ''; + const code404 = (await page.locator('.nf-code').textContent().catch(()=> '')) || ''; + console.log('\nSHALLOW 404 (/nonexistent-page):'); + console.log(' nf-code:', code404.trim()); + console.log(' nf-title:', title404.trim()); + console.log(' has home btn:', await page.locator('.nf-btn.primary').count()); + + // 3) Test deep 404 + await page.goto('http://127.0.0.1:5175/foo/bar/baz/deep'); + await page.waitForTimeout(1500); + const deepTitle = (await page.locator('.nf-title').textContent().catch(()=> '')) || ''; + console.log('\nDEEP 404 (/foo/bar/baz/deep):'); + console.log(' nf-title:', deepTitle.trim()); + + // 4) Test that clicking "返回首页" works + await page.goto('http://127.0.0.1:5175/xyz'); + await page.waitForTimeout(1500); + await page.locator('.nf-btn.primary').click(); + await page.waitForTimeout(1500); + console.log('\nCLICK HOME -> url:', page.url()); + console.log(' landed on home:', page.url().endsWith('/login') || page.url().endsWith('/5175/')); + + // 5) screenshot + await page.goto('http://127.0.0.1:5175/nonexistent'); + await page.waitForTimeout(1500); + await page.screenshot({ path: '../audit_shots/_404_page.png' }); + + console.log('\nERRORS:', errs.length); + if (errs.length) console.log('ERR_DETAIL:', [...new Set(errs)].slice(0,5)); + await browser.close(); +})(); diff --git a/frontend/_verify_games.cjs b/frontend/_verify_games.cjs new file mode 100644 index 0000000..f384424 --- /dev/null +++ b/frontend/_verify_games.cjs @@ -0,0 +1,38 @@ +const { chromium } = require('playwright-core'); +(async () => { + const CHROME = String.raw`C:\Program Files\Google\Chrome\Application\chrome.exe`; + const browser = await chromium.launch({ executablePath: CHROME, headless: true }); + const page = await browser.newPage({ viewport: { width: 1366, height: 850 } }); + const errors = []; + page.on('pageerror', e => errors.push(e.message)); + page.on('console', m => { if (m.type()==='error') errors.push(m.text()); }); + const base = 'http://127.0.0.1:8010'; + const r = await fetch(base + '/api/auth/login', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({phone:'13943441149',password:'Test1234'}) }); + const tok = (await r.json()).access_token; + await page.goto('http://127.0.0.1:5175/login'); + await page.evaluate(t => { localStorage.setItem('access_token', t); localStorage.setItem('refresh_token','r'); }, tok); + await page.goto('http://127.0.0.1:5175/exercise'); + await page.waitForTimeout(2500); + await page.locator('.tpl-btn').first().click(); + await page.waitForTimeout(1500); + const cards = await page.locator('.tg-card').count(); + const ours = ['知识分类','序列记忆','成语接龙','定时炸弹','入门数独']; + const results = []; + for (let i = 0; i < cards; i++) { + await page.locator('.tg-card').nth(i).click(); + await page.waitForTimeout(900); + const name = (await page.locator('.tg-main-head h3').textContent().catch(()=> '') || '').trim(); + if (ours.some(k => name.includes(k))) { + const frame = page.frameLocator('.tg-preview-wrap iframe').first(); + let ok = false; + try { ok = (await frame.locator('body').innerHTML({ timeout: 3000 }).catch(()=> '')).length > 200; } catch(e) {} + results.push({ name, ok }); + } + } + console.log('TOTAL_GAME_CARDS:', cards); + console.log('NEW_GAMES:', JSON.stringify(results)); + console.log('ALL_NEW_OK:', results.length === 5 && results.every(r => r.ok)); + console.log('ERRORS_DURING_NEW:', errors.length); + await page.screenshot({ path: '../audit_shots/_games_final.png' }); + await browser.close(); +})(); diff --git a/frontend/_verify_tpl.cjs b/frontend/_verify_tpl.cjs new file mode 100644 index 0000000..7ea4fcf --- /dev/null +++ b/frontend/_verify_tpl.cjs @@ -0,0 +1,56 @@ +const { chromium } = require('playwright-core'); +const fs = require('fs'); +(async () => { + const CHROME = String.raw`C:\Program Files\Google\Chrome\Application\chrome.exe`; + const browser = await chromium.launch({ executablePath: CHROME, headless: true }); + const page = await browser.newPage({ viewport: { width: 1366, height: 850 } }); + const errors = []; + page.on('console', m => { if (m.type() === 'error') errors.push(m.text()); }); + page.on('pageerror', e => errors.push('PAGEERROR: ' + e.message)); + + const base = 'http://127.0.0.1:8010'; + const loginRes = await fetch(base + '/api/auth/login', { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ phone: '13943441149', password: 'Test1234' }) + }); + const loginData = await loginRes.json(); + const token = loginData.data?.access_token || loginData.access_token; + await page.goto('http://127.0.0.1:5175/login'); + await page.evaluate(t => { localStorage.setItem('access_token', t); localStorage.setItem('refresh_token', 'r'); }, token); + await page.goto('http://127.0.0.1:5175/animation'); + await page.waitForTimeout(2000); + + // open gallery via the tpl-btn + await page.locator('.tpl-btn').first().click(); + await page.waitForTimeout(1500); + + const cards = await page.locator('.tg-card').count(); + console.log('gallery cards:', cards); + + const ours = ['勾股定理','单位圆','排序算法','DNA','牛顿第二','板块构造']; + const results = []; + const allCards = page.locator('.tg-card'); + for (let i = 0; i < cards; i++) { + await allCards.nth(i).click(); + await page.waitForTimeout(700); + const name = (await page.locator('.tg-main-head h3').textContent().catch(()=> '') || '').trim(); + const isOurs = ours.some(k => name.includes(k)); + if (isOurs) { + const beforeErr = errors.length; + const frame = page.frameLocator('.tg-preview-wrap iframe').first(); + let stageOk = false; + try { stageOk = (await frame.locator('body').innerHTML({ timeout: 3000 }).catch(()=> '')).length > 200; } catch(e) {} + await page.waitForTimeout(300); + const newErr = errors.length - beforeErr; + results.push({ name, stageOk, newErr }); + console.log(`[${name}] rendered=${stageOk} newErr=${newErr}`); + } + } + + await page.screenshot({ path: '../audit_shots/_tpl_gallery.png' }); + console.log('TOTAL_CONSOLE_ERRORS:', errors.length); + console.log('OURS_CHECKED:', results.length, 'ALL_RENDERED:', results.every(r => r.stageOk)); + if (errors.length) console.log('ERR_SAMPLES:', errors.slice(0,5)); + fs.writeFileSync('../audit_shots/_tpl_results.json', JSON.stringify({ totalCards: cards, errors: errors.slice(0,15), results }, null, 2)); + await browser.close(); +})(); diff --git a/frontend/_walk.cjs b/frontend/_walk.cjs new file mode 100644 index 0000000..af470fc --- /dev/null +++ b/frontend/_walk.cjs @@ -0,0 +1,49 @@ +const { chromium } = require('playwright-core'); +const http = require('http'); + +const BASE = 'http://127.0.0.1:8010'; +const UI = 'http://127.0.0.1:5175'; + +function apiPost(path, body) { + return new Promise((resolve, reject) => { + const data = JSON.stringify(body); + const req = http.request(BASE + path, { method:'POST', headers:{'Content-Type':'application/json','Content-Length':Buffer.byteLength(data)} }, res => { + let b=''; res.on('data',c=>b+=c); res.on('end',()=>resolve({status:res.statusCode, body:b})); + }); + req.on('error', reject); req.write(data); req.end(); + }); +} + +(async () => { + const login = await apiPost('/api/auth/login', {phone:'13943441149', password:'Test1234'}); + const tok = JSON.parse(login.body).access_token; + console.log('token:', tok ? 'yes' : 'NO'); + + const browser = await chromium.launch({ executablePath:'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', headless:true }); + const ctx = await browser.newContext({ viewport:{width:1440,height:900} }); + const page = await ctx.newPage(); + + const errors = []; + page.on('console', m => { if (m.type()==='error') errors.push('CONSOLE: '+m.text().slice(0,150)); }); + page.on('pageerror', e => errors.push('PAGEERR: '+e.message.slice(0,150))); + + // inject token + await page.goto(UI + '/', { waitUntil:'networkidle' }); + await page.evaluate((t) => { localStorage.setItem('access_token', t); localStorage.setItem('token', t); }, tok); + + const views = ['/', '/animation', '/exercise', '/mindmap', '/lesson-plan', '/exam', '/essay-grade', '/courseware/create', '/community', '/resources', '/classroom', '/assistant', '/profile', '/search']; + for (let i=0;inull); + const cnt = await page.locator('h1').count(); + console.log(v, '| h1:', (h1||'').trim().slice(0,40), '| h1count:', cnt); + await page.screenshot({ path:'audit_shots/v'+i+'.png', fullPage:false }); + } catch(e){ console.log(v, 'NAV-ERR', e.message.slice(0,100)); } + } + console.log('=== ERRORS ('+errors.length+') ==='); + errors.slice(0,15).forEach(e=>console.log(e)); + await browser.close(); +})(); diff --git a/frontend/_walk_views.cjs b/frontend/_walk_views.cjs new file mode 100644 index 0000000..92d3912 --- /dev/null +++ b/frontend/_walk_views.cjs @@ -0,0 +1,39 @@ +const { chromium } = require('playwright-core'); +(async () => { + const CHROME = String.raw`C:\Program Files\Google\Chrome\Application\chrome.exe`; + const browser = await chromium.launch({ executablePath: CHROME, headless: true }); + const page = await browser.newPage({ viewport: { width: 1366, height: 850 } }); + const errs = []; + page.on('pageerror', e => errs.push(e.message)); + page.on('console', m => { if (m.type()==='error' && !m.text().includes('401')) errs.push(m.text()); }); + const base = 'http://127.0.0.1:8010'; + const r = await fetch(base + '/api/auth/login', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({phone:'13943441149',password:'Test1234'}) }); + const tok = (await r.json()).access_token; + await page.goto('http://127.0.0.1:5175/login'); + await page.evaluate(t => { localStorage.setItem('access_token', t); localStorage.setItem('refresh_token','r'); }, tok); + const views = [ + {p:'/', n:'Home'}, + {p:'/courseware/create', n:'CoursewareCreate'}, + {p:'/courseware', n:'CoursewareList'}, + {p:'/materials', n:'Materials'}, + {p:'/resources', n:'Resources'}, + {p:'/classroom', n:'Classroom'}, + {p:'/community', n:'Community'}, + {p:'/admin', n:'Admin'}, + {p:'/profile', n:'Profile'}, + ]; + const results = []; + for (const v of views) { + const before = errs.length; + await page.goto('http://127.0.0.1:5175' + v.p); + await page.waitForTimeout(1800); + const after = errs.length - before; + const h1 = (await page.locator('h1').first().textContent().catch(()=> '')) || ''; + const empty = await page.locator('.el-empty').count().catch(()=>0); + results.push({ view: v.n, path: v.p, newErrs: after, h1: (h1||'').slice(0,40), emptyState: empty }); + } + console.log(JSON.stringify(results, null, 1)); + console.log('TOTAL_NEW_ERRORS:', errs.length); + if (errs.length) console.log('ERRORS:', [...new Set(errs)].slice(0,6)); + await browser.close(); +})(); diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..c48547b --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,40 @@ + + + + + + + + + 智教助手 - 教师专属AI备课工具 + + + + + + + + + + + + + + + + + + + +
    + + + + diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..bdce902 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,21 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } + + location /api/ { + proxy_pass http://backend:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_read_timeout 120s; + } + + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml; +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..41ddb08 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,3874 @@ +{ + "name": "ai-teaching-platform", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ai-teaching-platform", + "version": "1.0.0", + "dependencies": { + "@element-plus/icons-vue": "^2.3.1", + "@wangeditor/editor": "^5.1.23", + "@wangeditor/editor-for-vue": "^5.1.12", + "axios": "^1.7.9", + "echarts": "^5.5.1", + "element-plus": "^2.9.1", + "lottie-web": "^5.12.2", + "marked": "^18.0.4", + "pinia": "^2.3.0", + "vue": "^3.5.13", + "vue-echarts": "^7.0.3", + "vue-router": "^4.5.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.2.1", + "sass": "^1.83.4", + "typescript": "^5.7.2", + "unplugin-auto-import": "^0.18.6", + "unplugin-vue-components": "^0.27.5", + "vite": "^6.0.7", + "vue-tsc": "^2.2.0" + } + }, + "node_modules/@antfu/utils": { + "version": "0.7.10", + "resolved": "https://registry.npmmirror.com/@antfu/utils/-/utils-0.7.10.tgz", + "integrity": "sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "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/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz", + "integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@element-plus/icons-vue": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz", + "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==", + "license": "MIT", + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@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/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/@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/@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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@popperjs/core": { + "name": "@sxzz/popperjs-es", + "version": "2.11.8", + "resolved": "https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz", + "integrity": "sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@transloadit/prettier-bytes": { + "version": "0.0.7", + "resolved": "https://registry.npmmirror.com/@transloadit/prettier-bytes/-/prettier-bytes-0.0.7.tgz", + "integrity": "sha512-VeJbUb0wEKbcwaSlj5n+LscBl9IPgLPkHVGBkh00cztv6X4L/TJXK58LzFuBKX7/GAfiGhIwH67YTLTlzvIzBA==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmmirror.com/@types/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-zx2/Gg0Eg7gwEiOIIh5w9TrhKKTeQh7CPCOPNc0el4pLSwzebA8SmnHwZs2dWlLONvyulykSwGSQxQHLhjGLvQ==", + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.24", + "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.24.tgz", + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "license": "MIT" + }, + "node_modules/@uppy/companion-client": { + "version": "2.2.2", + "resolved": "https://registry.npmmirror.com/@uppy/companion-client/-/companion-client-2.2.2.tgz", + "integrity": "sha512-5mTp2iq97/mYSisMaBtFRry6PTgZA6SIL7LePteOV5x0/DxKfrZW3DEiQERJmYpHzy7k8johpm2gHnEKto56Og==", + "license": "MIT", + "dependencies": { + "@uppy/utils": "^4.1.2", + "namespace-emitter": "^2.0.1" + } + }, + "node_modules/@uppy/core": { + "version": "2.3.4", + "resolved": "https://registry.npmmirror.com/@uppy/core/-/core-2.3.4.tgz", + "integrity": "sha512-iWAqppC8FD8mMVqewavCz+TNaet6HPXitmGXpGGREGrakZ4FeuWytVdrelydzTdXx6vVKkOmI2FLztGg73sENQ==", + "license": "MIT", + "dependencies": { + "@transloadit/prettier-bytes": "0.0.7", + "@uppy/store-default": "^2.1.1", + "@uppy/utils": "^4.1.3", + "lodash.throttle": "^4.1.1", + "mime-match": "^1.0.2", + "namespace-emitter": "^2.0.1", + "nanoid": "^3.1.25", + "preact": "^10.5.13" + } + }, + "node_modules/@uppy/store-default": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/@uppy/store-default/-/store-default-2.1.1.tgz", + "integrity": "sha512-xnpTxvot2SeAwGwbvmJ899ASk5tYXhmZzD/aCFsXePh/v8rNvR2pKlcQUH7cF/y4baUGq3FHO/daKCok/mpKqQ==", + "license": "MIT" + }, + "node_modules/@uppy/utils": { + "version": "4.1.3", + "resolved": "https://registry.npmmirror.com/@uppy/utils/-/utils-4.1.3.tgz", + "integrity": "sha512-nTuMvwWYobnJcytDO3t+D6IkVq/Qs4Xv3vyoEZ+Iaf8gegZP+rEyoaFT2CK5XLRMienPyqRqNbIfRuFaOWSIFw==", + "license": "MIT", + "dependencies": { + "lodash.throttle": "^4.1.1" + } + }, + "node_modules/@uppy/xhr-upload": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/@uppy/xhr-upload/-/xhr-upload-2.1.3.tgz", + "integrity": "sha512-YWOQ6myBVPs+mhNjfdWsQyMRWUlrDLMoaG7nvf/G6Y3GKZf8AyjFDjvvJ49XWQ+DaZOftGkHmF1uh/DBeGivJQ==", + "license": "MIT", + "dependencies": { + "@uppy/companion-client": "^2.2.2", + "@uppy/utils": "^4.1.2", + "nanoid": "^3.1.25" + }, + "peerDependencies": { + "@uppy/core": "^2.3.3" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.15", + "resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.15.tgz", + "integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.15" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.15", + "resolved": "https://registry.npmmirror.com/@volar/source-map/-/source-map-2.4.15.tgz", + "integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.15", + "resolved": "https://registry.npmmirror.com/@volar/typescript/-/typescript-2.4.15.tgz", + "integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.34", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.34.tgz", + "integrity": "sha512-s9cLyK5mLcvZ4Agva5QgRsQyLKvts9WbU9DB6NqiZkkGEdwmcEiylj5Jbwkp680drF/NNCV8OlAJSe+yMLxaJw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@vue/shared": "3.5.34", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.34", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.34.tgz", + "integrity": "sha512-EbF/T++k0e2MMZlJsBhzK8Sgwt0HcIPOhzn1CTB/lv6sQcyk+OWf8YeiLxZp3ro7MbbLcAfAJ6sEvjFWuNgUCw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.34", + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.34", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.34.tgz", + "integrity": "sha512-D/ihr6uZeIt6r+pVZf46RWT1fAsLFMbUP7k8G1VkiiWexriED9GrX3echHd4Abbt17zjlfiFJ8z7a3BxZOPNjg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@vue/compiler-core": "3.5.34", + "@vue/compiler-dom": "3.5.34", + "@vue/compiler-ssr": "3.5.34", + "@vue/shared": "3.5.34", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.14", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.34", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.34.tgz", + "integrity": "sha512-cDtTHKibkThKGHH1SP+WdccquNRYQDFH6rRjQCqT9G2ltFAfoR5pUftpab/z+aM5mW9HLLVQW7hfKKQe/1GBeQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.34", + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/compiler-vue2": { + "version": "2.7.16", + "resolved": "https://registry.npmmirror.com/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", + "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", + "dev": true, + "license": "MIT", + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/language-core": { + "version": "2.2.12", + "resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-2.2.12.tgz", + "integrity": "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "@vue/compiler-dom": "^3.5.0", + "@vue/compiler-vue2": "^2.7.16", + "@vue/shared": "^3.5.0", + "alien-signals": "^1.0.3", + "minimatch": "^9.0.3", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.34", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.34.tgz", + "integrity": "sha512-y9XDjCEuBp+98k+UL5dbYkh57AHU4o6cxZedOPXw3bmrZZYLQsVHguGurq7hVrPCSrQtrnz1f9dssyFr+dMXfQ==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.34", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.34.tgz", + "integrity": "sha512-mKeBYvu8tcMSLhypAHBmriUFfWXKTCF/23Z4jiCoYK3UtWepkliViNLuR90V9XOyD62mUxs9p1jsrpK3CCGIzw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.34", + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.34", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.34.tgz", + "integrity": "sha512-e8kZzERmCwUnBRVsgSQlAfrfU2rGoy0FFKPBXSlfEjc/O3KfA7QP0t1/2ZylrbchjmIKB4dPTd07A6WPr0eOrg==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.34", + "@vue/runtime-core": "3.5.34", + "@vue/shared": "3.5.34", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.34", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.34.tgz", + "integrity": "sha512-nHxmJoTrKsmrkbILRhkC9gY1G3moZbJTqCzDd7DOOzG5KH9oeJ0Unqrff5f9v0pW//jES05ZkJcNtfE8JjOIew==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.34", + "@vue/shared": "3.5.34" + }, + "peerDependencies": { + "vue": "3.5.34" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.34", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.34.tgz", + "integrity": "sha512-24uqU4OIiX29ryC3MeWid/Xf2fa2EFRUVLb77nRhk+UrTVrh/XiGtFAFmJBAtBRbjwNdsPRP+jj/OL27Eg1NDA==", + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "14.3.0", + "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-14.3.0.tgz", + "integrity": "sha512-aHfz47g0ZhMtTVHmIzMVpJy8ePhhOy68GY5bv110+5DVtZ+W7BsOx+m61UNQqfrWyPztIHIanWa3E2tib3NFIw==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "14.3.0", + "@vueuse/shared": "14.3.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@vueuse/metadata": { + "version": "14.3.0", + "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-14.3.0.tgz", + "integrity": "sha512-BwxmbAzwAVF50+MW57GXOUEV61nFBGnlBvrTqj49PqWJu3uw7hdu72ztXeZ33RdZtDY6kO+bfCAE1PCn88Tktw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "14.3.0", + "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-14.3.0.tgz", + "integrity": "sha512-bZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@wangeditor/basic-modules": { + "version": "1.1.7", + "resolved": "https://registry.npmmirror.com/@wangeditor/basic-modules/-/basic-modules-1.1.7.tgz", + "integrity": "sha512-cY9CPkLJaqF05STqfpZKWG4LpxTMeGSIIF1fHvfm/mz+JXatCagjdkbxdikOuKYlxDdeqvOeBmsUBItufDLXZg==", + "license": "MIT", + "dependencies": { + "is-url": "^1.2.4" + }, + "peerDependencies": { + "@wangeditor/core": "1.x", + "dom7": "^3.0.0", + "lodash.throttle": "^4.1.1", + "nanoid": "^3.2.0", + "slate": "^0.72.0", + "snabbdom": "^3.1.0" + } + }, + "node_modules/@wangeditor/code-highlight": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@wangeditor/code-highlight/-/code-highlight-1.0.3.tgz", + "integrity": "sha512-iazHwO14XpCuIWJNTQTikqUhGKyqj+dUNWJ9288Oym9M2xMVHvnsOmDU2sgUDWVy+pOLojReMPgXCsvvNlOOhw==", + "license": "MIT", + "dependencies": { + "prismjs": "^1.23.0" + }, + "peerDependencies": { + "@wangeditor/core": "1.x", + "dom7": "^3.0.0", + "slate": "^0.72.0", + "snabbdom": "^3.1.0" + } + }, + "node_modules/@wangeditor/core": { + "version": "1.1.19", + "resolved": "https://registry.npmmirror.com/@wangeditor/core/-/core-1.1.19.tgz", + "integrity": "sha512-KevkB47+7GhVszyYF2pKGKtCSj/YzmClsD03C3zTt+9SR2XWT5T0e3yQqg8baZpcMvkjs1D8Dv4fk8ok/UaS2Q==", + "license": "MIT", + "dependencies": { + "@types/event-emitter": "^0.3.3", + "event-emitter": "^0.3.5", + "html-void-elements": "^2.0.0", + "i18next": "^20.4.0", + "scroll-into-view-if-needed": "^2.2.28", + "slate-history": "^0.66.0" + }, + "peerDependencies": { + "@uppy/core": "^2.1.1", + "@uppy/xhr-upload": "^2.0.3", + "dom7": "^3.0.0", + "is-hotkey": "^0.2.0", + "lodash.camelcase": "^4.3.0", + "lodash.clonedeep": "^4.5.0", + "lodash.debounce": "^4.0.8", + "lodash.foreach": "^4.5.0", + "lodash.isequal": "^4.5.0", + "lodash.throttle": "^4.1.1", + "lodash.toarray": "^4.4.0", + "nanoid": "^3.2.0", + "slate": "^0.72.0", + "snabbdom": "^3.1.0" + } + }, + "node_modules/@wangeditor/editor": { + "version": "5.1.23", + "resolved": "https://registry.npmmirror.com/@wangeditor/editor/-/editor-5.1.23.tgz", + "integrity": "sha512-0RxfeVTuK1tktUaPROnCoFfaHVJpRAIE2zdS0mpP+vq1axVQpLjM8+fCvKzqYIkH0Pg+C+44hJpe3VVroSkEuQ==", + "license": "MIT", + "dependencies": { + "@uppy/core": "^2.1.1", + "@uppy/xhr-upload": "^2.0.3", + "@wangeditor/basic-modules": "^1.1.7", + "@wangeditor/code-highlight": "^1.0.3", + "@wangeditor/core": "^1.1.19", + "@wangeditor/list-module": "^1.0.5", + "@wangeditor/table-module": "^1.1.4", + "@wangeditor/upload-image-module": "^1.0.2", + "@wangeditor/video-module": "^1.1.4", + "dom7": "^3.0.0", + "is-hotkey": "^0.2.0", + "lodash.camelcase": "^4.3.0", + "lodash.clonedeep": "^4.5.0", + "lodash.debounce": "^4.0.8", + "lodash.foreach": "^4.5.0", + "lodash.isequal": "^4.5.0", + "lodash.throttle": "^4.1.1", + "lodash.toarray": "^4.4.0", + "nanoid": "^3.2.0", + "slate": "^0.72.0", + "snabbdom": "^3.1.0" + } + }, + "node_modules/@wangeditor/editor-for-vue": { + "version": "5.1.12", + "resolved": "https://registry.npmmirror.com/@wangeditor/editor-for-vue/-/editor-for-vue-5.1.12.tgz", + "integrity": "sha512-0Ds3D8I+xnpNWezAeO7HmPRgTfUxHLMd9JKcIw+QzvSmhC5xUHbpCcLU+KLmeBKTR/zffnS5GQo6qi3GhTMJWQ==", + "license": "MIT", + "peerDependencies": { + "@wangeditor/editor": ">=5.1.0", + "vue": "^3.0.5" + } + }, + "node_modules/@wangeditor/list-module": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/@wangeditor/list-module/-/list-module-1.0.5.tgz", + "integrity": "sha512-uDuYTP6DVhcYf7mF1pTlmNn5jOb4QtcVhYwSSAkyg09zqxI1qBqsfUnveeDeDqIuptSJhkh81cyxi+MF8sEPOQ==", + "license": "MIT", + "peerDependencies": { + "@wangeditor/core": "1.x", + "dom7": "^3.0.0", + "slate": "^0.72.0", + "snabbdom": "^3.1.0" + } + }, + "node_modules/@wangeditor/table-module": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/@wangeditor/table-module/-/table-module-1.1.4.tgz", + "integrity": "sha512-5saanU9xuEocxaemGdNi9t8MCDSucnykEC6jtuiT72kt+/Hhh4nERYx1J20OPsTCCdVr7hIyQenFD1iSRkIQ6w==", + "license": "MIT", + "peerDependencies": { + "@wangeditor/core": "1.x", + "dom7": "^3.0.0", + "lodash.isequal": "^4.5.0", + "lodash.throttle": "^4.1.1", + "nanoid": "^3.2.0", + "slate": "^0.72.0", + "snabbdom": "^3.1.0" + } + }, + "node_modules/@wangeditor/upload-image-module": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/@wangeditor/upload-image-module/-/upload-image-module-1.0.2.tgz", + "integrity": "sha512-z81lk/v71OwPDYeQDxj6cVr81aDP90aFuywb8nPD6eQeECtOymrqRODjpO6VGvCVxVck8nUxBHtbxKtjgcwyiA==", + "license": "MIT", + "peerDependencies": { + "@uppy/core": "^2.0.3", + "@uppy/xhr-upload": "^2.0.3", + "@wangeditor/basic-modules": "1.x", + "@wangeditor/core": "1.x", + "dom7": "^3.0.0", + "lodash.foreach": "^4.5.0", + "slate": "^0.72.0", + "snabbdom": "^3.1.0" + } + }, + "node_modules/@wangeditor/video-module": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/@wangeditor/video-module/-/video-module-1.1.4.tgz", + "integrity": "sha512-ZdodDPqKQrgx3IwWu4ZiQmXI8EXZ3hm2/fM6E3t5dB8tCaIGWQZhmqd6P5knfkRAd3z2+YRSRbxOGfoRSp/rLg==", + "license": "MIT", + "peerDependencies": { + "@uppy/core": "^2.1.4", + "@uppy/xhr-upload": "^2.0.7", + "@wangeditor/core": "1.x", + "dom7": "^3.0.0", + "nanoid": "^3.2.0", + "slate": "^0.72.0", + "snabbdom": "^3.1.0" + } + }, + "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/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/alien-signals": { + "version": "1.0.13", + "resolved": "https://registry.npmmirror.com/alien-signals/-/alien-signals-1.0.13.tgz", + "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==", + "dev": true, + "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==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.16.1", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.16.1.tgz", + "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-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==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compute-scroll-into-view": { + "version": "1.0.20", + "resolved": "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz", + "integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==", + "license": "MIT" + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "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/d": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/dayjs": { + "version": "1.11.20", + "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", + "license": "MIT" + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dom7": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/dom7/-/dom7-3.0.0.tgz", + "integrity": "sha512-oNlcUdHsC4zb7Msx7JN3K0Nro1dzJ48knvBOnDPKJ2GV9wl1i5vydJZUSyOfrkKFDZEud/jBsTk92S/VGSAe/g==", + "license": "MIT", + "dependencies": { + "ssr-window": "^3.0.0-alpha.1" + } + }, + "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==", + "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/echarts": { + "version": "5.6.0", + "resolved": "https://registry.npmmirror.com/echarts/-/echarts-5.6.0.tgz", + "integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "2.3.0", + "zrender": "5.6.1" + } + }, + "node_modules/element-plus": { + "version": "2.14.0", + "resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.14.0.tgz", + "integrity": "sha512-POgH+TtoreaEKWqYYAVQyE6i8rQMEFqAEublyF29dBA5yASWPLKY6EzfeqBTr2Uv26mPss4vSrMrNPyaK7LX5w==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^4.2.0", + "@element-plus/icons-vue": "^2.3.2", + "@floating-ui/dom": "^1.0.1", + "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.8", + "@types/lodash": "^4.17.24", + "@types/lodash-es": "^4.17.12", + "@vueuse/core": "14.3.0", + "async-validator": "^4.2.5", + "dayjs": "^1.11.20", + "lodash": "^4.18.1", + "lodash-es": "^4.18.1", + "lodash-unified": "^1.0.3", + "memoize-one": "^6.0.0", + "normalize-wheel-es": "^1.2.0", + "vue-component-type-helpers": "^3.2.8" + }, + "peerDependencies": { + "vue": "^3.3.7" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "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==", + "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/es5-ext": { + "version": "0.10.64", + "resolved": "https://registry.npmmirror.com/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-symbol": { + "version": "3.1.4", + "resolved": "https://registry.npmmirror.com/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "ext": "^1.7.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmmirror.com/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmmirror.com/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "license": "ISC", + "dependencies": { + "type": "^2.7.2" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.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/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.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/he": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/html-void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/html-void-elements/-/html-void-elements-2.0.1.tgz", + "integrity": "sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/i18next": { + "version": "20.6.1", + "resolved": "https://registry.npmmirror.com/i18next/-/i18next-20.6.1.tgz", + "integrity": "sha512-yCMYTMEJ9ihCwEQQ3phLo7I/Pwycf8uAx+sRHwwk5U9Aui/IZYgQRyMqXafQOw5QQ7DM1Z+WyEXWIqSuJHhG2A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.0" + } + }, + "node_modules/immer": { + "version": "9.0.21", + "resolved": "https://registry.npmmirror.com/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/immutable": { + "version": "5.1.5", + "resolved": "https://registry.npmmirror.com/immutable/-/immutable-5.1.5.tgz", + "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hotkey": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/is-hotkey/-/is-hotkey-0.2.0.tgz", + "integrity": "sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw==", + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/local-pkg": { + "version": "0.5.1", + "resolved": "https://registry.npmmirror.com/local-pkg/-/local-pkg-0.5.1.tgz", + "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.3", + "pkg-types": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "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-es": { + "version": "4.18.1", + "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, + "node_modules/lodash-unified": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/lodash-unified/-/lodash-unified-1.0.3.tgz", + "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==", + "license": "MIT", + "peerDependencies": { + "@types/lodash-es": "*", + "lodash": "*", + "lodash-es": "*" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "license": "MIT" + }, + "node_modules/lodash.foreach": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz", + "integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, + "node_modules/lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", + "license": "MIT" + }, + "node_modules/lodash.toarray": { + "version": "4.4.0", + "resolved": "https://registry.npmmirror.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz", + "integrity": "sha512-QyffEA3i5dma5q2490+SgCvDN0pXLmRGSyAANuVi0HQ01Pkfr9fuoKQW8wm1wGBnJITs/mS7wQvS6VshUEBFCw==", + "license": "MIT" + }, + "node_modules/lottie-web": { + "version": "5.13.0", + "resolved": "https://registry.npmmirror.com/lottie-web/-/lottie-web-5.13.0.tgz", + "integrity": "sha512-+gfBXl6sxXMPe8tKQm7qzLnUy5DUPJPKIyRHwtpCpyUEYjHYRJC/5gjUvdkuO2c3JllrPtHXH5UJJK8LRYl5yQ==", + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/marked": { + "version": "18.0.4", + "resolved": "https://registry.npmmirror.com/marked/-/marked-18.0.4.tgz", + "integrity": "sha512-c/BTaKzg0G6ezQx97DAkYU7k0HM6ys0FqYeKBL6hlBByZwy+ycA1+f0vDdjMHKKeEjdgkx0GOv9Il6D+85cOqA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/mime-match/-/mime-match-1.0.2.tgz", + "integrity": "sha512-VXp/ugGDVh3eCLOBCiHZMYWQaTNUHv2IJrut+yXA6+JbLPXHglHwfS/5A5L0ll+jkCY7fIzRJcH6OIunF+c6Cg==", + "license": "ISC", + "dependencies": { + "wildcard": "^1.1.0" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "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/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmmirror.com/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/namespace-emitter": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/namespace-emitter/-/namespace-emitter-2.0.1.tgz", + "integrity": "sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "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/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", + "license": "ISC" + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-wheel-es": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", + "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==", + "license": "BSD-3-Clause" + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "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": "4.0.4", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pinia": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/pinia/-/pinia-2.3.1.tgz", + "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "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.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/preact": { + "version": "10.29.2", + "resolved": "https://registry.npmmirror.com/preact/-/preact-10.29.2.tgz", + "integrity": "sha512-7tNmwg/7mzzAoB/8kSg6Hl37JraAZw3Z3A0JSY7VXlZwo82Xn0G7wKbNNs2qoF4ZEEsQGTwDAroNdqKs1ofJxQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmmirror.com/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/quansync": { + "version": "0.2.11", + "resolved": "https://registry.npmmirror.com/quansync/-/quansync-0.2.11.tgz", + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, + "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==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup/node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/sass": { + "version": "1.100.0", + "resolved": "https://registry.npmmirror.com/sass/-/sass-1.100.0.tgz", + "integrity": "sha512-B5j0rYMlinhhOo9tjQebMVVn0TfyXAF+wB3b2ggZUuJ/is/Y+7+JGjirAMxHZ9Z3hIP98NPfamlAkBHa1lAaXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^5.0.0", + "immutable": "^5.1.5", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=20.19.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/scroll-into-view-if-needed": { + "version": "2.2.31", + "resolved": "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz", + "integrity": "sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==", + "license": "MIT", + "dependencies": { + "compute-scroll-into-view": "^1.0.20" + } + }, + "node_modules/scule": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/scule/-/scule-1.3.0.tgz", + "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==", + "dev": true, + "license": "MIT" + }, + "node_modules/slate": { + "version": "0.72.8", + "resolved": "https://registry.npmmirror.com/slate/-/slate-0.72.8.tgz", + "integrity": "sha512-/nJwTswQgnRurpK+bGJFH1oM7naD5qDmHd89JyiKNT2oOKD8marW0QSBtuFnwEbL5aGCS8AmrhXQgNOsn4osAw==", + "license": "MIT", + "dependencies": { + "immer": "^9.0.6", + "is-plain-object": "^5.0.0", + "tiny-warning": "^1.0.3" + } + }, + "node_modules/slate-history": { + "version": "0.66.0", + "resolved": "https://registry.npmmirror.com/slate-history/-/slate-history-0.66.0.tgz", + "integrity": "sha512-6MWpxGQZiMvSINlCbMW43E2YBSVMCMCIwQfBzGssjWw4kb0qfvj0pIdblWNRQZD0hR6WHP+dHHgGSeVdMWzfng==", + "license": "MIT", + "dependencies": { + "is-plain-object": "^5.0.0" + }, + "peerDependencies": { + "slate": ">=0.65.3" + } + }, + "node_modules/snabbdom": { + "version": "3.6.3", + "resolved": "https://registry.npmmirror.com/snabbdom/-/snabbdom-3.6.3.tgz", + "integrity": "sha512-W2lHLLw2qR2Vv0DcMmcxXqcfdBaIcoN+y/86SmHv8fn4DazEQSH6KN3TjZcWvwujW56OHiiirsbHWZb4vx/0fg==", + "license": "MIT", + "engines": { + "node": ">=12.17.0" + } + }, + "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/ssr-window": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/ssr-window/-/ssr-window-3.0.0.tgz", + "integrity": "sha512-q+8UfWDg9Itrg0yWK7oe5p/XRCJpJF9OBtXfOPgSJl+u3Xd5KI328RUEvUqSMVM9CiQUEf1QdBzJMkYGErj9QA==", + "license": "MIT" + }, + "node_modules/strip-literal": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/strip-literal/-/strip-literal-2.1.1.tgz", + "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + }, + "node_modules/type": { + "version": "2.7.3", + "resolved": "https://registry.npmmirror.com/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", + "license": "ISC" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmmirror.com/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/unimport": { + "version": "3.14.6", + "resolved": "https://registry.npmmirror.com/unimport/-/unimport-3.14.6.tgz", + "integrity": "sha512-CYvbDaTT04Rh8bmD8jz3WPmHYZRG/NnvYVzwD6V1YAlvvKROlAeNDUBhkBGzNav2RKaeuXvlWYaa1V4Lfi/O0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.4", + "acorn": "^8.14.0", + "escape-string-regexp": "^5.0.0", + "estree-walker": "^3.0.3", + "fast-glob": "^3.3.3", + "local-pkg": "^1.0.0", + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "pathe": "^2.0.1", + "picomatch": "^4.0.2", + "pkg-types": "^1.3.0", + "scule": "^1.3.0", + "strip-literal": "^2.1.1", + "unplugin": "^1.16.1" + } + }, + "node_modules/unimport/node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unimport/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/unimport/node_modules/local-pkg": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/local-pkg/-/local-pkg-1.2.1.tgz", + "integrity": "sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.4", + "pkg-types": "^2.3.0", + "quansync": "^0.2.11" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/unimport/node_modules/local-pkg/node_modules/pkg-types": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-2.3.1.tgz", + "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.4", + "exsolve": "^1.0.8", + "pathe": "^2.0.3" + } + }, + "node_modules/unplugin": { + "version": "1.16.1", + "resolved": "https://registry.npmmirror.com/unplugin/-/unplugin-1.16.1.tgz", + "integrity": "sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.14.0", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/unplugin-auto-import": { + "version": "0.18.6", + "resolved": "https://registry.npmmirror.com/unplugin-auto-import/-/unplugin-auto-import-0.18.6.tgz", + "integrity": "sha512-LMFzX5DtkTj/3wZuyG5bgKBoJ7WSgzqSGJ8ppDRdlvPh45mx6t6w3OcbExQi53n3xF5MYkNGPNR/HYOL95KL2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@antfu/utils": "^0.7.10", + "@rollup/pluginutils": "^5.1.3", + "fast-glob": "^3.3.2", + "local-pkg": "^0.5.1", + "magic-string": "^0.30.14", + "minimatch": "^9.0.5", + "unimport": "^3.13.4", + "unplugin": "^1.16.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@nuxt/kit": "^3.2.2", + "@vueuse/core": "*" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + }, + "@vueuse/core": { + "optional": true + } + } + }, + "node_modules/unplugin-vue-components": { + "version": "0.27.5", + "resolved": "https://registry.npmmirror.com/unplugin-vue-components/-/unplugin-vue-components-0.27.5.tgz", + "integrity": "sha512-m9j4goBeNwXyNN8oZHHxvIIYiG8FQ9UfmKWeNllpDvhU7btKNNELGPt+o3mckQKuPwrE7e0PvCsx+IWuDSD9Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@antfu/utils": "^0.7.10", + "@rollup/pluginutils": "^5.1.3", + "chokidar": "^3.6.0", + "debug": "^4.3.7", + "fast-glob": "^3.3.2", + "local-pkg": "^0.5.1", + "magic-string": "^0.30.14", + "minimatch": "^9.0.5", + "mlly": "^1.7.3", + "unplugin": "^1.16.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@babel/parser": "^7.15.8", + "@nuxt/kit": "^3.2.2", + "vue": "2 || 3" + }, + "peerDependenciesMeta": { + "@babel/parser": { + "optional": true + }, + "@nuxt/kit": { + "optional": true + } + } + }, + "node_modules/unplugin-vue-components/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "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/unplugin-vue-components/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/unplugin-vue-components/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/vite": { + "version": "6.4.2", + "resolved": "https://registry.npmmirror.com/vite/-/vite-6.4.2.tgz", + "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.34", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.34.tgz", + "integrity": "sha512-WdLBG9gm02OgJIG9axd5Hpx0TFLdzVgfG2evFFu8Rur5O/IoGc5cMjnjh3tPL6GnRGsYvUhBSKVPYVcxRKpMCA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.34", + "@vue/compiler-sfc": "3.5.34", + "@vue/runtime-dom": "3.5.34", + "@vue/server-renderer": "3.5.34", + "@vue/shared": "3.5.34" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-component-type-helpers": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/vue-component-type-helpers/-/vue-component-type-helpers-3.3.1.tgz", + "integrity": "sha512-pu58kqxmVyEH6VfNYW1UyEfR3XAnJ27ZXT3yzXxxpjLxVzAbyC35Zk/nm/RMs7ijWnJNSd9fWkeex2OhUsx3MA==", + "license": "MIT" + }, + "node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-echarts": { + "version": "7.0.3", + "resolved": "https://registry.npmmirror.com/vue-echarts/-/vue-echarts-7.0.3.tgz", + "integrity": "sha512-/jSxNwOsw5+dYAUcwSfkLwKPuzTQ0Cepz1LxCOpj2QcHrrmUa/Ql0eQqMmc1rTPQVrh2JQ29n2dhq75ZcHvRDw==", + "license": "MIT", + "dependencies": { + "vue-demi": "^0.13.11" + }, + "peerDependencies": { + "@vue/runtime-core": "^3.0.0", + "echarts": "^5.5.1", + "vue": "^2.7.0 || ^3.1.1" + }, + "peerDependenciesMeta": { + "@vue/runtime-core": { + "optional": true + } + } + }, + "node_modules/vue-echarts/node_modules/vue-demi": { + "version": "0.13.11", + "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.13.11.tgz", + "integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/vue-tsc": { + "version": "2.2.12", + "resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-2.2.12.tgz", + "integrity": "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.15", + "@vue/language-core": "2.2.12" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/wildcard": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/wildcard/-/wildcard-1.1.2.tgz", + "integrity": "sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng==", + "license": "MIT" + }, + "node_modules/zrender": { + "version": "5.6.1", + "resolved": "https://registry.npmmirror.com/zrender/-/zrender-5.6.1.tgz", + "integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==", + "license": "BSD-3-Clause", + "dependencies": { + "tslib": "2.3.0" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..beb75a1 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,34 @@ +{ + "name": "ai-teaching-platform", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@element-plus/icons-vue": "^2.3.1", + "@wangeditor/editor": "^5.1.23", + "@wangeditor/editor-for-vue": "^5.1.12", + "axios": "^1.7.9", + "echarts": "^5.5.1", + "element-plus": "^2.9.1", + "lottie-web": "^5.12.2", + "marked": "^18.0.4", + "pinia": "^2.3.0", + "vue": "^3.5.13", + "vue-echarts": "^7.0.3", + "vue-router": "^4.5.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.2.1", + "sass": "^1.83.4", + "typescript": "^5.7.2", + "unplugin-auto-import": "^0.18.6", + "unplugin-vue-components": "^0.27.5", + "vite": "^6.0.7", + "vue-tsc": "^2.2.0" + } +} diff --git a/frontend/public/debug.html b/frontend/public/debug.html new file mode 100644 index 0000000..9eea042 --- /dev/null +++ b/frontend/public/debug.html @@ -0,0 +1,107 @@ + + + + +API调试工具 + + + +

    AI课件生成 - 调试工具

    + + +
    + + + diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000..beb2ecb --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1,10 @@ + + + + + + + + + AI + diff --git a/frontend/public/test-content.html b/frontend/public/test-content.html new file mode 100644 index 0000000..9f4515c --- /dev/null +++ b/frontend/public/test-content.html @@ -0,0 +1,32 @@ +
    +
    +
    +
    +
    +
    +
    + 初一数学 +
    +

    一元一次方程

    +
    +

    从生活问题到代数思维的桥梁

    +
    +
    +
    📖
    +
    定义与性质
    +
    概念理解
    +
    +
    +
    ✏️
    +
    解题步骤
    +
    方法掌握
    +
    +
    +
    🎯
    +
    实战练习
    +
    巩固提升
    +
    +
    +
    +
    点击下方箭头开始学习 →
    +
    \ No newline at end of file diff --git a/frontend/public/test-iframe.html b/frontend/public/test-iframe.html new file mode 100644 index 0000000..77737de --- /dev/null +++ b/frontend/public/test-iframe.html @@ -0,0 +1,51 @@ + + +Test + +

    Test: iframe srcdoc rendering

    +
    + +
    + + + + + + \ No newline at end of file diff --git a/frontend/public/test-js-iframe.html b/frontend/public/test-js-iframe.html new file mode 100644 index 0000000..96d1505 --- /dev/null +++ b/frontend/public/test-js-iframe.html @@ -0,0 +1,46 @@ + +Vue Iframe Test + +

    Test: Does srcdoc work?

    +
    + + \ No newline at end of file diff --git a/frontend/public/test-render.html b/frontend/public/test-render.html new file mode 100644 index 0000000..afa316f --- /dev/null +++ b/frontend/public/test-render.html @@ -0,0 +1,40 @@ + +Iframe Test + +

    Iframe srcdoc Test

    +

    Below should show the courseware page:

    +
    + +
    + \ No newline at end of file diff --git a/frontend/shot.cjs b/frontend/shot.cjs new file mode 100644 index 0000000..4ca7605 --- /dev/null +++ b/frontend/shot.cjs @@ -0,0 +1,17 @@ +const { chromium } = require('playwright'); +(async () => { + const tok = require('fs').readFileSync('D:/AI/jiaoyu/_tok.txt','utf8').trim(); + const browser = await chromium.launch({ channel: 'chrome' }); + const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } }); + const page = await ctx.newPage(); + await page.goto('http://localhost:5173/login', { waitUntil: 'domcontentloaded' }); + await page.evaluate((t) => { localStorage.setItem('access_token', t); localStorage.setItem('refresh_token', t); }, tok); + await page.goto('http://localhost:5173/assistant', { waitUntil: 'networkidle' }); + await page.waitForTimeout(1500); + await page.screenshot({ path: 'D:/AI/jiaoyu/_assistant_empty.png' }); + await page.fill('textarea', '帮我设计一节小学三年级分数初步认识的导入活动'); + await page.waitForTimeout(400); + await page.screenshot({ path: 'D:/AI/jiaoyu/_assistant_compose.png' }); + await browser.close(); + console.log('OK'); +})().catch(e => { console.error('FAIL', e.message); process.exit(1); }); diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..cd9578a --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,6 @@ + + + diff --git a/frontend/src/api/admin.ts b/frontend/src/api/admin.ts new file mode 100644 index 0000000..fa54575 --- /dev/null +++ b/frontend/src/api/admin.ts @@ -0,0 +1,22 @@ +import request from '@/utils/request' + +export const getAdminDashboard = () => + request.get('/api/admin/dashboard') as unknown as Promise + +export const getAdminUsers = (params?: any) => + request.get('/api/admin/users', { params }) as unknown as Promise + +export const updateAdminUser = (id: number, data: any) => + request.put(`/api/admin/users/${id}`, data) as unknown as Promise + +export const grantAdminCredits = (id: number, amount: number) => + request.post(`/api/admin/users/${id}/grant-credits`, { amount }) as unknown as Promise + +export const getAdminResources = (params?: any) => + request.get('/api/admin/resources', { params }) as unknown as Promise + +export const moderateResource = (id: number, status: string) => + request.put(`/api/admin/resources/${id}/moderate?status=${status}`) as unknown as Promise + +export const getAdminAuditLogs = (params?: any) => + request.get('/api/admin/audit-logs', { params }) as unknown as Promise diff --git a/frontend/src/api/ai.ts b/frontend/src/api/ai.ts new file mode 100644 index 0000000..4a6346e --- /dev/null +++ b/frontend/src/api/ai.ts @@ -0,0 +1,21 @@ +import request from '@/utils/request' + +export const gradeEssay = (data: { essay_text: string; grade_level?: string; essay_type?: string; total_score?: number }) => + request.post('/api/ai/essay-grade', data) + +export const generateExam = (data: { subject: string; grade?: string; knowledge_points?: string[]; difficulty?: string; question_types?: string[]; count?: number; total_score?: number }) => + request.post('/api/ai/exam-generate', data) + +export const parseMaterial = (file: File) => { + const formData = new FormData() + formData.append('file', file) + return request.post('/api/ai/materials/parse', formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + }) as unknown as Promise<{ success: boolean; data: { material_id: number; filename: string; title: string; material_type: string; size: number; summary: string; char_count: number } }> +} + +export const exportHtml = (data: { title: string; html: string }): Promise => + request.post('/api/ai/export/html', data, { responseType: 'blob' }) as unknown as Promise + +export const exportExamDocx = (data: { title: string; subject?: string; grade?: string; questions: any[]; answers?: any[] }): Promise => + request.post('/api/ai/export/exam-docx', data, { responseType: 'blob' }) as unknown as Promise diff --git a/frontend/src/api/animation.ts b/frontend/src/api/animation.ts new file mode 100644 index 0000000..b34f059 --- /dev/null +++ b/frontend/src/api/animation.ts @@ -0,0 +1,27 @@ +import request from '@/utils/request' + +export const getAnimations = (params?: any) => + request.get('/api/animations/', { params }) + +export const getAnimation = (id: number) => + request.get(`/api/animations/${id}`) + +export const createAnimation = (data: any) => + request.post('/api/animations/', data) + +export const updateAnimation = (id: number, data: any) => + request.put(`/api/animations/${id}`, data) + +export const remixAnimation = (id: number) => + request.post(`/api/animations/${id}/remix`) + +export const aiGenerateAnimation = (data: { prompt: string; anim_type?: string; subject?: string }) => + request.post('/api/animations/ai-generate', data, { + headers: { 'Content-Type': 'application/json; charset=utf-8' }, + }) + +export const deleteAnimation = (id: number) => + request.delete(`/api/animations/${id}`) + +export const exportAnimationHtml = (id: number): Promise => + request.get(`/api/animations/${id}/export-html`, { responseType: 'blob' }) as unknown as Promise diff --git a/frontend/src/api/chat.ts b/frontend/src/api/chat.ts new file mode 100644 index 0000000..36c4077 --- /dev/null +++ b/frontend/src/api/chat.ts @@ -0,0 +1,44 @@ +import request from '@/utils/request' + +export interface ChatMessage { + id?: number + role: 'user' | 'assistant' | 'system' + content: string + created_at?: string +} + +export interface ChatConversation { + id: number + title: string + subject?: string + grade?: string + pinned?: boolean + created_at?: string + updated_at?: string + messages: ChatMessage[] +} + +export interface SendMessageResult { + success: boolean + data: ChatMessage + title?: string + credits?: { balance: number; cost: number; action: string } +} + +export const listConversations = (params?: { keyword?: string }) => + request.get('/api/chat/conversations', { params }) as unknown as Promise + +export const createConversation = (data: { title?: string; subject?: string; grade?: string }) => + request.post('/api/chat/conversations', data) as unknown as Promise + +export const getConversation = (id: number) => + request.get(`/api/chat/conversations/${id}`) as unknown as Promise + +export const renameConversation = (id: number, data: { title: string; pinned?: boolean }) => + request.put(`/api/chat/conversations/${id}`, data) as unknown as Promise + +export const deleteConversation = (id: number) => + request.delete(`/api/chat/conversations/${id}`) as unknown as Promise + +export const sendMessage = (id: number, data: { content: string; subject?: string; grade?: string }) => + request.post(`/api/chat/conversations/${id}/messages`, data) as unknown as Promise diff --git a/frontend/src/api/classroom.ts b/frontend/src/api/classroom.ts new file mode 100644 index 0000000..29572cc --- /dev/null +++ b/frontend/src/api/classroom.ts @@ -0,0 +1,28 @@ +import request from '@/utils/request' + +export const getClassroomActivities = (params?: any) => + request.get('/api/classroom/activities', { params }) as unknown as Promise + +export const getClassroomAnalysis = (id: number) => + request.get(`/api/classroom/activities/${id}/analysis`) as unknown as Promise + +export const exportClassroomAnalysis = (id: number): Promise => + request.get(`/api/classroom/activities/${id}/analysis/export`, { responseType: 'blob' }) as unknown as Promise + +export const exportClassroomResponses = (id: number): Promise => + request.get(`/api/classroom/activities/${id}/responses/export`, { responseType: 'blob' }) as unknown as Promise + +export const getPublicClassroomActivity = (id: number) => + request.get(`/api/classroom/public/activities/${id}`) as unknown as Promise + +export const createClassroomActivity = (data: any) => + request.post('/api/classroom/activities', data) as unknown as Promise + +export const updateClassroomActivity = (id: number, data: any) => + request.put(`/api/classroom/activities/${id}`, data) as unknown as Promise + +export const submitClassroomResponse = (id: number, data: any) => + request.post(`/api/classroom/activities/${id}/responses`, data) as unknown as Promise + +export const deleteClassroomActivity = (id: number) => + request.delete(`/api/classroom/activities/${id}`) diff --git a/frontend/src/api/community.ts b/frontend/src/api/community.ts new file mode 100644 index 0000000..f499bea --- /dev/null +++ b/frontend/src/api/community.ts @@ -0,0 +1,37 @@ +import request from '@/utils/request' + +export const getPosts = (params?: any) => + request.get('/api/community/posts', { params }) + +export const getPostRanking = (params?: any) => + request.get('/api/community/posts/ranking', { params }) + +export const createPost = (data: any) => + request.post('/api/community/posts', data) + +export const getPost = (id: number) => + request.get(`/api/community/posts/${id}`) + +export const updatePost = (id: number, data: any) => + request.put(`/api/community/posts/${id}`, data) + +export const deletePost = (id: number) => + request.delete(`/api/community/posts/${id}`) + +export const likePost = (id: number) => + request.post(`/api/community/posts/${id}/like`) + +export const unlikePost = (id: number) => + request.delete(`/api/community/posts/${id}/like`) + +export const getComments = (postId: number) => + request.get(`/api/community/posts/${postId}/comments`) + +export const createComment = (postId: number, data: { content: string; parent_id?: number }) => + request.post(`/api/community/posts/${postId}/comments`, data) + +export const deleteComment = (postId: number, commentId: number) => + request.delete(`/api/community/posts/${postId}/comments/${commentId}`) + +export const getFavoritePosts = (params?: any) => + request.get('/api/community/posts', { params: { ...(params || {}), scope: 'favorite' } }) diff --git a/frontend/src/api/courseware.ts b/frontend/src/api/courseware.ts new file mode 100644 index 0000000..a97f51a --- /dev/null +++ b/frontend/src/api/courseware.ts @@ -0,0 +1,37 @@ +import request from '@/utils/request' + +export const getCoursewares = (params?: any) => + request.get('/api/coursewares/', { params }) + +export const getCourseware = (id: number) => + request.get(`/api/coursewares/${id}`) + +export const createCourseware = (data: any) => + request.post('/api/coursewares/', data) + +export const updateCourseware = (id: number, data: any) => + request.put(`/api/coursewares/${id}`, data) + +export const remixCourseware = (id: number) => + request.post(`/api/coursewares/${id}/remix`) + +export const createCoursewareShare = (id: number) => + request.post(`/api/coursewares/${id}/share`) as unknown as Promise + +export const getCoursewareShare = (token: string) => + request.get(`/api/coursewares/shares/${token}`) as unknown as Promise + +export const getMyCoursewareShares = (params?: any) => + request.get('/api/coursewares/shares/mine/list', { params }) as unknown as Promise + +export const revokeCoursewareShare = (id: number) => + request.delete(`/api/coursewares/shares/mine/${id}`) + +export const deleteCourseware = (id: number) => + request.delete(`/api/coursewares/${id}`) + +export const exportCoursewareDocx = (id: number): Promise => + request.get(`/api/coursewares/${id}/export-docx`, { responseType: 'blob' }) as unknown as Promise + +export const aiGenerateCourseware = (data: { prompt: string; subject?: string; grade?: string; page_count?: number; aspect_ratio?: string }) => + request.post('/api/coursewares/ai-generate', data) diff --git a/frontend/src/api/essay.ts b/frontend/src/api/essay.ts new file mode 100644 index 0000000..7aab3dc --- /dev/null +++ b/frontend/src/api/essay.ts @@ -0,0 +1,13 @@ +import request from '@/utils/request' + +export const getEssayGrades = (params?: any) => + request.get('/api/essay-grades/', { params }) + +export const getEssayGrade = (id: number) => + request.get(`/api/essay-grades/${id}`) + +export const createEssayGrade = (data: any) => + request.post('/api/essay-grades/', data) + +export const deleteEssayGrade = (id: number) => + request.delete(`/api/essay-grades/${id}`) diff --git a/frontend/src/api/exam.ts b/frontend/src/api/exam.ts new file mode 100644 index 0000000..519f895 --- /dev/null +++ b/frontend/src/api/exam.ts @@ -0,0 +1,22 @@ +import request from '@/utils/request' + +export const getExams = (params?: any) => + request.get('/api/exams/', { params }) + +export const getExam = (id: number) => + request.get(`/api/exams/${id}`) + +export const createExam = (data: any) => + request.post('/api/exams/', data) + +export const updateExam = (id: number, data: any) => + request.put(`/api/exams/${id}`, data) + +export const remixExam = (id: number) => + request.post(`/api/exams/${id}/remix`) + +export const deleteExam = (id: number) => + request.delete(`/api/exams/${id}`) + +export const exportSavedExamDocx = (id: number): Promise => + request.get(`/api/exams/${id}/export-docx`, { responseType: 'blob' }) as unknown as Promise diff --git a/frontend/src/api/exercise.ts b/frontend/src/api/exercise.ts new file mode 100644 index 0000000..78e0450 --- /dev/null +++ b/frontend/src/api/exercise.ts @@ -0,0 +1,40 @@ +import request from '@/utils/request' + +export const getExercises = (params?: any) => + request.get('/api/exercises/', { params }) + +export const getExercise = (id: number) => + request.get(`/api/exercises/${id}`) + +export const createExercise = (data: any) => + request.post('/api/exercises/', data) + +export const updateExercise = (id: number, data: any) => + request.put(`/api/exercises/${id}`, data) + +export const remixExercise = (id: number) => + request.post(`/api/exercises/${id}/remix`) + +export const aiGenerateExercise = (data: { prompt: string; exercise_type?: string; subject?: string; knowledge_points?: string[]; count?: number }) => + request.post('/api/exercises/ai-generate', data) + +export const deleteExercise = (id: number) => + request.delete(`/api/exercises/${id}`) + +export const exportExerciseDocx = (id: number): Promise => + request.get(`/api/exercises/${id}/export-docx`, { responseType: 'blob' }) as unknown as Promise + + +export interface ExerciseAttemptData { + student_name?: string + answers: any[] + score: number + total: number + duration?: number +} + +export const submitExerciseAttempt = (id: number, data: ExerciseAttemptData) => + request.post(`/api/exercises/${id}/attempt`, data) + +export const getExerciseAttempts = (id: number) => + request.get(`/api/exercises/${id}/attempts`) \ No newline at end of file diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts new file mode 100644 index 0000000..5cf037e --- /dev/null +++ b/frontend/src/api/index.ts @@ -0,0 +1,39 @@ +import request from '@/utils/request' + +export const login = (data: { phone: string; password: string }) => + request.post('/api/auth/login', data) + +export const register = (data: { phone: string; password: string; name: string; subject?: string; school?: string }) => + request.post('/api/auth/register', data) + +export const getProfile = () => + request.get('/api/auth/me') + +export const updateProfile = (data: any) => + request.put('/api/auth/me', data) + +export const getProfileStats = () => + request.get('/api/auth/stats') + +export const getCreditBenefits = () => + request.get('/api/auth/credits/benefits') + +export const claimDailyCheckin = () => + request.post('/api/auth/credits/daily-checkin') +export const changePassword = (data: { old_password: string; new_password: string }) => + request.post('/api/auth/change-password', data) +export const uploadAvatar = (file: File) => { + const fd = new FormData() + fd.append('file', file) + return request.post('/api/auth/avatar', fd, { + headers: { 'Content-Type': 'multipart/form-data' }, + }) +} +export const refreshAccessToken = (refreshToken: string) => + request.post('/api/auth/refresh', null, { params: { refresh_token: refreshToken } }) + +export const sendResetCode = (phone: string) => + request.post('/api/auth/send-code', { phone }) + +export const resetPassword = (data: { phone: string; code: string; new_password: string }) => + request.post('/api/auth/reset-password', data) diff --git a/frontend/src/api/lessonPlan.ts b/frontend/src/api/lessonPlan.ts new file mode 100644 index 0000000..faec707 --- /dev/null +++ b/frontend/src/api/lessonPlan.ts @@ -0,0 +1,25 @@ +import request from '@/utils/request' + +export const getLessonPlans = (params?: any) => + request.get('/api/lesson-plans/', { params }) + +export const getLessonPlan = (id: number) => + request.get(`/api/lesson-plans/${id}`) + +export const createLessonPlan = (data: any) => + request.post('/api/lesson-plans/', data) + +export const updateLessonPlan = (id: number, data: any) => + request.put(`/api/lesson-plans/${id}`, data) + +export const remixLessonPlan = (id: number) => + request.post(`/api/lesson-plans/${id}/remix`) + +export const aiGenerateLessonPlan = (data: { title: string; subject?: string; grade?: string; objectives?: string[]; duration?: number; extra_requirements?: string }) => + request.post('/api/lesson-plans/ai-generate', data) + +export const deleteLessonPlan = (id: number) => + request.delete(`/api/lesson-plans/${id}`) + +export const exportLessonPlanDocx = (id: number): Promise => + request.get(`/api/lesson-plans/${id}/export-docx`, { responseType: 'blob' }) as unknown as Promise diff --git a/frontend/src/api/material.ts b/frontend/src/api/material.ts new file mode 100644 index 0000000..f525ba8 --- /dev/null +++ b/frontend/src/api/material.ts @@ -0,0 +1,16 @@ +import request from '@/utils/request' + +export const getMaterials = (params?: any) => + request.get('/api/materials/', { params }) + +export const getMaterial = (id: number) => + request.get(`/api/materials/${id}`) + +export const createMaterial = (data: any) => + request.post('/api/materials/', data) + +export const updateMaterial = (id: number, data: any) => + request.put(`/api/materials/${id}`, data) + +export const deleteMaterial = (id: number) => + request.delete(`/api/materials/${id}`) diff --git a/frontend/src/api/mindmap.ts b/frontend/src/api/mindmap.ts new file mode 100644 index 0000000..3a0fa49 --- /dev/null +++ b/frontend/src/api/mindmap.ts @@ -0,0 +1,30 @@ +import request from '@/utils/request' + +export interface MindMapNode { + title: string + color?: string + children?: MindMapNode[] +} + +export interface MindMapData { + title: string + nodes: MindMapNode[] +} + +export const aiGenerateMindMap = (data: { topic: string; subject?: string; grade?: string }) => + request.post('/api/mind-maps/ai-generate', data) + +export const getMindMaps = (params?: any) => + request.get('/api/mind-maps/', { params }) + +export const getMindMap = (id: number) => + request.get(`/api/mind-maps/${id}`) + +export const createMindMap = (data: { title: string; subject?: string; nodes: MindMapNode[] }) => + request.post('/api/mind-maps/', data) + +export const updateMindMap = (id: number, data: { title?: string; nodes?: MindMapNode[] }) => + request.put(`/api/mind-maps/${id}`, data) + +export const deleteMindMap = (id: number) => + request.delete(`/api/mind-maps/${id}`) diff --git a/frontend/src/api/notification.ts b/frontend/src/api/notification.ts new file mode 100644 index 0000000..dc77f9e --- /dev/null +++ b/frontend/src/api/notification.ts @@ -0,0 +1,28 @@ +import request from '@/utils/request' + +export interface NotificationItem { + id: number + user_id: number + actor_id: number | null + ntype: string + title: string + content: string + link: string + is_read: boolean + created_at: string | null +} + +export const getNotifications = (params?: { unread_only?: boolean; limit?: number }) => + request.get('/api/notifications', { params }) + +export const getUnreadCount = () => + request.get('/api/notifications/unread-count') + +export const markRead = (id: number) => + request.put(`/api/notifications/${id}/read`) + +export const markAllRead = () => + request.put('/api/notifications/read-all') + +export const deleteNotification = (id: number) => + request.delete(`/api/notifications/${id}`) diff --git a/frontend/src/api/resource.ts b/frontend/src/api/resource.ts new file mode 100644 index 0000000..9e494e2 --- /dev/null +++ b/frontend/src/api/resource.ts @@ -0,0 +1,34 @@ +import request from '@/utils/request' + +export const getResources = (params?: any) => + request.get('/api/resources/', { params }) + +export const createResource = (data: any) => + request.post('/api/resources/', data) + +export const publishResource = (data: any) => + request.post('/api/resources/publish', data) + +export const getResource = (id: number) => + request.get(`/api/resources/${id}`) + +export const updateResource = (id: number, data: any) => + request.put(`/api/resources/${id}`, data) + +export const likeResource = (id: number) => + request.post(`/api/resources/${id}/like`) + +export const unlikeResource = (id: number) => + request.delete(`/api/resources/${id}/like`) + +export const getFavoriteResources = (params?: any) => + request.get('/api/resources/favorites/me', { params }) + +export const getMyResources = (params?: any) => + request.get('/api/resources/mine', { params }) + +export const deleteResource = (id: number) => + request.delete(`/api/resources/${id}`) + +export const downloadResourceStat = (id: number) => + request.post(`/api/resources/${id}/download`) diff --git a/frontend/src/api/search.ts b/frontend/src/api/search.ts new file mode 100644 index 0000000..da6daa8 --- /dev/null +++ b/frontend/src/api/search.ts @@ -0,0 +1,4 @@ +import request from '@/utils/request' + +export const globalSearch = (params: { keyword?: string; limit?: number }) => + request.get('/api/search', { params }) as unknown as Promise diff --git a/frontend/src/auto-imports.d.ts b/frontend/src/auto-imports.d.ts new file mode 100644 index 0000000..f6e2bab --- /dev/null +++ b/frontend/src/auto-imports.d.ts @@ -0,0 +1,88 @@ +/* eslint-disable */ +/* prettier-ignore */ +// @ts-nocheck +// noinspection JSUnusedGlobalSymbols +// Generated by unplugin-auto-import +// biome-ignore lint: disable +export {} +declare global { + const EffectScope: typeof import('vue')['EffectScope'] + const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate'] + const computed: typeof import('vue')['computed'] + const createApp: typeof import('vue')['createApp'] + const createPinia: typeof import('pinia')['createPinia'] + const customRef: typeof import('vue')['customRef'] + const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] + const defineComponent: typeof import('vue')['defineComponent'] + const defineStore: typeof import('pinia')['defineStore'] + const effectScope: typeof import('vue')['effectScope'] + const getActivePinia: typeof import('pinia')['getActivePinia'] + const getCurrentInstance: typeof import('vue')['getCurrentInstance'] + const getCurrentScope: typeof import('vue')['getCurrentScope'] + const h: typeof import('vue')['h'] + const inject: typeof import('vue')['inject'] + const isProxy: typeof import('vue')['isProxy'] + const isReactive: typeof import('vue')['isReactive'] + const isReadonly: typeof import('vue')['isReadonly'] + const isRef: typeof import('vue')['isRef'] + const mapActions: typeof import('pinia')['mapActions'] + const mapGetters: typeof import('pinia')['mapGetters'] + const mapState: typeof import('pinia')['mapState'] + const mapStores: typeof import('pinia')['mapStores'] + const mapWritableState: typeof import('pinia')['mapWritableState'] + const markRaw: typeof import('vue')['markRaw'] + const nextTick: typeof import('vue')['nextTick'] + const onActivated: typeof import('vue')['onActivated'] + const onBeforeMount: typeof import('vue')['onBeforeMount'] + const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave'] + const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate'] + const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] + const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] + const onDeactivated: typeof import('vue')['onDeactivated'] + const onErrorCaptured: typeof import('vue')['onErrorCaptured'] + const onMounted: typeof import('vue')['onMounted'] + const onRenderTracked: typeof import('vue')['onRenderTracked'] + const onRenderTriggered: typeof import('vue')['onRenderTriggered'] + const onScopeDispose: typeof import('vue')['onScopeDispose'] + const onServerPrefetch: typeof import('vue')['onServerPrefetch'] + const onUnmounted: typeof import('vue')['onUnmounted'] + const onUpdated: typeof import('vue')['onUpdated'] + const onWatcherCleanup: typeof import('vue')['onWatcherCleanup'] + const provide: typeof import('vue')['provide'] + const reactive: typeof import('vue')['reactive'] + const readonly: typeof import('vue')['readonly'] + const ref: typeof import('vue')['ref'] + const resolveComponent: typeof import('vue')['resolveComponent'] + const setActivePinia: typeof import('pinia')['setActivePinia'] + const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix'] + const shallowReactive: typeof import('vue')['shallowReactive'] + const shallowReadonly: typeof import('vue')['shallowReadonly'] + const shallowRef: typeof import('vue')['shallowRef'] + const storeToRefs: typeof import('pinia')['storeToRefs'] + const toRaw: typeof import('vue')['toRaw'] + const toRef: typeof import('vue')['toRef'] + const toRefs: typeof import('vue')['toRefs'] + const toValue: typeof import('vue')['toValue'] + const triggerRef: typeof import('vue')['triggerRef'] + const unref: typeof import('vue')['unref'] + const useAttrs: typeof import('vue')['useAttrs'] + const useCssModule: typeof import('vue')['useCssModule'] + const useCssVars: typeof import('vue')['useCssVars'] + const useId: typeof import('vue')['useId'] + const useLink: typeof import('vue-router')['useLink'] + const useModel: typeof import('vue')['useModel'] + const useRoute: typeof import('vue-router')['useRoute'] + const useRouter: typeof import('vue-router')['useRouter'] + const useSlots: typeof import('vue')['useSlots'] + const useTemplateRef: typeof import('vue')['useTemplateRef'] + const watch: typeof import('vue')['watch'] + const watchEffect: typeof import('vue')['watchEffect'] + const watchPostEffect: typeof import('vue')['watchPostEffect'] + const watchSyncEffect: typeof import('vue')['watchSyncEffect'] +} +// for type re-export +declare global { + // @ts-ignore + export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue' + import('vue') +} diff --git a/frontend/src/components.d.ts b/frontend/src/components.d.ts new file mode 100644 index 0000000..9d2bb8a --- /dev/null +++ b/frontend/src/components.d.ts @@ -0,0 +1,47 @@ +/* eslint-disable */ +// @ts-nocheck +// Generated by unplugin-vue-components +// Read more: https://github.com/vuejs/core/pull/3399 +export {} + +/* prettier-ignore */ +declare module 'vue' { + export interface GlobalComponents { + AppHeader: typeof import('./components/layout/AppHeader.vue')['default'] + AppLayout: typeof import('./components/layout/AppLayout.vue')['default'] + AppSidebar: typeof import('./components/layout/AppSidebar.vue')['default'] + ElAside: typeof import('element-plus/es')['ElAside'] + ElButton: typeof import('element-plus/es')['ElButton'] + ElColorPicker: typeof import('element-plus/es')['ElColorPicker'] + ElContainer: typeof import('element-plus/es')['ElContainer'] + ElDialog: typeof import('element-plus/es')['ElDialog'] + ElDropdown: typeof import('element-plus/es')['ElDropdown'] + ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] + ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu'] + ElEmpty: typeof import('element-plus/es')['ElEmpty'] + ElForm: typeof import('element-plus/es')['ElForm'] + ElFormItem: typeof import('element-plus/es')['ElFormItem'] + ElHeader: typeof import('element-plus/es')['ElHeader'] + ElIcon: typeof import('element-plus/es')['ElIcon'] + ElInput: typeof import('element-plus/es')['ElInput'] + ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] + ElMain: typeof import('element-plus/es')['ElMain'] + ElOption: typeof import('element-plus/es')['ElOption'] + ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm'] + ElPopover: typeof import('element-plus/es')['ElPopover'] + ElSelect: typeof import('element-plus/es')['ElSelect'] + ElTable: typeof import('element-plus/es')['ElTable'] + ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] + ElTag: typeof import('element-plus/es')['ElTag'] + HtmlPreview: typeof import('./components/common/HtmlPreview.vue')['default'] + MarkdownContent: typeof import('./components/common/MarkdownContent.vue')['default'] + MaterialPicker: typeof import('./components/common/MaterialPicker.vue')['default'] + MindNode: typeof import('./components/common/MindNode.vue')['default'] + RouterLink: typeof import('vue-router')['RouterLink'] + RouterView: typeof import('vue-router')['RouterView'] + TemplateGallery: typeof import('./components/common/TemplateGallery.vue')['default'] + } + export interface ComponentCustomProperties { + vLoading: typeof import('element-plus/es')['ElLoadingDirective'] + } +} diff --git a/frontend/src/components/common/HtmlPreview.vue b/frontend/src/components/common/HtmlPreview.vue new file mode 100644 index 0000000..8059455 --- /dev/null +++ b/frontend/src/components/common/HtmlPreview.vue @@ -0,0 +1,649 @@ +