From 132155c2802a2b3dd8531227eef4b0ac17c4b5b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E9=87=91=E5=AE=9D?= Date: Mon, 1 Jun 2026 17:32:18 +0800 Subject: [PATCH] prepare backend-only fastapi deployment --- .env.example | 9 +- .gitignore | 3 + README.md | 299 ++- backend/README.md | 37 +- backend/app/api/agent.py | 9 +- backend/app/api/auth.py | 22 +- backend/app/core/config.py | 9 +- backend/app/core/context.py | 3 +- backend/app/core/errors.py | 16 +- backend/app/core/user_context.py | 49 +- backend/app/schemas/agent.py | 3 + backend/app/schemas/auth.py | 21 + backend/app/services/external_auth_service.py | 98 +- backend/tests/test_api_contract.py | 49 +- docs/00_development_log.md | 92 - docs/01_functional_scope.md | 71 - docs/02_database_design.md | 87 - docs/02_database_table_dictionary.md | 172 -- docs/03_api_design.md | 887 -------- docs/04_data_collection.md | 97 - docs/05_agent_prompt_design.md | 169 -- docs/06_demo_testing_guide.md | 215 -- docs/07_demo_function_traceability.md | 76 - docs/08_pediatric_case_demo_script.md | 119 -- docs/sql/schema.sql | 516 ----- docs/sql/seed_pediatric_pneumonia.sql | 197 -- frontend/.gitignore | 7 - frontend/README.md | 81 - frontend/index.html | 12 - frontend/package-lock.json | 1853 ----------------- frontend/package.json | 24 - frontend/src/App.vue | 7 - frontend/src/components/AppShell.vue | 81 - frontend/src/components/CaseCard.vue | 37 - frontend/src/components/ChatBubble.vue | 49 - frontend/src/components/ExamOrderPanel.vue | 75 - frontend/src/components/FlowStepper.vue | 46 - frontend/src/components/ReportPanel.vue | 144 -- frontend/src/main.ts | 8 - frontend/src/router/index.ts | 28 - frontend/src/services/apiClient.ts | 263 --- frontend/src/stores/consultationStore.ts | 486 ----- frontend/src/styles/main.css | 1409 ------------- frontend/src/types/api.ts | 241 --- frontend/src/views/CasesView.vue | 217 -- frontend/src/views/ChatView.vue | 178 -- frontend/src/views/HistoryView.vue | 53 - frontend/src/views/HomeView.vue | 184 -- frontend/src/views/ImportCaseView.vue | 192 -- frontend/src/views/LlmTestView.vue | 71 - frontend/src/views/ReportView.vue | 64 - frontend/src/views/SessionView.vue | 103 - frontend/src/views/SubmitView.vue | 114 - frontend/src/vite-env.d.ts | 1 - frontend/tsconfig.json | 21 - frontend/tsconfig.node.json | 10 - frontend/vite.config.ts | 10 - scripts/check_mysql_demo.ps1 | 40 - scripts/init_mysql_demo.ps1 | 95 - 59 files changed, 374 insertions(+), 9155 deletions(-) delete mode 100644 docs/00_development_log.md delete mode 100644 docs/01_functional_scope.md delete mode 100644 docs/02_database_design.md delete mode 100644 docs/02_database_table_dictionary.md delete mode 100644 docs/03_api_design.md delete mode 100644 docs/04_data_collection.md delete mode 100644 docs/05_agent_prompt_design.md delete mode 100644 docs/06_demo_testing_guide.md delete mode 100644 docs/07_demo_function_traceability.md delete mode 100644 docs/08_pediatric_case_demo_script.md delete mode 100644 docs/sql/schema.sql delete mode 100644 docs/sql/seed_pediatric_pneumonia.sql delete mode 100644 frontend/.gitignore delete mode 100644 frontend/README.md delete mode 100644 frontend/index.html delete mode 100644 frontend/package-lock.json delete mode 100644 frontend/package.json delete mode 100644 frontend/src/App.vue delete mode 100644 frontend/src/components/AppShell.vue delete mode 100644 frontend/src/components/CaseCard.vue delete mode 100644 frontend/src/components/ChatBubble.vue delete mode 100644 frontend/src/components/ExamOrderPanel.vue delete mode 100644 frontend/src/components/FlowStepper.vue delete mode 100644 frontend/src/components/ReportPanel.vue delete mode 100644 frontend/src/main.ts delete mode 100644 frontend/src/router/index.ts delete mode 100644 frontend/src/services/apiClient.ts delete mode 100644 frontend/src/stores/consultationStore.ts delete mode 100644 frontend/src/styles/main.css delete mode 100644 frontend/src/types/api.ts delete mode 100644 frontend/src/views/CasesView.vue delete mode 100644 frontend/src/views/ChatView.vue delete mode 100644 frontend/src/views/HistoryView.vue delete mode 100644 frontend/src/views/HomeView.vue delete mode 100644 frontend/src/views/ImportCaseView.vue delete mode 100644 frontend/src/views/LlmTestView.vue delete mode 100644 frontend/src/views/ReportView.vue delete mode 100644 frontend/src/views/SessionView.vue delete mode 100644 frontend/src/views/SubmitView.vue delete mode 100644 frontend/src/vite-env.d.ts delete mode 100644 frontend/tsconfig.json delete mode 100644 frontend/tsconfig.node.json delete mode 100644 frontend/vite.config.ts delete mode 100644 scripts/check_mysql_demo.ps1 delete mode 100644 scripts/init_mysql_demo.ps1 diff --git a/.env.example b/.env.example index c62ab75..20fdc46 100644 --- a/.env.example +++ b/.env.example @@ -2,23 +2,24 @@ APP_NAME=Medical Consultation Agent Demo APP_ENV=local APP_DEBUG=true API_V1_PREFIX=/api/v1 +APP_HOST=127.0.0.1 +APP_PORT=9000 # MySQL # MYSQL_URL keeps the original async style URL for future async SQLAlchemy. # DATABASE_URL is optional; the current sync backend will normalize MYSQL_URL to mysql+pymysql automatically. -MYSQL_URL=mysql+aiomysql://root:password@localhost:3306/medical_consultation_agent?charset=utf8mb4 +MYSQL_URL=mysql+aiomysql://root:@mysql:3306/medical?charset=utf8mb4 +DATABASE_URL=mysql+pymysql://root:@mysql:3306/medical?charset=utf8mb4 # Redis RUNTIME_MEMORY_BACKEND=redis -REDIS_URL=redis://localhost:6379/0 +REDIS_URL=redis://redis:6379/0 RUNTIME_MEMORY_TTL_SECONDS=7200 # Django user center auth for frontend integration -AUTH_VALIDATE_ENABLED=false AUTH_USER_ME_URL=http://192.168.2.76:8000/api/user/users/me/ AUTH_TIMEOUT_SECONDS=5 AUTH_CACHE_TTL_SECONDS=300 -AUTH_ALLOW_DEMO_USER_ID=true # OpenAI-compatible LLM LLM_BASE_URL=https://api.deepseek.com/chat/completions diff --git a/.gitignore b/.gitignore index 59f240c..3875b39 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ venv/ backend/.venv/ # Node / frontend +frontend/ frontend/node_modules/ frontend/dist/ frontend/.npm-cache/ @@ -34,7 +35,9 @@ reports/ uploads/ # Demo-only or temporary files +docs/ demo_frontend/ +scripts/ backend/test*.sql # Editor / OS diff --git a/README.md b/README.md index bda8bfd..8b876c4 100644 --- a/README.md +++ b/README.md @@ -1,215 +1,158 @@ -# 医疗问诊 Agent 第一版 Demo +# 医疗问诊 Agent FastAPI 后端 -这是大系统中的“医疗问诊 Agent”子功能 Demo。系统不做独立注册登录,宿主系统进入本 Agent 时通过请求头传入 `X-User-Id`,后端按该用户标识隔离训练会话、检查申请、诊断治疗提交、AI 评价报告和历史记录。 +本仓库提交内容只包含医疗问诊 Agent 的 FastAPI 后端工程。前端 Demo、开发文档、运行产物和本地环境文件不进入 Git。 -## 当前功能 +## 运行地址 + +后端服务固定按以下地址启动: ```text -病例列表 --> 病例详情 --> 病例 SQL 导入/删除病例 --> 创建训练会话 --> 多轮问诊 Chat / SSE 流式 Chat --> 练习提示 --> 检查/检验申请 --> 完成问诊 --> 提交诊断 --> 提交治疗方案 --> 生成 AI 评价报告 --> 导出 PDF --> 查询历史记录 --> LLM Fast/Reason 测试 +http://127.0.0.1:9000 ``` -## 技术栈 - -| 层级 | 技术 | -|---|---| -| 后端 | FastAPI、SQLAlchemy 2.x、Pydantic | -| 数据库 | MySQL,库名 `medical_consultation_agent` | -| 短期记忆 | Redis 优先,进程内 memory 兜底 | -| LLM | OpenAI-compatible Chat Completions Adapter | -| 前端 | Vue 3、Vite、TypeScript、Pinia、Vue Router | -| 报告 | 本地 PDF 文件生成 | -| 提示词 | Markdown 模板 + Agent 运行时拼接 | - -## 核心数据表 - -当前功能依赖以下表: +Swagger: ```text -case_base -traditional_case -teaching_case -scoring_rule -case_exam_item -training_session -training_order -training_submission -training_record -prompt_templates -knowledge_sources -knowledge_documents -knowledge_chunks -user_learning_profiles -audit_logs +http://127.0.0.1:9000/docs ``` -旧表已不参与运行。 - -## 启动后端 +启动命令: ```powershell -cd D:\Code\newfounder\medical-consultation-agent\backend +cd backend .\.venv\Scripts\activate -uvicorn app.main:app --reload --host 127.0.0.1 --port 8000 +uvicorn app.main:app --host 127.0.0.1 --port 9000 ``` -接口文档: +## 服务依赖 + +MySQL 使用容器或内网服务名 `mysql`: ```text -http://127.0.0.1:8000/docs +host=mysql +port=3306 +database=medical +user=root ``` +Redis 使用容器或内网服务名 `redis`: + +```text +host=redis +port=6379 +``` + +后端通过 `.env` 读取连接串。真实 `.env` 不提交到 Git,仓库只提供 `.env.example`。 + +## 环境变量 + +复制示例文件: + +```powershell +copy .env.example .env +``` + +关键配置: + +```env +APP_HOST=127.0.0.1 +APP_PORT=9000 +MYSQL_URL=mysql+aiomysql://root:@mysql:3306/medical?charset=utf8mb4 +DATABASE_URL=mysql+pymysql://root:@mysql:3306/medical?charset=utf8mb4 +RUNTIME_MEMORY_BACKEND=redis +REDIS_URL=redis://redis:6379/0 +``` + +`` 由部署环境注入。不要把真实数据库密码、LLM Key 或 access token 提交到 Git。 + +## 用户认证 + +医疗问诊 Agent 不做登录注册。正式联调时,前端携带宿主系统 access token: + +```http +Authorization: Bearer +X-Entry-Scene: mac_vue_dev +``` + +后端调用 Django 用户中心: + +```text +GET /api/user/users/me/ +``` + +Django 返回 200 后,后端使用返回的 `id` 作为内部 `user_id`,用于会话、训练记录、评价报告和历史记录隔离。 + +快速验证: + +```bash +curl -X GET "http://127.0.0.1:9000/api/v1/auth/me" \ + -H "Authorization: Bearer " \ + -H "X-Entry-Scene: mac_vue_dev" +``` + +成功条件: + +```json +{ + "code": "OK", + "data": { + "source": "django_user_center" + } +} +``` + +## 后端功能 + +- 病例列表与病例详情 +- 病例 SQL 安全导入与删除 +- 训练会话创建 +- 多轮问诊与 SSE 流式回复 +- 练习提示 +- 检查/检验申请 +- 诊断与治疗提交 +- AI 评价报告生成 +- PDF 报告导出 +- 历史评价查询 +- LLM Fast/Reason 测试 + ## 初始化数据库 ```powershell -cd D:\Code\newfounder\medical-consultation-agent\backend +cd backend .\.venv\Scripts\python.exe scripts\migrate_to_new_schema.py ``` -清理旧表: +## 测试 ```powershell -.\.venv\Scripts\python.exe scripts\drop_legacy_tables.py -``` - -## 导入接口解析后的病例 SQL - -后端提供安全导入能力,只解析源 SQL 中的 `case_base`、`traditional_case`、`teaching_case`、`scoring_rule` 四类病例数据,不执行源 SQL 中的删表、建表或锁表语句。 - -命令行预检: - -```powershell -cd D:\Code\newfounder\medical-consultation-agent\backend -.\.venv\Scripts\python.exe scripts\import_source_case_sql.py "C:\path\to\case.sql" -``` - -确认预检通过后写入: - -```powershell -.\.venv\Scripts\python.exe scripts\import_source_case_sql.py "C:\path\to\case.sql" --apply -``` - -前端导入页: - -```text -http://127.0.0.1:5173/#/import -``` - -## 启动前端 - -```powershell -cd D:\Code\newfounder\medical-consultation-agent\frontend -npm.cmd install -npm.cmd run dev -- --host 127.0.0.1 --port 5173 -``` - -访问: - -```text -http://127.0.0.1:5173 -``` - -## 环境变量 - -真实 `.env` 只保留在本地,不提交 API Key。 - -| 变量 | 说明 | -|---|---| -| `MYSQL_URL` | MySQL 连接串 | -| `REDIS_URL` | Redis 地址 | -| `RUNTIME_MEMORY_BACKEND` | `redis` 或 `memory` | -| `LLM_BASE_URL` | OpenAI-compatible `chat/completions` URL | -| `LLM_API_KEY` | 模型服务 API Key | -| `LLM_MODEL` | 默认模型 | -| `LLM_FAST_MODEL` | 问诊、提示、快速测试模型 | -| `LLM_REASON_MODEL` | Reason 测试模型 | -| `LLM_MOCK_ENABLED` | 是否强制 mock | -| `LLM_FALLBACK_TO_MOCK` | 真实模型失败时是否回退 mock | -| `AUTH_VALIDATE_ENABLED` | 是否启用 Django 用户中心鉴权 | -| `AUTH_USER_ME_URL` | Django 当前用户接口,例如 `http://192.168.2.76:8000/api/user/users/me/` | -| `AUTH_TIMEOUT_SECONDS` | 调用用户中心超时时间 | -| `AUTH_CACHE_TTL_SECONDS` | 用户信息短期缓存时间 | -| `AUTH_ALLOW_DEMO_USER_ID` | 外部鉴权开启时是否允许 `X-User-Id` Demo 兜底 | - -## Django 用户中心联调 - -正式联调时,前端进入医疗问诊 Agent 后先调用: - -```text -GET /api/v1/auth/me -``` - -前端需要携带宿主系统登录态: - -```http -Authorization: Bearer -X-Entry-Scene: mac_vue_dev -``` - -如果宿主系统使用 Cookie 登录,则前端需要开启 `withCredentials`,后端会把 Cookie 转发到: - -```text -http://192.168.2.76:8000/api/user/users/me/ -``` - -FastAPI 会从 Django 返回值中提取 `user_id`,后续病例训练、会话、评价和历史记录都按该 `user_id` 隔离。`X-User-Id` 只作为本地 Demo 兼容方式。 - -## 验证命令 - -后端: - -```powershell -cd D:\Code\newfounder\medical-consultation-agent\backend +cd backend .\.venv\Scripts\python.exe -m compileall app scripts tests .\.venv\Scripts\python.exe tests\test_core_logic.py .\.venv\Scripts\python.exe tests\test_api_contract.py .\.venv\Scripts\python.exe tests\test_demo_flow.py ``` -前端: +## Git 提交范围 -```powershell -cd D:\Code\newfounder\medical-consultation-agent\frontend -npm.cmd run build +Git 仓库只跟踪: + +```text +backend/ +README.md +.env.example +.gitignore +.gitattributes ``` -## 文档入口 +不提交: -| 文档 | 内容 | -|---|---| -| [docs/00_development_log.md](docs/00_development_log.md) | 开发过程和变更记录 | -| [docs/01_functional_scope.md](docs/01_functional_scope.md) | 当前功能范围 | -| [docs/02_database_design.md](docs/02_database_design.md) | 数据库总体设计 | -| [docs/02_database_table_dictionary.md](docs/02_database_table_dictionary.md) | 表字段字典 | -| [docs/03_api_design.md](docs/03_api_design.md) | 前端 API 对接文档 | -| [docs/04_data_collection.md](docs/04_data_collection.md) | 数据采集和存储边界 | -| [docs/05_agent_prompt_design.md](docs/05_agent_prompt_design.md) | Agent 和提示词模板调用说明 | -| [docs/06_demo_testing_guide.md](docs/06_demo_testing_guide.md) | 前端测试指南 | -| [docs/07_demo_function_traceability.md](docs/07_demo_function_traceability.md) | 功能到代码和数据表追踪 | -| [docs/08_pediatric_case_demo_script.md](docs/08_pediatric_case_demo_script.md) | 儿科病例演示脚本 | - -## Git 管理说明 - -仓库上传范围包含 `backend`、`frontend`、`docs`、`scripts` 和项目说明文件。 - -不上传: - -- `demo_frontend/` -- `.env` -- `backend/.venv/` -- `frontend/node_modules/` -- `frontend/dist/` -- `storage/` -- 本地 PDF、日志、数据库文件和临时 SQL 文件 +```text +frontend/ +docs/ +demo_frontend/ +scripts/ +.env +storage/ +backend/.venv/ +本地报告、日志、数据库文件和临时 SQL 文件 +``` diff --git a/backend/README.md b/backend/README.md index f378dee..5f1bc9a 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,23 +1,42 @@ # Backend -医疗问诊 Agent 第一版 Demo 后端工程。 +医疗问诊 Agent FastAPI 后端工程。 -## 启动流程 +## 启动 -```bash +```powershell cd backend python -m venv .venv -.venv\Scripts\activate +.\.venv\Scripts\activate pip install -r requirements.txt copy ..\.env.example ..\.env -python -m scripts.init_demo_db -uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 +uvicorn app.main:app --host 127.0.0.1 --port 9000 ``` +Swagger: + +```text +http://127.0.0.1:9000/docs +``` + +## 配置 + +后端读取项目根目录 `.env`。 + +```env +DATABASE_URL=mysql+pymysql://root:@mysql:3306/medical?charset=utf8mb4 +MYSQL_URL=mysql+aiomysql://root:@mysql:3306/medical?charset=utf8mb4 +REDIS_URL=redis://redis:6379/0 +``` + +真实密码和 API Key 只写入部署环境或本地 `.env`,不提交 Git。 + ## 核心约束 -- 所有业务接口通过 `X-User-Id` 做用户隔离。 +- 用户身份只来自 `Authorization: Bearer `。 +- 后端转发 token 到 Django 用户中心 `/api/user/users/me/`。 +- Django 返回的 `id` 是本系统内部用户隔离字段。 - 问诊消息进入短期 memory,不作为长期历史保存。 - 检查检验结果只从数据库读取。 -- 完整训练结束后只保存评价记录、PDF 导出记录、学习档案和审计日志。 -- DeepSeek 调用统一经过 `agents/llm_adapter.py`。 +- 完整训练结束后只保存评价记录、PDF 路径、学习档案和审计日志。 +- LLM 调用统一经过 `app/agents/llm_adapter.py`。 diff --git a/backend/app/api/agent.py b/backend/app/api/agent.py index ad92965..13e8fbb 100644 --- a/backend/app/api/agent.py +++ b/backend/app/api/agent.py @@ -18,7 +18,14 @@ def hello(ctx: UserContext = Depends(get_user_context), db: Session = Depends(ge db.commit() return ok( AgentHelloResponse( - user=AgentHelloUser(user_id=ctx.user_id, tenant_id=ctx.tenant_id, role=ctx.role), + user=AgentHelloUser( + user_id=ctx.user_id, + tenant_id=ctx.tenant_id, + role=ctx.role, + source=ctx.auth_source, + username=ctx.username, + display_name=ctx.display_name, + ), features=settings.as_public_dict(), ) ) diff --git a/backend/app/api/auth.py b/backend/app/api/auth.py index 4a33c23..7d868fe 100644 --- a/backend/app/api/auth.py +++ b/backend/app/api/auth.py @@ -9,7 +9,8 @@ router = APIRouter() @router.get("/me", response_model=ApiResponse[AuthMeResponse]) async def auth_me(ctx: UserContext = Depends(get_user_context)): - """当前用户:返回经 Django 用户中心或 Demo Header 标准化后的用户信息。""" + """当前用户:转发 Authorization 到 Django 用户中心,并返回标准化后的用户信息。""" + profile = ctx.profile or {} return ok( AuthMeResponse( user_id=ctx.user_id, @@ -18,5 +19,24 @@ async def auth_me(ctx: UserContext = Depends(get_user_context)): display_name=ctx.display_name, tenant_id=ctx.tenant_id, role=ctx.role, + phone=profile.get("phone"), + avatar=profile.get("avatar"), + gender=profile.get("gender"), + institution=profile.get("institution"), + institution_name=profile.get("institution_name"), + department=profile.get("department"), + department_name=profile.get("department_name"), + title_name=profile.get("title_name"), + major=profile.get("major"), + training_stage=profile.get("training_stage"), + learning_target=profile.get("learning_target"), + competency_profile=profile.get("competency_profile"), + weak_dimensions=profile.get("weak_dimensions"), + strong_dimensions=profile.get("strong_dimensions"), + ai_preference=profile.get("ai_preference"), + total_training_count=profile.get("total_training_count"), + total_case_count=profile.get("total_case_count"), + current_level=profile.get("current_level"), + status=profile.get("status"), ) ) diff --git a/backend/app/core/config.py b/backend/app/core/config.py index abcf5e6..a5060e9 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -82,12 +82,11 @@ class Settings(BaseModel): report_storage_dir: str = Field(default_factory=lambda: os.getenv("REPORT_STORAGE_DIR", "./storage/reports")) runtime_memory_ttl_seconds: int = Field(default_factory=lambda: int(os.getenv("RUNTIME_MEMORY_TTL_SECONDS", "7200"))) runtime_memory_backend: str = Field(default_factory=lambda: os.getenv("RUNTIME_MEMORY_BACKEND", "memory")) - redis_url: str = Field(default_factory=lambda: os.getenv("REDIS_URL", "redis://127.0.0.1:6379/0")) - auth_validate_enabled: bool = Field(default_factory=lambda: os.getenv("AUTH_VALIDATE_ENABLED", "false").lower() == "true") + redis_url: str = Field(default_factory=lambda: os.getenv("REDIS_URL", "redis://redis:6379/0")) + auth_validate_enabled: bool = Field(default_factory=lambda: os.getenv("AUTH_VALIDATE_ENABLED", "true").lower() == "true") auth_user_me_url: str = Field(default_factory=lambda: os.getenv("AUTH_USER_ME_URL", "")) auth_timeout_seconds: int = Field(default_factory=lambda: int(os.getenv("AUTH_TIMEOUT_SECONDS", "5"))) auth_cache_ttl_seconds: int = Field(default_factory=lambda: int(os.getenv("AUTH_CACHE_TTL_SECONDS", "300"))) - auth_allow_demo_user_id: bool = Field(default_factory=lambda: os.getenv("AUTH_ALLOW_DEMO_USER_ID", "true").lower() == "true") def as_public_dict(self) -> dict[str, Any]: """配置展示:返回允许暴露给 Demo 前端的功能开关。""" @@ -107,8 +106,8 @@ class Settings(BaseModel): "llm_reasoning_effort": self.llm_reasoning_effort, "llm_fast_max_tokens": self.llm_fast_max_tokens, "runtime_memory_backend": self.runtime_memory_backend, - "auth_validate_enabled": self.auth_validate_enabled, - "auth_source": "django_user_center" if self.auth_validate_enabled else "demo_header", + "auth_validate_enabled": True, + "auth_source": "django_user_center", } diff --git a/backend/app/core/context.py b/backend/app/core/context.py index 912df82..09ebb80 100644 --- a/backend/app/core/context.py +++ b/backend/app/core/context.py @@ -15,4 +15,5 @@ class UserContext: user_agent: str | None = None username: str | None = None display_name: str | None = None - auth_source: str = "demo_header" + auth_source: str = "django_user_center" + profile: dict | None = None diff --git a/backend/app/core/errors.py b/backend/app/core/errors.py index 572538e..d5ce954 100644 --- a/backend/app/core/errors.py +++ b/backend/app/core/errors.py @@ -16,10 +16,10 @@ def register_exception_handlers(app: FastAPI) -> None: @app.exception_handler(AppError) async def handle_app_error(request: Request, exc: AppError) -> JSONResponse: logger.warning( - "business_error code=%s path=%s user_id=%s", + "business_error code=%s path=%s request_id=%s", exc.code, request.url.path, - request.headers.get("X-User-Id"), + request.headers.get("X-Request-Id"), ) return JSONResponse( status_code=exc.status_code, @@ -29,9 +29,9 @@ def register_exception_handlers(app: FastAPI) -> None: @app.exception_handler(RequestValidationError) async def handle_validation_error(request: Request, exc: RequestValidationError) -> JSONResponse: logger.warning( - "validation_error path=%s user_id=%s errors=%s", + "validation_error path=%s request_id=%s errors=%s", request.url.path, - request.headers.get("X-User-Id"), + request.headers.get("X-Request-Id"), exc.errors(), ) return JSONResponse( @@ -42,9 +42,9 @@ def register_exception_handlers(app: FastAPI) -> None: @app.exception_handler(SQLAlchemyError) async def handle_database_error(request: Request, exc: SQLAlchemyError) -> JSONResponse: logger.exception( - "database_error path=%s user_id=%s", + "database_error path=%s request_id=%s", request.url.path, - request.headers.get("X-User-Id"), + request.headers.get("X-Request-Id"), ) return JSONResponse( status_code=500, @@ -54,9 +54,9 @@ def register_exception_handlers(app: FastAPI) -> None: @app.exception_handler(Exception) async def handle_unexpected_error(request: Request, exc: Exception) -> JSONResponse: logger.exception( - "unexpected_error path=%s user_id=%s", + "unexpected_error path=%s request_id=%s", request.url.path, - request.headers.get("X-User-Id"), + request.headers.get("X-Request-Id"), ) return JSONResponse( status_code=500, diff --git a/backend/app/core/user_context.py b/backend/app/core/user_context.py index 6c1ee50..aa4683a 100644 --- a/backend/app/core/user_context.py +++ b/backend/app/core/user_context.py @@ -1,51 +1,34 @@ -from fastapi import Header, Request +from fastapi import Header, Request, Security +from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer -from app.core.config import settings from app.core.context import UserContext from app.core.exceptions import AppError from app.services.external_auth_service import ExternalAuthService +bearer_scheme = HTTPBearer(auto_error=False, description="Django 用户中心 access token") + async def get_user_context( request: Request, - x_user_id: str | None = Header(default=None, alias="X-User-Id"), - x_tenant_id: str | None = Header(default=None, alias="X-Tenant-Id"), - x_user_role: str | None = Header(default=None, alias="X-User-Role"), - x_class_id: str | None = Header(default=None, alias="X-Class-Id"), + credentials: HTTPAuthorizationCredentials | None = Security(bearer_scheme), x_entry_scene: str | None = Header(default=None, alias="X-Entry-Scene"), x_request_id: str | None = Header(default=None, alias="X-Request-Id"), ) -> UserContext: - """用户校验:正式联调优先调用 Django 用户中心,Demo 模式兼容 X-User-Id。""" - if settings.auth_validate_enabled and (request.headers.get("Authorization") or request.headers.get("Cookie")): - user = await ExternalAuthService().authenticate(request) - return UserContext( - user_id=user.user_id, - tenant_id=user.tenant_id or x_tenant_id, - role=user.role or x_user_role, - class_id=x_class_id, - entry_scene=x_entry_scene, - request_id=x_request_id, - ip_address=request.client.host if request.client else None, - user_agent=request.headers.get("User-Agent"), - username=user.username, - display_name=user.display_name, - auth_source=user.source, - ) - - if settings.auth_validate_enabled and not settings.auth_allow_demo_user_id: - raise AppError("AUTH_CREDENTIAL_REQUIRED", "Authorization or Cookie is required", 401) - - if not x_user_id or not x_user_id.strip(): - raise AppError("USER_ID_REQUIRED", "X-User-Id header is required", 401) + """用户校验:只接受宿主系统 access token,并转发 Django 用户中心 `/me` 获取真实用户。""" + if not credentials or not credentials.credentials.strip(): + raise AppError("AUTH_CREDENTIAL_REQUIRED", "Authorization header is required", 401) + user = await ExternalAuthService().authenticate(request) return UserContext( - user_id=x_user_id.strip(), - tenant_id=x_tenant_id, - role=x_user_role, - class_id=x_class_id, + user_id=user.user_id, + tenant_id=user.tenant_id, + role=user.role, entry_scene=x_entry_scene, request_id=x_request_id, ip_address=request.client.host if request.client else None, user_agent=request.headers.get("User-Agent"), - auth_source="demo_header", + username=user.username, + display_name=user.display_name, + auth_source=user.source, + profile=user.profile, ) diff --git a/backend/app/schemas/agent.py b/backend/app/schemas/agent.py index 8a9ec57..0b35317 100644 --- a/backend/app/schemas/agent.py +++ b/backend/app/schemas/agent.py @@ -7,6 +7,9 @@ class AgentHelloUser(BaseModel): user_id: str tenant_id: str | None = None role: str | None = None + source: str | None = None + username: str | None = None + display_name: str | None = None class AgentHelloResponse(BaseModel): diff --git a/backend/app/schemas/auth.py b/backend/app/schemas/auth.py index ebccf45..5962818 100644 --- a/backend/app/schemas/auth.py +++ b/backend/app/schemas/auth.py @@ -1,3 +1,5 @@ +from typing import Any + from pydantic import BaseModel @@ -10,3 +12,22 @@ class AuthMeResponse(BaseModel): display_name: str | None = None tenant_id: str | None = None role: str | None = None + phone: str | None = None + avatar: str | None = None + gender: int | None = None + institution: int | None = None + institution_name: str | None = None + department: int | None = None + department_name: str | None = None + title_name: str | None = None + major: str | None = None + training_stage: str | None = None + learning_target: str | None = None + competency_profile: dict[str, Any] | None = None + weak_dimensions: list[Any] | None = None + strong_dimensions: list[Any] | None = None + ai_preference: dict[str, Any] | None = None + total_training_count: int | None = None + total_case_count: int | None = None + current_level: str | None = None + status: int | None = None diff --git a/backend/app/services/external_auth_service.py b/backend/app/services/external_auth_service.py index 83a4000..4807c2a 100644 --- a/backend/app/services/external_auth_service.py +++ b/backend/app/services/external_auth_service.py @@ -22,6 +22,20 @@ class AuthenticatedUser: display_name: str | None = None tenant_id: str | None = None role: str | None = None + phone: str | None = None + avatar: str | None = None + gender: int | None = None + institution_id: int | None = None + institution_name: str | None = None + department_id: int | None = None + department_name: str | None = None + title_name: str | None = None + major: str | None = None + training_stage: str | None = None + learning_target: str | None = None + current_level: str | None = None + status: int | None = None + profile: dict[str, Any] | None = None class ExternalAuthService: @@ -36,7 +50,7 @@ class ExternalAuthService: outbound_headers = self._build_forward_headers(request) if not outbound_headers: - raise AppError("AUTH_CREDENTIAL_REQUIRED", "Authorization or Cookie is required", 401) + raise AppError("AUTH_CREDENTIAL_REQUIRED", "Authorization header is required", 401) cache_key = self._cache_key(outbound_headers) cached = self._read_cache(cache_key) @@ -64,16 +78,14 @@ class ExternalAuthService: return user def _build_forward_headers(self, request: Request) -> dict[str, str]: - """认证转发:只透传鉴权必要 Header,避免把无关内部头转发给用户中心。""" + """认证转发:只透传 Bearer token,避免把无关内部头转发给用户中心。""" headers: dict[str, str] = {} authorization = request.headers.get("Authorization") - cookie = request.headers.get("Cookie") if authorization: + authorization = authorization.strip() + if authorization and " " not in authorization: + authorization = f"Bearer {authorization}" headers["Authorization"] = authorization - if cookie: - headers["Cookie"] = cookie - if request.headers.get("X-CSRFToken"): - headers["X-CSRFToken"] = request.headers["X-CSRFToken"] return headers def _parse_user(self, payload: dict[str, Any]) -> AuthenticatedUser: @@ -82,18 +94,69 @@ class ExternalAuthService: user_id = self._first_present(data, ["id", "user_id", "uid", "pk", "uuid"]) if user_id is None: raise AppError("AUTH_USER_PARSE_FAILED", "auth user id is missing", 502) + status = self._to_int(data.get("status")) + if status == 0: + raise AppError("AUTH_USER_DISABLED", "current user is disabled", 403) username = self._first_present(data, ["username", "account", "mobile", "phone"]) display_name = self._first_present(data, ["display_name", "name", "nickname", "real_name"]) - role = self._first_present(data, ["role", "user_role"]) - tenant_id = self._first_present(data, ["tenant_id", "org_id", "organization_id"]) + role = self._first_present(data, ["role_type", "role", "user_role"]) + institution_id = self._to_int(data.get("institution")) + tenant_id = str(institution_id) if institution_id is not None else None + profile = self._build_profile(data) return AuthenticatedUser( user_id=str(user_id), username=str(username) if username is not None else None, display_name=str(display_name) if display_name is not None else None, role=str(role) if role is not None else None, tenant_id=str(tenant_id) if tenant_id is not None else None, + phone=self._to_str(data.get("phone")), + avatar=self._to_str(data.get("avatar")), + gender=self._to_int(data.get("gender")), + institution_id=institution_id, + institution_name=self._to_str(data.get("institution_name")), + department_id=self._to_int(data.get("department")), + department_name=self._to_str(data.get("department_name")), + title_name=self._to_str(data.get("title_name")), + major=self._to_str(data.get("major")), + training_stage=self._to_str(data.get("training_stage")), + learning_target=self._to_str(data.get("learning_target")), + current_level=self._to_str(data.get("current_level")), + status=status, + profile=profile, ) + def _build_profile(self, data: dict[str, Any]) -> dict[str, Any]: + """用户画像:按 Django `/me` 字段白名单保留学习画像,供前端和后续 Agent 个性化使用。""" + keys = [ + "id", + "username", + "real_name", + "phone", + "avatar", + "gender", + "role_type", + "institution", + "institution_name", + "department", + "department_name", + "title_name", + "major", + "training_stage", + "learning_target", + "competency_profile", + "weak_dimensions", + "strong_dimensions", + "ai_preference", + "total_training_count", + "total_case_count", + "current_level", + "status", + "last_login_time", + "created_at", + "updated_at", + ] + return {key: data.get(key) for key in keys if key in data} + @staticmethod def _first_present(data: dict[str, Any], keys: list[str]) -> Any: """用户解析:按优先级读取第一个非空字段。""" @@ -103,6 +166,23 @@ class ExternalAuthService: return value return None + @staticmethod + def _to_int(value: Any) -> int | None: + """用户解析:把 Django 返回的数字字段稳定转成 int,空值保持 None。""" + if value is None or value == "": + return None + try: + return int(value) + except (TypeError, ValueError): + return None + + @staticmethod + def _to_str(value: Any) -> str | None: + """用户解析:把可展示字段转成字符串,空值保持 None。""" + if value is None or value == "": + return None + return str(value) + @staticmethod def _cache_key(headers: dict[str, str]) -> str: """认证缓存:基于鉴权凭证生成不可逆缓存键,避免保存明文 token。""" diff --git a/backend/tests/test_api_contract.py b/backend/tests/test_api_contract.py index 2877b34..f3390a1 100644 --- a/backend/tests/test_api_contract.py +++ b/backend/tests/test_api_contract.py @@ -6,6 +6,7 @@ from pathlib import Path os.environ.setdefault("DATABASE_URL", "sqlite:///./storage/test_api_contract.db") os.environ.setdefault("RUNTIME_MEMORY_BACKEND", "memory") os.environ.setdefault("LLM_MOCK_ENABLED", "true") +os.environ.setdefault("AUTH_USER_ME_URL", "http://django-user-center.test/api/user/users/me/") sys.path.insert(0, str(Path(__file__).resolve().parents[1])) Path("storage").mkdir(exist_ok=True) @@ -23,18 +24,47 @@ def run_api_contract_tests() -> None: return from app.main import app + from app.services.external_auth_service import AuthenticatedUser, ExternalAuthService from app.db.session import SessionLocal from app.models.source_case import CaseBase, CaseExamItem, ScoringRule, TraditionalCase from app.repositories.case_repository import CaseRepository from scripts.init_demo_db import init_database + async def fake_authenticate(self, request): # noqa: ARG001 + """测试认证:模拟 Django `/me` 返回 200 后的标准用户解析结果。""" + authorization = request.headers.get("Authorization") + if not authorization: + from app.core.exceptions import AppError + + raise AppError("AUTH_CREDENTIAL_REQUIRED", "Authorization header is required", 401) + user_id = "api_user_002" if "api_user_002_token" in authorization else "api_user_001" + return AuthenticatedUser( + user_id=user_id, + username=f"{user_id}_name", + display_name="Swagger测试", + role="student", + tenant_id="1", + status=1, + profile={ + "id": user_id, + "username": f"{user_id}_name", + "real_name": "Swagger测试", + "role_type": "student", + "institution": 1, + "institution_name": "测试机构", + "status": 1, + }, + ) + + ExternalAuthService.authenticate = fake_authenticate + init_database() client = TestClient(app) - headers = {"X-User-Id": "api_user_001", "X-Entry-Scene": "api_test"} + headers = {"Authorization": "Bearer api_user_001_token", "X-Entry-Scene": "api_test"} missing_user = client.get("/api/v1/agent/hello") assert missing_user.status_code == 401 - assert missing_user.json()["code"] == "USER_ID_REQUIRED" + assert missing_user.json()["code"] == "AUTH_CREDENTIAL_REQUIRED" hello = client.get("/api/v1/agent/hello", headers=headers) assert hello.status_code == 200 @@ -43,7 +73,15 @@ def run_api_contract_tests() -> None: auth_me = client.get("/api/v1/auth/me", headers=headers) assert auth_me.status_code == 200 assert auth_me.json()["data"]["user_id"] == "api_user_001" - assert auth_me.json()["data"]["source"] == "demo_header" + assert auth_me.json()["data"]["source"] == "django_user_center" + assert auth_me.json()["data"]["display_name"] == "Swagger测试" + + openapi = client.get("/openapi.json") + assert openapi.status_code == 200 + openapi_payload = openapi.json() + auth_me_operation = openapi_payload["paths"]["/api/v1/auth/me"]["get"] + assert any("HTTPBearer" in item for item in auth_me_operation.get("security", [])) + assert "HTTPBearer" in openapi_payload["components"]["securitySchemes"] cases = client.get("/api/v1/cases", headers=headers) assert cases.status_code == 200 @@ -57,7 +95,10 @@ def run_api_contract_tests() -> None: assert created.status_code == 200 session_id = created.json()["data"]["session_id"] - cross_user = client.get(f"/api/v1/sessions/{session_id}/order-items", headers={"X-User-Id": "api_user_002"}) + cross_user = client.get( + f"/api/v1/sessions/{session_id}/order-items", + headers={"Authorization": "Bearer api_user_002_token", "X-Entry-Scene": "api_test"}, + ) assert cross_user.status_code == 404 assert cross_user.json()["code"] == "SESSION_NOT_FOUND" diff --git a/docs/00_development_log.md b/docs/00_development_log.md deleted file mode 100644 index 15d14f9..0000000 --- a/docs/00_development_log.md +++ /dev/null @@ -1,92 +0,0 @@ -# 开发过程记录 - -本文档只记录开发过程和阶段性变更。其他设计文档只描述当前确定状态。 - -## 第一版 Demo 已完成 - -已完成核心问诊训练闭环: - -```text -病例列表 -> 病例详情 -> 创建训练会话 -> 多轮问诊 -> 检查申请 --> 完成问诊 -> 提交诊断 -> 提交治疗 -> AI 评价 -> PDF 导出 -> 历史记录 -``` - -## 数据库结构调整记录 - -当前数据库以源库病例结构为基础: - -- `case_base`:病例主表。 -- `traditional_case`:练习模式病例扩展。 -- `teaching_case`:教学互动模式病例扩展。 -- `scoring_rule`:病例评分规则。 - -业务运行新增表: - -- `case_exam_item` -- `training_session` -- `training_order` -- `training_submission` - -长期评价记录使用: - -- `training_record` - -旧表已删除: - -```text -cases -case_exam_items -training_sessions -session_orders -session_submissions -session_runtime_messages -evaluation_records -evaluation_report_exports -rubric_templates -``` - -旧 ORM 模型文件已删除: - -```text -backend/app/models/case.py -backend/app/models/evaluation.py -``` - -## 当前验证结果 - -已通过: - -- 后端编译:`python -m compileall app scripts tests` -- 核心逻辑测试:`tests/test_core_logic.py` -- API 合约测试:`tests/test_api_contract.py` -- Demo 闭环测试:`tests/test_demo_flow.py` -- MySQL 新表链路测试 -- 删除旧表后的 MySQL 闭环复测 -- 前端构建:`npm.cmd run build` - -最终数据库检查: - -```text -old_remaining = [] -case_base = 1 -traditional_case = 1 -teaching_case = 1 -scoring_rule = 5 -case_exam_item = 5 -training_session = 6+ -training_order = 6+ -training_submission = 6+ -training_record = 7+ -``` - -`training_*` 和 `training_record` 数量会随测试运行增加。 - -## 病例 SQL 上传导入功能 - -本轮新增前端和后端联调入口,用于接收接口解析后的病例 SQL 文件: - -- 后端新增 `POST /api/v1/imports/case-sql/preview`,只解析校验,不写数据库。 -- 后端新增 `POST /api/v1/imports/case-sql/apply`,确认后写入 `case_base`、`traditional_case`、`teaching_case`、`scoring_rule`。 -- 导入器继续沿用安全解析逻辑,不执行源 SQL 中的 DDL/DML。 -- 前端新增 `#/import` 页面,支持选择 SQL、预检、确认导入和刷新病例库。 -- 导入成功后,新增病例会出现在病例列表中,并可继续创建训练会话。 diff --git a/docs/01_functional_scope.md b/docs/01_functional_scope.md deleted file mode 100644 index 2cc1205..0000000 --- a/docs/01_functional_scope.md +++ /dev/null @@ -1,71 +0,0 @@ -# 第一版 Demo 功能范围 - -## 项目定位 - -医疗问诊 Agent 是大系统中的子功能。宿主系统进入时传入 `X-User-Id`,Agent 内部基于该值做会话隔离、训练记录、评价报告和学习档案沉淀。 - -## 已实现功能 - -| 功能 | 说明 | -|---|---| -| 入口连接 | 校验 `X-User-Id`,返回当前用户上下文和 Demo 能力开关 | -| 病例列表 | 从 `case_base` 读取已发布病例 | -| 病例详情 | 展示训练入口信息,不暴露标准答案、隐藏信息和评分细则 | -| 创建训练会话 | 写入 `training_session`,初始化短期 memory | -| 多轮问诊 | 医学生向 AI 病人提问,支持普通和 SSE 流式返回 | -| 查看提示 | 练习模式下用户手动点击,Hint Agent 基于病例和当前对话生成提示 | -| 检查/检验申请 | 从 `case_exam_item` 返回固定结果,写入 `training_order` 和短期 memory | -| 完成问诊 | 至少一轮医生问诊后进入诊断阶段 | -| 提交诊断 | 写入 `training_submission` | -| 提交治疗 | 更新 `training_submission`,进入评价阶段 | -| AI 评价报告 | 读取 `scoring_rule`、知识库命中结果和作答过程,写入 `training_record` | -| PDF 导出 | 基于 `training_record` 生成本地 PDF 并回写路径 | -| 历史记录 | 按 `external_user_id` 查询 `training_record` | -| LLM 测试 | 测试 Fast 和 Reason 模型响应耗时 | - -## 训练模式 - -当前只保留两个业务模式: - -| 模式 | 内部值 | 数据来源 | 说明 | -|---|---|---|---| -| 练习模式 | `practice` | `case_base + traditional_case` | 支持自由问诊、检查申请、诊断治疗提交和查看提示 | -| 教学互动模式 | `teaching` | `case_base + teaching_case` | 保留教学目标、讨论题、教师引导和评分重点 | - -历史 `novice` 请求在后端归一为 `practice`。查看提示功能不作为独立模式存在,而是练习模式中的手动辅助能力。 - -## 状态流转 - -```text -inquiry --> diagnosis --> treatment --> evaluating --> completed -``` - -| 状态 | 可执行操作 | -|---|---| -| `inquiry` | Chat、查看提示、申请检查、完成问诊 | -| `diagnosis` | 提交诊断、申请检查 | -| `treatment` | 提交治疗、申请检查 | -| `evaluating` | 生成评价 | -| `completed` | 查看报告、导出 PDF、进入历史 | - -## 存储边界 - -- 多轮聊天只作为单次会话短期 memory,不写入长期历史。 -- 检查结果写入 `training_order`,用于评分依据。 -- 诊断和治疗写入 `training_submission`。 -- 只有完整完成评价后才写入 `training_record`。 -- 历史记录和 PDF 导出均基于 `training_record`。 - -## 第一版不包含 - -- 独立注册登录。 -- 多租户权限后台。 -- HIS/LIS/PACS 对接。 -- 语音、视频、影像阅片。 -- PDF 病例自动解析。 -- 生产级知识库后台。 -- 复杂考试系统。 diff --git a/docs/02_database_design.md b/docs/02_database_design.md deleted file mode 100644 index c258ee0..0000000 --- a/docs/02_database_design.md +++ /dev/null @@ -1,87 +0,0 @@ -# 数据库设计 - -## 1. 当前确定方案 - -医疗问诊 Agent 使用独立数据库 `medical_consultation_agent`。病例来源以源库结构为准,当前业务不再依赖旧的 `cases`、`case_exam_items`、`training_sessions`、`session_orders`、`session_submissions`、`evaluation_records`、`evaluation_report_exports`、`rubric_templates`。 - -旧表已清理。后续开发直接面向新表: - -| 分类 | 表 | 说明 | -|---|---|---| -| 病例来源 | `case_base` | 病例主表,来自源库字段 | -| 病例来源 | `traditional_case` | 练习模式病例扩展 | -| 病例来源 | `teaching_case` | 教学互动模式病例扩展 | -| 评分规则 | `scoring_rule` | 病例级基础评分规则 | -| 检查项目 | `case_exam_item` | 本业务新增,保存固定检查/检验结果 | -| 训练运行 | `training_session` | 本业务新增,保存一次训练会话状态 | -| 训练运行 | `training_order` | 本业务新增,保存一次训练中的检查申请 | -| 训练运行 | `training_submission` | 本业务新增,保存诊断和治疗提交 | -| 训练结果 | `training_record` | 完整训练结束后的长期评价记录 | -| 知识库 | `knowledge_sources`、`knowledge_documents`、`knowledge_chunks` | 评分指南检索和引用 | -| 提示词 | `prompt_templates` | Markdown 提示词模板元数据 | -| 用户档案 | `user_learning_profiles` | 用户历史评价聚合 | -| 审计 | `audit_logs` | 关键行为审计 | - -## 2. 病例读取逻辑 - -练习模式读取: - -```text -case_base + traditional_case + case_exam_item + scoring_rule -``` - -教学互动模式读取: - -```text -case_base + teaching_case + case_exam_item + scoring_rule -``` - -当前模式只保留 `practice` 和 `teaching`。历史的 `novice` 请求在后端归一为 `practice`,查看提示功能保留在练习模式中,由用户点击后触发。 - -## 3. 评价生成逻辑 - -评价生成时读取以下数据: - -1. `training_session`:当前训练会话、模式、分数类型。 -2. Redis/进程内短期 memory:本次对话过程。 -3. `training_order`:用户申请过的检查结果。 -4. `training_submission`:用户提交的诊断和治疗方案。 -5. `scoring_rule`:病例基础评分规则。 -6. `knowledge_chunks`:科室/任务下命中的评分指南或人文关怀参考。 -7. 提示词模板:评分 Agent 使用的结构化输出约束。 - -LLM 返回结构化评价后,只写入 `training_record`。中断、退出、未完成诊断或未完成治疗的训练不写入 `training_record`。 - -## 4. 短期记忆和长期记录 - -| 数据 | 存储位置 | 生命周期 | -|---|---|---| -| 医生和 AI 病人的多轮对话 | Redis 或进程内 memory | 单次会话有效,TTL 到期或评价完成后释放 | -| 检查申请结果 | `training_order` + runtime memory | 当前训练长期可查,参与评分 | -| 诊断/治疗提交 | `training_submission` | 当前训练长期可查,参与评分 | -| AI 评价报告 | `training_record.ai_feedback_structured` | 完整训练后长期保存 | -| PDF 路径 | `training_record.pdf_file_path` | 导出后长期保存 | - -## 5. 用户隔离 - -宿主系统进入 Agent 时传入 `X-User-Id`。后端以该值写入 `external_user_id`: - -| 表 | 用户字段 | -|---|---| -| `training_session` | `external_user_id` | -| `training_order` | `external_user_id` | -| `training_submission` | `external_user_id` | -| `training_record` | `external_user_id` | - -`training_record.user_id` 保留源库数字用户字段。宿主传入字符串 user_id 时,业务隔离以 `external_user_id` 为准。 - -## 6. 初始化和清理脚本 - -| 脚本 | 作用 | -|---|---| -| `backend/scripts/migrate_to_new_schema.py` | 创建新表、写入 Demo 种子数据、补齐表中文注释 | -| `backend/scripts/drop_legacy_tables.py` | 删除已不再使用的旧表 | -| `backend/scripts/init_demo_db.py` | 新表体系的 Demo 数据初始化入口 | -| `backend/scripts/import_source_case_sql.py` | 安全导入接口解析后的病例 SQL,只做字段映射导入,不执行源 SQL 的建表和删表语句 | - -验证结果:旧表已清理,`old_remaining=[]`;新表已完成完整 Demo 流程复测。 diff --git a/docs/02_database_table_dictionary.md b/docs/02_database_table_dictionary.md deleted file mode 100644 index a36d88c..0000000 --- a/docs/02_database_table_dictionary.md +++ /dev/null @@ -1,172 +0,0 @@ -# 数据库表字段说明 - -## 1. 核心表总览 - -| 表 | 中文名 | 职责 | -|---|---|---| -| `case_base` | 病例主表 | 病例基础信息、标签、知识点、科室、发布状态 | -| `traditional_case` | 传统病例扩展表 | 练习模式下的标准诊断、标准治疗、指南参考 | -| `teaching_case` | 教学互动病例扩展表 | 教学互动模式下的教学目标、讨论题、教师引导、评分重点 | -| `scoring_rule` | 评分规则表 | 病例级基础评分维度和细则 | -| `case_exam_item` | 病例检查检验项目表 | 固定检查/检验结果,禁止 LLM 编造检查结果 | -| `training_session` | 训练会话表 | 一次训练的运行状态、模式、分数类型和 memory key | -| `training_order` | 训练检查申请表 | 用户在一次训练中申请过的检查结果 | -| `training_submission` | 训练诊断治疗提交表 | 用户提交的诊断、治疗、沟通和随访 | -| `training_record` | 训练记录表 | 完整训练结束后的 AI 评价、评分细则、PDF 路径 | -| `prompt_templates` | 提示词模板元数据表 | Markdown 提示词模板路径和版本 | -| `knowledge_*` | 知识库表 | 评分参考指南和人文关怀资料检索 | -| `user_learning_profiles` | 学习档案表 | 用户历史评价聚合 | -| `audit_logs` | 审计日志表 | 关键行为日志 | - -## 2. `case_base` - -| 字段 | 含义 | -|---|---| -| `id` | 病例 ID | -| `title` | 病例标题 | -| `case_type` | 病例/训练类型 | -| `difficulty`、`difficulty_score` | 难度和难度分 | -| `chief_complaint` | 主诉 | -| `description` | 病例描述,用于 AI 病人上下文 | -| `patient_age`、`patient_gender` | 患者年龄和性别 | -| `symptom_tags` | 关键症状标签 | -| `disease_tags` | 疾病标签 | -| `competency_tags` | 能力/考核标签 | -| `guideline_tags` | 指南标签 | -| `knowledge_points` | 知识点和关键检查提示 | -| `estimated_minutes` | 预计训练时长 | -| `osce_enabled` | 是否启用 OSCE | -| `rag_enabled` | 是否启用 RAG | -| `ai_prompt_template` | 病例 AI 提示词片段 | -| `multimodal_assets` | 多模态资源 | -| `publish_status`、`status` | 发布状态和启用状态 | -| `created_by_id`、`department_id` | 创建人和科室 | - -## 3. `traditional_case` - -| 字段 | 含义 | -|---|---| -| `id` | 传统病例扩展 ID | -| `case_id` | 关联 `case_base.id` | -| `standard_diagnosis` | 标准诊断 | -| `standard_treatment` | 标准治疗 | -| `guideline_reference` | 诊断和治疗参考依据 | - -## 4. `teaching_case` - -| 字段 | 含义 | -|---|---| -| `id` | 教学互动病例扩展 ID | -| `case_id` | 关联 `case_base.id` | -| `teaching_goal` | 教学目标 | -| `discussion_questions` | 讨论问题 | -| `teacher_guide` | 教师引导 | -| `scoring_focus` | 评分重点 | - -## 5. `scoring_rule` - -| 字段 | 含义 | -|---|---| -| `id` | 评分规则 ID | -| `case_id` | 关联 `case_base.id` | -| `dimension` | 一级评分维度 | -| `competency_dimension` | 能力维度 | -| `score_weight` | 分值权重 | -| `ai_auto_score` | 是否由 AI 自动评分 | -| `osce_dimension` | 是否 OSCE 维度 | -| `scoring_standard` | 评分标准文本 | -| `rubric_json` | 结构化评分细则 | - -## 6. `case_exam_item` - -| 字段 | 含义 | -|---|---| -| `id` | 检查项目 ID | -| `case_id` | 关联 `case_base.id` | -| `item_code` | 检查项目编码,当前会话去重依据 | -| `item_name` | 检查项目名称 | -| `item_type` | 项目类型,如 lab、imaging、vital_sign | -| `category` | 项目分类 | -| `result_text` | 固定返回结果文本 | -| `result_structured` | 结构化检查结果 | -| `is_key` | 是否关键检查 | -| `is_abnormal` | 是否异常结果 | -| `score_weight` | 评分权重 | -| `display_order` | 展示顺序 | - -## 7. `training_session` - -| 字段 | 含义 | -|---|---| -| `id` | 训练会话 ID | -| `session_code` | 会话编码 | -| `external_user_id` | 宿主系统用户 ID | -| `tenant_id`、`class_id`、`entry_scene` | 宿主上下文 | -| `case_id` | 关联 `case_base.id` | -| `case_type` | 训练类型 | -| `training_mode` | 训练模式,当前为 `practice` 或 `teaching` | -| `score_type` | 评分类型,`percentage` 或 `five_point` | -| `status` | 阶段状态:`inquiry -> diagnosis -> treatment -> evaluating -> completed` | -| `started_at`、`inquiry_completed_at`、`completed_at` | 阶段时间 | -| `memory_key` | Redis/进程内短期 memory key | -| `metadata` | 扩展数据 | - -## 8. `training_order` - -| 字段 | 含义 | -|---|---| -| `id` | 检查申请 ID | -| `session_id` | 关联 `training_session.id` | -| `external_user_id` | 宿主系统用户 ID | -| `case_id` | 关联 `case_base.id` | -| `exam_item_id` | 关联 `case_exam_item.id` | -| `item_code`、`item_name`、`item_type` | 检查项目编码、名称、类型 | -| `result_text`、`result_structured` | 固定检查结果 | -| `is_key`、`is_abnormal` | 是否关键、是否异常 | -| `ordered_at` | 申请时间 | - -同一 `session_id + item_code` 唯一,重复申请返回已有记录,不重复写 memory。 - -## 9. `training_submission` - -| 字段 | 含义 | -|---|---| -| `id` | 提交记录 ID | -| `session_id` | 关联 `training_session.id` | -| `external_user_id` | 宿主系统用户 ID | -| `primary_diagnosis` | 主要诊断 | -| `differential_diagnoses` | 鉴别诊断 | -| `diagnosis_basis` | 诊断依据 | -| `treatment_principle` | 治疗原则 | -| `treatment_measures` | 治疗措施 | -| `risk_plan` | 风险预案 | -| `communication` | 医患沟通 | -| `follow_up` | 随访安排 | -| `diagnosis_submitted_at`、`treatment_submitted_at` | 提交时间 | - -## 10. `training_record` - -| 字段 | 含义 | -|---|---| -| `id` | 训练记录 ID | -| `training_mode`、`case_type` | 模式和训练类型 | -| `start_time`、`end_time`、`duration_seconds` | 训练时间 | -| `total_score`、`ai_score`、`teacher_score` | 总分、AI 分、教师分 | -| `evaluation_level` | 评价等级 | -| `status` | 记录状态 | -| `feedback` | 总体反馈 | -| `thinking_chain` | 证据摘要和评分依据 | -| `diagnosis_path` | 诊断路径摘要 | -| `wrong_points` | 错误点/扣分点 | -| `missed_questions` | 遗漏问题 | -| `recommendation_result` | 改进建议和导出信息 | -| `ai_feedback_structured` | AI 结构化评价报告 | -| `prompt_version`、`rag_context_version` | 提示词版本和 RAG 版本 | -| `case_id` | 关联病例 | -| `user_id` | 源库数字用户 ID | -| `external_user_id` | 宿主系统用户 ID | -| `session_id` | 关联训练会话 | -| `score_type` | 评分类型 | -| `pdf_file_path` | PDF 报告路径 | - -`training_record` 是历史记录、报告详情和 PDF 导出的唯一长期来源。 diff --git a/docs/03_api_design.md b/docs/03_api_design.md deleted file mode 100644 index 7bac01e..0000000 --- a/docs/03_api_design.md +++ /dev/null @@ -1,887 +0,0 @@ -# 前端 API 对接文档 - -本文档面向 Vue 前端与后续正式前端开发,描述当前第一版 Demo 已实现的后端接口、请求头、状态流转、字段结构和错误处理规则。 - -## 1. 通用约定 - -### 1.1 Base URL - -本地开发默认地址: - -```text -http://127.0.0.1:8000/api/v1 -``` - -前端默认配置: - -```text -Vite dev server: http://127.0.0.1:5173 -API base: http://127.0.0.1:8000/api/v1 -``` - -### 1.2 必传 Header - -正式联调时,前端必须携带宿主系统登录态,推荐使用 `Authorization`: - -| Header | 类型 | 说明 | -|---|---:|---| -| `Authorization` | string | 宿主系统登录 token,例如 `Bearer `。医疗问诊 Agent 会转发到 Django `/api/user/users/me/`。 | -| `Cookie` | string | 如果宿主系统使用 Cookie 登录,浏览器可通过 `withCredentials` 携带 Cookie。 | - -本地 Demo 兼容以下 Header: - -| Header | 类型 | 说明 | -|---|---:|---| -| `X-User-Id` | string | Demo 兼容用户标识。正式联调启用外部鉴权后,以 Django `/me/` 返回的用户为准。 | -| `X-Entry-Scene` | string | 入口场景。Demo 前端默认 `vue_demo`。 | - -可选 Header: - -| Header | 类型 | 说明 | -|---|---:|---| -| `X-Tenant-Id` | string | 租户、机构或项目 ID。第一版只透传和审计。 | -| `X-User-Role` | string | 用户角色。第一版只透传和审计。 | -| `X-Class-Id` | string | 班级或课程 ID。第一版只透传和审计。 | -| `X-Request-Id` | string | 请求链路 ID。前端有链路追踪需求时传入。 | - -### 1.3 统一响应结构 - -除 SSE 流式接口外,后端统一返回: - -```json -{ - "code": "OK", - "message": "success", - "data": {} -} -``` - -前端处理规则: - -- `code === "OK"`:读取 `data`。 -- `code !== "OK"`:展示 `message`,不要直接展示底层异常。 -- HTTP 401 通常表示缺少 `X-User-Id`。 -- HTTP 404 通常表示资源不存在或不属于当前 `user_id`。 -- HTTP 400 通常表示当前状态不允许操作或入参不合法。 - -### 1.4 常见错误码 - -| code | 场景 | 前端处理 | -|---|---|---| -| `USER_ID_REQUIRED` | 缺少 `X-User-Id` | 回到入口设置页,提示填写 user_id。 | -| `CASE_NOT_FOUND` | 病例不存在、未启用或已删除 | 刷新病例列表。 | -| `CASE_DELETE_CONFIRM_REQUIRED` | 删除病例未传 `confirm=true` | 保持确认弹窗,不执行删除。 | -| `CASE_DELETE_TRAINING_DATA_EXISTS` | 病例存在训练数据但未允许删除训练数据 | 当前前端固定传 `delete_training_data=true`。 | -| `SESSION_NOT_FOUND` | 会话不存在或不属于当前用户 | 回到病例页重新创建会话。 | -| `SESSION_STATUS_INVALID` | 当前阶段不允许执行该操作 | 根据 `status` 禁用按钮并提示原因。 | -| `INQUIRY_REQUIRED` | 完成问诊前没有医生提问 | 提示至少完成一轮问诊。 | -| `DIAGNOSIS_REQUIRED` | 提交治疗前未提交诊断 | 引导到诊断表单。 | -| `TREATMENT_REQUIRED` | 生成评价前未提交治疗 | 引导到治疗表单。 | -| `ORDER_ITEM_NOT_FOUND` | 检查项不存在 | 刷新检查项列表。 | -| `LLM_CALL_TIMEOUT` | 非流式模型调用超时 | 提示重试。 | -| `LLM_STREAM_TIMEOUT` | 流式模型调用超时 | 结束 pending,提示重试或关闭流式。 | -| `LLM_STREAM_FAILED` | 流式模型调用失败 | 结束 pending,提示模型服务异常。 | -| `CASE_SQL_FILE_INVALID` | 上传文件不是 `.sql` | 提示选择 SQL 文件。 | -| `CASE_SQL_FILE_EMPTY` | SQL 文件为空 | 提示重新导出文件。 | -| `CASE_SQL_FILE_TOO_LARGE` | 文件超过 5MB | 提示压缩或拆分。 | -| `CASE_SQL_IMPORT_INVALID` | SQL 解析或字段映射失败 | 展示 `errors`,不允许确认导入。 | -| `AUTH_CREDENTIAL_REQUIRED` | 启用外部鉴权后未携带 Authorization/Cookie | 引导用户回宿主系统登录。 | -| `AUTH_USER_CENTER_UNAVAILABLE` | Django 用户中心不可访问或超时 | 提示稍后重试,检查用户中心服务。 | -| `AUTH_USER_INVALID` | Django 返回用户无效或登录态过期 | 引导用户重新登录。 | -| `AUTH_USER_PARSE_FAILED` | Django 返回结构无法提取 user_id | 联系后端确认用户接口字段。 | - -## 2. 前端主流程 - -```text -入口页 - -> GET /auth/me - -> GET /agent/hello -病例页 - -> GET /cases - -> GET /cases/{case_id} - -> GET /cases/{case_id}/delete-preview - -> DELETE /cases/{case_id} -训练配置页 - -> POST /sessions -Chat 页 - -> POST /sessions/{session_id}/chat 或 /chat/stream - -> POST /sessions/{session_id}/hints - -> GET /sessions/{session_id}/order-items - -> POST /sessions/{session_id}/orders -提交页 - -> POST /sessions/{session_id}/complete-inquiry - -> POST /sessions/{session_id}/diagnosis - -> POST /sessions/{session_id}/treatment -报告页 - -> POST /sessions/{session_id}/evaluation - -> POST /evaluations/{evaluation_id}/export-pdf -历史页 - -> GET /evaluations - -> GET /evaluations/{evaluation_id} -导入页 - -> POST /imports/case-sql/preview - -> POST /imports/case-sql/apply -LLM 测试页 - -> POST /llm/test/deepseek-fast - -> POST /llm/test/deepseek-reason -``` - -训练会话状态流转: - -```text -inquiry -> diagnosis -> treatment -> evaluating -> evaluated -``` - -前端按钮启用规则: - -| 阶段 | 允许操作 | ----|---| -| `inquiry` | 问诊、查看提示、申请检查、完成问诊 | -| `diagnosis` | 提交诊断 | -| `treatment` | 提交治疗方案 | -| `evaluating` | 生成评价报告 | -| `evaluated` | 查看报告、导出 PDF、查看历史 | - -## 3. 认证接口 - -### `GET /auth/me` - -用途:进入医疗问诊 Agent 前校验当前登录用户,并返回标准化用户信息。 - -调用链路: - -```text -Vue 前端 - -> GET /api/v1/auth/me -FastAPI 医疗问诊 Agent - -> GET http://192.168.2.76:8000/api/user/users/me/ -Django 用户中心 - -> 返回当前登录用户 -FastAPI - -> 提取 user_id 并返回标准结构 -``` - -正式联调请求头: - -```http -Authorization: Bearer <宿主系统token> -X-Entry-Scene: mac_vue_dev -``` - -Cookie 登录时: - -```http -Cookie: sessionid=... -X-Entry-Scene: mac_vue_dev -``` - -Response `data`: - -```json -{ - "user_id": "123", - "source": "django_user_center", - "username": "zhangsan", - "display_name": "张三", - "tenant_id": null, - "role": null -} -``` - -本地 Demo 兼容模式中,如果未开启外部鉴权或允许 `X-User-Id` 兜底,返回: - -```json -{ - "user_id": "demo_user_001", - "source": "demo_header", - "username": null, - "display_name": null, - "tenant_id": null, - "role": null -} -``` - -前端处理规则: - -- 进入 Agent 时先调用 `/auth/me`。 -- 成功后使用返回的 `user_id` 展示当前用户。 -- 后续业务接口继续携带相同 `Authorization` 或 Cookie。 -- 正式联调不要让用户手动输入 `X-User-Id`。 - -## 4. Agent Hello - -### `GET /agent/hello` - -用途:检查后端连接,返回当前用户上下文和 Demo 能力开关。 - -Response `data`: - -```json -{ - "user": { - "user_id": "demo_user_001", - "tenant_id": null, - "role": null - }, - "features": { - "stream_chat": true, - "score_types": ["percentage", "five_point"], - "pdf_export": true, - "knowledge_search": true, - "llm_mock_enabled": false, - "llm_mode": "real", - "llm_fallback_to_mock": false, - "llm_fast_model": "deepseek-v4-pro", - "llm_reason_model": "deepseek-v4-pro", - "runtime_memory_backend": "redis" - } -} -``` - -前端展示说明: - -- `stream_chat`:是否支持 SSE 流式问诊。 -- `score_types`:评分输出类型,`percentage` 为百分制,`five_point` 为五分制。 -- `pdf_export`:是否支持 PDF 导出。 -- `knowledge_search`:是否已接入评分参考指南检索。 -- `llm_mock_enabled`:是否强制使用 mock 模型。 -- `llm_fallback_to_mock`:真实模型失败时是否回退 mock。 -- `runtime_memory_backend`:短期记忆后端,通常为 `redis` 或 `memory`。 - -## 5. 病例接口 - -### `GET /cases` - -用途:获取病例列表。不会返回标准答案、隐藏病史、评分细则。 - -Query: - -| 参数 | 类型 | 必填 | 说明 | -|---|---:|---:|---| -| `department_id` | number | 否 | 科室筛选。 | -| `training_type` | string | 否 | 训练类别筛选:`case_analysis`、`diagnosis_treatment`、`consultation`。 | -| `mode` | string | 否 | 模式筛选:`practice`、`teaching`。 | - -Response `data`: - -```json -{ - "items": [ - { - "id": 2, - "case_code": "SRC_2", - "department_id": 1, - "title": "支气管肺炎 - 6岁男性患儿", - "difficulty": "medium", - "chief_complaint": "发热、咳嗽4天,喘息1天。", - "supported_training_type": "diagnosis_treatment", - "supported_mode": "free_chat", - "has_teaching_video": false, - "has_knowledge_points": true, - "has_quiz": false - } - ] -} -``` - -### `GET /cases/{case_id}` - -用途:获取病例训练入口详情。不会返回标准诊断、标准治疗、隐藏病史。 - -Response `data`: - -```json -{ - "id": 2, - "case_code": "SRC_2", - "title": "支气管肺炎 - 6岁男性患儿", - "department": "儿科", - "difficulty": "medium", - "patient": { - "name": null, - "age": 6, - "gender": "male", - "occupation": null - }, - "chief_complaint": "发热、咳嗽4天,喘息1天。", - "supported_training_type": "diagnosis_treatment", - "supported_mode": "free_chat", - "has_teaching_video": false, - "has_knowledge_points": true, - "has_quiz": false, - "order_item_types": ["lab", "imaging", "vital_sign"] -} -``` - -### `GET /cases/{case_id}/delete-preview` - -用途:删除病例前统计影响范围。前端删除弹窗先调用该接口。 - -Response `data`: - -```json -{ - "case_id": 2, - "case_title": "支气管肺炎 - 6岁男性患儿", - "can_delete": true, - "affected": { - "case_base": 1, - "traditional_case": 1, - "teaching_case": 0, - "scoring_rule": 1, - "case_exam_item": 6, - "training_session": 1, - "training_order": 6, - "training_submission": 1, - "training_record": 1 - } -} -``` - -### `DELETE /cases/{case_id}` - -用途:确认后删除病例及关联业务数据。审计日志只记录删除行为,不反删。 - -Request: - -```json -{ - "confirm": true, - "delete_training_data": true -} -``` - -Response `data`: - -```json -{ - "deleted": true, - "case_id": 2, - "deleted_counts": { - "training_order": 6, - "training_submission": 1, - "training_record": 1, - "training_session": 1, - "case_exam_item": 6, - "scoring_rule": 1, - "traditional_case": 1, - "teaching_case": 0, - "case_base": 1 - } -} -``` - -前端交互规则: - -- 删除按钮只放在病例详情区,不放在病例卡片上。 -- 点击删除后先调用 `delete-preview`。 -- 弹窗展示 `affected`。 -- 用户输入固定确认文案后再调用 `DELETE`。 -- 删除成功后清空当前病例、会话、检查结果、报告缓存,并刷新 `/cases`。 - -## 6. 会话与问诊接口 - -### `POST /sessions` - -用途:创建训练会话,初始化短期 memory。 - -Request: - -```json -{ - "case_id": 2, - "training_type": "diagnosis_treatment", - "mode": "practice", - "score_type": "percentage" -} -``` - -字段说明: - -| 字段 | 允许值 | 说明 | -|---|---|---| -| `training_type` | `case_analysis`、`diagnosis_treatment`、`consultation` | 训练类别。 | -| `mode` | `practice`、`teaching` | 训练模式。后端兼容旧值 `novice`,会归一为 `practice`。 | -| `score_type` | `percentage`、`five_point` | 评分输出类型。 | - -Response `data`: - -```json -{ - "session_id": 10, - "session_code": "sess_20260528100000_abcd1234", - "status": "inquiry", - "patient_opening": "家长:医生,孩子发热咳嗽好几天了,昨天开始喘得厉害,精神也不太好。" -} -``` - -### `POST /sessions/{session_id}/chat` - -用途:普通非流式问诊。 - -Request: - -```json -{ - "message": "孩子发热几天了?最高体温多少?" -} -``` - -Response `data`: - -```json -{ - "reply": "发热有4天了,最高烧到39度多,吃了退烧药能降下来,但过几个小时又会烧。", - "latency_ms": 2500, - "model": "deepseek-v4-pro", - "fallback_used": false -} -``` - -校验: - -- 会话必须属于当前 `X-User-Id`。 -- 当前状态必须为 `inquiry`。 -- `message` 长度 1 到 2000。 - -### `POST /sessions/{session_id}/chat/stream` - -用途:SSE 流式问诊。当前 Chat 页面优先使用该接口。 - -Request 同普通问诊。 - -SSE 事件: - -```text -event: message_delta -data: {"delta":"发热有4天了,"} - -event: message_done -data: {"latency_ms":3200,"first_token_ms":800,"model":"deepseek-v4-pro","fallback_used":false} - -event: error -data: {"code":"LLM_STREAM_TIMEOUT","message":"AI 病人回复超时,请重试"} -``` - -前端处理规则: - -- 收到 `message_delta`:追加到当前 AI 病人气泡。 -- 收到 `message_done`:结束 pending,启用发送按钮。 -- 收到 `error`:结束 pending,展示错误。 -- fetch abort 或 reader done 但未收到 `message_done`:结束 pending,提示“AI 病人回复超时或失败,请重试”。 - -### `POST /sessions/{session_id}/hints` - -用途:练习模式下手动生成提示。提示基于病例、当前对话和已申请检查动态生成。 - -Request: - -```json -{ - "last_user_message": "孩子发热几天了?最高体温多少?", - "scope": "current_conversation" -} -``` - -Response `data`: - -```json -{ - "hints": [ - "可以继续追问最高体温、热型和退热药反应。" - ], - "missing_dimensions": [ - "既往史", - "严重程度评估" - ], - "next_questions": [ - "孩子以前有没有喘息、哮喘或过敏史?", - "现在血氧是多少?有没有呼吸困难?" - ], - "recommended_orders": [ - { - "item_code": "oxygen_saturation", - "reason": "用于判断低氧和病情严重程度" - } - ] -} -``` - -前端处理规则: - -- 提示不自动弹出。 -- 只在练习模式中显示“查看提示”按钮。 -- 练习模式中是否点击提示不影响评分链路。 -- 教学互动模式当前不显示提示入口。 - -## 7. 检查/检验接口 - -### `GET /sessions/{session_id}/order-items` - -用途:获取当前病例可申请的检查项目。只返回名称和类型,不返回检查结果。 - -Response `data`: - -```json -{ - "items": [ - { - "item_code": "complete_blood_count", - "item_name": "血常规", - "item_type": "lab" - } - ] -} -``` - -### `POST /sessions/{session_id}/orders` - -用途:申请检查/检验,结果必须来自数据库 `case_exam_item`,不允许 LLM 编造。 - -Request: - -```json -{ - "item_code": "complete_blood_count" -} -``` - -Response `data`: - -```json -{ - "item_code": "complete_blood_count", - "item_name": "血常规", - "item_type": "lab", - "result_text": "WBC 12.4×10^9/L,中性粒细胞72%。", - "result_structured": {}, - "is_key": true, - "is_abnormal": true, - "context_written": true, - "already_ordered": false -} -``` - -幂等规则: - -- 同一 `session_id + item_code` 只写入一次。 -- 重复申请返回已有结果,`already_ordered=true`。 -- 重复申请不重复写入 runtime memory。 -- 前端按 `item_code` 去重展示。 - -## 8. 阶段提交接口 - -### `POST /sessions/{session_id}/complete-inquiry` - -用途:完成问诊,进入诊断阶段。 - -Response `data`: - -```json -{ - "session_id": 10, - "status": "diagnosis" -} -``` - -校验:至少存在一轮医生提问。 - -### `POST /sessions/{session_id}/diagnosis` - -用途:提交诊断。 - -Request: - -```json -{ - "primary_diagnosis": "支气管肺炎", - "differential_diagnoses": ["毛细支气管炎", "支气管哮喘急性发作", "上呼吸道感染"], - "diagnosis_basis": "结合发热、咳嗽、喘息、肺部体征、炎症指标升高、胸片异常和血氧情况,符合儿童支气管肺炎表现。" -} -``` - -Response `data`: - -```json -{ - "status": "treatment" -} -``` - -### `POST /sessions/{session_id}/treatment` - -用途:提交治疗方案。 - -Request: - -```json -{ - "treatment_principle": "抗感染、止咳平喘、改善氧合、严密观察病情变化。", - "treatment_measures": "根据病情选择抗感染治疗,必要时雾化吸入缓解喘息,监测体温、呼吸、血氧和精神反应。", - "risk_plan": "关注低氧、呼吸困难加重、持续高热、精神反应差、脱水等情况。", - "communication": "向家属说明肺炎病情、用药注意事项、观察指标和复诊/住院指征。", - "follow_up": "治疗后复查体温、呼吸、血氧和必要炎症指标,症状加重时及时就诊。" -} -``` - -Response `data`: - -```json -{ - "status": "evaluating" -} -``` - -## 9. 评价与报告接口 - -### `POST /sessions/{session_id}/evaluation` - -用途:生成 AI 评价报告。后端读取评分规则、知识检索结果、检查申请、问诊过程和提交内容后调用 Scoring Agent。 - -Request: - -```json -{ - "score_type": "percentage" -} -``` - -Response `data`: - -```json -{ - "evaluation_id": 1, - "score_type": "percentage", - "total_score": 82, - "dimension_scores": [ - { - "dimension": "信息采集", - "score": 18, - "max_score": 25, - "comment": "已覆盖发热和咳嗽,但既往喘息史和家族过敏史追问不足。", - "evidence": ["询问发热天数和最高体温", "申请血常规和CRP"], - "deductions": ["未充分询问既往喘息史"], - "improvement": "补充既往史、过敏史、严重程度评估相关问题。" - } - ], - "errors": [], - "improvement_plan": ["加强儿童肺炎严重程度评估训练。"], - "evidence_summary": ["检查结果已写入评分依据。"], - "guideline_refs": [], - "overall_comment": "诊断方向正确,检查利用和沟通细节仍需加强。" -} -``` - -评价完成后: - -- 写入 `training_record`。 -- 释放当前会话短期 memory。 -- 历史记录只保存评价报告,不长期保存完整聊天记录。 - -### `GET /evaluations` - -用途:按当前 `X-User-Id` 查询历史评价。 - -Response `data`: - -```json -{ - "items": [ - { - "evaluation_id": 1, - "case_title": "支气管肺炎 - 6岁男性患儿", - "score_type": "percentage", - "total_score": 82, - "created_at": "2026-05-29T10:00:00", - "pdf_exported": true - } - ] -} -``` - -### `GET /evaluations/{evaluation_id}` - -用途:获取评价详情。只能读取当前用户自己的评价。 - -Response `data`:继承评价报告字段,并额外包含: - -```json -{ - "session_id": 10, - "case_id": 2, - "case_title": "支气管肺炎 - 6岁男性患儿", - "created_at": "2026-05-29T10:00:00", - "pdf_file_path": "storage/reports/training_record_1_percentage_xxx.pdf" -} -``` - -### `POST /evaluations/{evaluation_id}/export-pdf` - -用途:生成本地 PDF 报告并保存文件路径。 - -Response `data`: - -```json -{ - "export_id": 1, - "file_path": "storage/reports/training_record_1_percentage_xxx.pdf" -} -``` - -## 10. 病例 SQL 导入接口 - -### `POST /imports/case-sql/preview` - -用途:上传接口解析后的 SQL 文件,只解析和校验,不写入数据库。 - -Request: - -```text -Content-Type: multipart/form-data -file: case.sql -``` - -Response `data`: - -```json -{ - "file_name": "case.sql", - "encoding": "utf-8", - "tables": { - "case_base": 1, - "traditional_case": 1, - "teaching_case": 0, - "scoring_rule": 1 - }, - "can_import": true, - "warnings": [ - "源 SQL 未包含 case_exam_item,导入器会按当前业务规则处理。" - ], - "errors": [], - "preview_cases": [ - { - "id": 2, - "title": "支气管肺炎 - 6岁男性患儿", - "case_type": "traditional", - "difficulty": "medium" - } - ] -} -``` - -规则: - -- 只识别 `case_base`、`traditional_case`、`teaching_case`、`scoring_rule`。 -- 不执行源 SQL 中的 `DROP TABLE`、`CREATE TABLE`、`ALTER TABLE`、`LOCK TABLES`。 -- 字段数量不匹配、JSON 损坏、字符串未闭合时返回 `can_import=false`。 -- `preview` 不写库。 - -### `POST /imports/case-sql/apply` - -用途:确认导入,将 SQL 中的病例源表数据映射写入当前数据库。 - -Request 同预检接口。 - -Response `data`: - -```json -{ - "imported": true, - "file_name": "case.sql", - "encoding": "utf-8", - "inserted_or_updated_cases": 1, - "imported_traditional_cases": 1, - "imported_teaching_cases": 0, - "imported_scoring_rules": 1, - "generated_exam_items": 6, - "warnings": [] -} -``` - -前端处理: - -- 先调用 `preview`,只有 `can_import=true` 才允许点击“确认导入”。 -- 导入成功后刷新病例列表。 -- 如果源 SQL 缺少 `case_exam_item`,后端会生成基础检查项,保证新病例可训练。 - -## 11. 知识检索接口 - -### `GET /knowledge/search` - -用途:按科室、训练类别和关键词检索评分参考指南。第一版主要供评价链路和调试使用。 - -Query: - -| 参数 | 类型 | 必填 | 说明 | -|---|---:|---:|---| -| `department_id` | number | 是 | 科室 ID。 | -| `training_type` | string | 是 | 训练类别。 | -| `q` | string | 否 | 关键词,多个关键词用英文逗号分隔。 | - -Response `data`: - -```json -{ - "matched_chunks": [], - "source_refs": [], - "no_match": true -} -``` - -## 12. LLM 测试接口 - -### `POST /llm/test/deepseek-fast` - -用途:测试快速模型耗时。 - -Request: - -```json -{ - "message": "请用一句话说明医疗问诊训练 Demo 的用途。" -} -``` - -Response `data`: - -```json -{ - "model": "deepseek-v4-pro", - "first_token_ms": null, - "total_latency_ms": 3000, - "stream": false, - "mock_mode": false, - "fallback_used": false, - "thinking_enabled": false, - "reasoning_effort": null -} -``` - -### `POST /llm/test/deepseek-reason` - -用途:测试 reason 模型耗时。接口内部会优先按配置执行,流式不兼容时降级为非流式,不影响问诊主链路。 - -Request 同 Fast 测试。 - -Response 字段同 Fast 测试。 - -## 13. 前端字段枚举 - -| 字段 | 允许值 | -|---|---| -| `score_type` | `percentage`、`five_point` | -| `mode` | `practice`、`teaching` | -| `training_type` | `case_analysis`、`diagnosis_treatment`、`consultation` | -| `session.status` | `inquiry`、`diagnosis`、`treatment`、`evaluating`、`evaluated` | -| `patient.gender` | `male`、`female`、`null` | - -## 14. 前端联调注意事项 - -1. 所有请求必须带 `X-User-Id`,否则后端返回 `USER_ID_REQUIRED`。 -2. 当前 Demo 不做登录注册,`user_id` 由宿主系统或测试页传入。 -3. 病例详情不返回标准答案,避免前端泄露训练答案。 -4. 检查结果只来自数据库,不来自 LLM。 -5. 聊天记录只在 runtime memory 中短期保存,评价完成后释放。 -6. 历史页读取的是 `training_record`,只展示完整训练结束后的评价。 -7. 删除病例会级联删除该病例训练数据,前端必须保留二次确认。 -8. `.env` 不进入 Git,前端不能写死 API Key。 diff --git a/docs/04_data_collection.md b/docs/04_data_collection.md deleted file mode 100644 index 415a8fa..0000000 --- a/docs/04_data_collection.md +++ /dev/null @@ -1,97 +0,0 @@ -# 数据采集与存储边界 - -## 1. 采集原则 - -- 进入 Agent 时只接收宿主系统传入的用户上下文,不做登录注册。 -- 问诊聊天只作为本次训练的短期 memory 使用,训练中断或退出不写入长期聊天历史。 -- 检查申请、诊断提交、治疗提交属于本次训练过程数据,参与最终评分。 -- 只有生成 AI 评价报告后,系统才写入长期训练记录 `training_record`。 -- 所有会话、检查、提交、评价和历史查询都按 `X-User-Id` 隔离。 - -## 2. 进入 Agent - -| 数据 | 来源 | 存储位置 | 用途 | -|---|---|---|---| -| `user_id` | Header `X-User-Id` | `training_session.external_user_id`、`training_record.external_user_id`、`audit_logs.user_id` | 用户隔离 | -| `tenant_id` | Header `X-Tenant-Id` | `training_session.tenant_id`、`audit_logs.tenant_id` | 宿主系统组织上下文 | -| `class_id` | Header `X-Class-Id` | `training_session.class_id` | 班级/教学上下文 | -| `entry_scene` | Header `X-Entry-Scene` | `training_session.entry_scene`、`audit_logs.entry_scene` | 入口来源 | -| `role` | Header `X-Role` | `audit_logs.role` | 审计上下文 | -| `request_id` | 系统生成或 Header | `audit_logs.request_id` | 链路追踪 | - -## 3. 创建会话 - -| 数据 | 存储位置 | 说明 | -|---|---|---| -| `case_id` | `training_session.case_id` | 关联 `case_base.id` | -| `training_type` | `training_session.case_type` | 当前来自病例类型 | -| `mode` | `training_session.training_mode` | `practice` 或 `teaching` | -| `score_type` | `training_session.score_type` | `percentage` 或 `five_point` | -| `session_code` | `training_session.session_code` | 会话业务编号 | -| `memory_key` | `training_session.memory_key` | Redis/进程内 memory key | -| `patient_opening` | runtime memory | AI 病人首句,不作为长期历史保存 | - -## 4. 问诊过程 - -| 数据 | 存储位置 | 生命周期 | -|---|---|---| -| 医学生提问 | runtime memory | 本次会话有效,TTL 到期或评价完成后释放 | -| AI 病人回复 | runtime memory | 本次会话有效,TTL 到期或评价完成后释放 | -| SSE 响应耗时 | 前端状态、评价结构摘要 | 用于演示和问题排查 | -| LLM 模型名/fallback 状态 | 前端状态、评价结构摘要 | 用于确认真实模型或 fallback | - -短期 memory 默认使用 Redis,Redis 不可用时降级为进程内 memory。TTL 由 `RUNTIME_MEMORY_TTL_SECONDS` 控制;每次写入会刷新 TTL。 - -## 5. 检查/检验申请 - -| 数据 | 存储位置 | 说明 | -|---|---|---| -| `item_code` | `training_order.item_code` | 同一会话去重依据 | -| `item_name` | `training_order.item_name` | 检查名称 | -| `item_type` | `training_order.item_type` | `lab`、`imaging`、`vital_sign` 等 | -| `result_text` | `training_order.result_text` | 从 `case_exam_item` 读取的固定结果 | -| `result_structured` | `training_order.result_structured` | 结构化结果 | -| `is_key`、`is_abnormal` | `training_order` | 评分参考 | - -检查结果只来自数据库 `case_exam_item`。LLM 不生成、不改写检查结果。 - -## 6. 诊断与治疗提交 - -| 数据 | 存储位置 | 说明 | -|---|---|---| -| `primary_diagnosis` | `training_submission.primary_diagnosis` | 主要诊断 | -| `differential_diagnoses` | `training_submission.differential_diagnoses` | 鉴别诊断 | -| `diagnosis_basis` | `training_submission.diagnosis_basis` | 诊断依据 | -| `treatment_principle` | `training_submission.treatment_principle` | 治疗原则 | -| `treatment_measures` | `training_submission.treatment_measures` | 治疗措施 | -| `risk_plan` | `training_submission.risk_plan` | 风险预案 | -| `communication` | `training_submission.communication` | 医患沟通 | -| `follow_up` | `training_submission.follow_up` | 随访安排 | - -## 7. 评价报告 - -评价生成读取以下数据: - -- `training_session`:模式、分数类型、病例 ID、memory key。 -- runtime memory:本次问诊对话摘要。 -- `training_order`:已申请检查和结果。 -- `training_submission`:诊断和治疗提交。 -- `case_base + traditional_case/teaching_case`:病例基础资料和标准参考。 -- `scoring_rule`:基础评分规则。 -- `knowledge_chunks`:科室/类型下命中的评分指南片段。 - -评价完成后写入: - -| 数据 | 存储位置 | -|---|---| -| 总分、评分类型、等级 | `training_record.total_score`、`score_type`、`evaluation_level` | -| 结构化评分 | `training_record.ai_feedback_structured` | -| 证据摘要 | `training_record.thinking_chain` | -| 诊断路径摘要 | `training_record.diagnosis_path` | -| 扣分点 | `training_record.wrong_points` | -| 改进计划 | `training_record.recommendation_result` | -| PDF 路径 | `training_record.pdf_file_path` | - -## 8. 审计日志 - -`audit_logs` 记录关键动作:进入 Agent、创建会话、问诊、申请检查、完成问诊、提交诊断、提交治疗、生成评价、导出 PDF。审计日志只记录元数据和对象 ID,不保存完整聊天全文。 diff --git a/docs/05_agent_prompt_design.md b/docs/05_agent_prompt_design.md deleted file mode 100644 index 01f008d..0000000 --- a/docs/05_agent_prompt_design.md +++ /dev/null @@ -1,169 +0,0 @@ -# Agent 编排与提示词模板 - -## 1. 当前编排结构 - -```mermaid -flowchart LR - API["FastAPI Router"] --> Service["Session / Order / Evaluation Service"] - Service --> Orchestrator["MedicalConsultationOrchestrator"] - Orchestrator --> Patient["Patient Agent"] - Orchestrator --> Hint["Hint Agent"] - Service --> Order["Order Engine"] - Service --> Knowledge["Knowledge Retrieval"] - Orchestrator --> Scoring["Scoring Agent"] - Scoring --> Report["Report Agent"] -``` - -## 2. Agent 职责 - -| 模块 | 文件 | 调用时机 | 输出 | -|---|---|---|---| -| Orchestrator | `backend/app/agents/orchestrator.py` | Service 需要调用子 Agent 时 | 统一调度结果 | -| Patient Agent | `backend/app/agents/patient_agent.py` | Chat / SSE Chat | AI 病人文本回复 | -| Hint Agent | `backend/app/agents/hint_agent.py` | 练习模式点击“查看提示” | 结构化提示 JSON | -| Order Engine | `backend/app/services/order_service.py` | 申请检查/检验 | 数据库检查结果 | -| Knowledge Retrieval | `backend/app/services/knowledge_service.py` | 生成评价前 | 评分指南命中片段 | -| Scoring Agent | `backend/app/agents/scoring_agent.py` | 生成 AI 评价 | 结构化评分 JSON | -| Report Agent | `backend/app/agents/report_agent.py` | Scoring Agent 之后 | 报告字段归一化 | - -## 3. 模板目录 - -```text -backend/app/prompts/ -├─ hint/ -├─ knowledge/ -├─ patient/ -├─ polish/ -├─ report/ -└─ scoring/ -``` - -`prompt_templates` 表保存模板元数据和 Markdown 路径。第一版 Demo 中,`HintAgent` 已直接读取 Markdown 模板;`PatientAgent` 和 `ScoringAgent` 当前由代码内置结构化提示拼接,Markdown 模板作为标准资产和后续热加载基础保留。 - -## 4. 模板清单 - -| 模板文件 | agent_type | 当前调用状态 | 调用时机 | -|---|---|---|---| -| `hint/novice_case_hint.md` | `hint` | 已调用 | `POST /sessions/{session_id}/hints` | -| `patient/practice.md` | `patient` | 标准资产 | 练习模式 AI 病人回复规则 | -| `patient/teaching.md` | `patient` | 标准资产 | 教学互动模式 AI 病人回复规则 | -| `patient/free_chat.md` | `patient` | 保留 | 早期自由问诊模板 | -| `patient/novice.md` | `patient` | 保留 | 早期新手模式模板,当前归并到 practice | -| `scoring/pediatrics_pneumonia.md` | `scoring` | 标准资产 | 儿科支气管肺炎评分规则 | -| `scoring/default_percentage.md` | `scoring` | 标准资产 | 百分制评分输出约束 | -| `scoring/default_five_point.md` | `scoring` | 标准资产 | 五分制评分输出约束 | -| `report/evaluation_report.md` | `report` | 标准资产 | 评价报告整理规则 | -| `knowledge/guideline_search_query.md` | `knowledge` | 标准资产 | 评分指南检索查询生成 | -| `polish/doctor_question_polish.md` | `polish` | 保留 | 后续医生提问润色 | -| `hint/novice_hint.md` | `hint` | 保留 | 早期提示模板 | - -## 5. Markdown 模板字段规范 - -所有新增模板使用同一结构: - -```markdown ---- -template_code: novice_case_hint -agent_type: hint -version: v1 -scene: practice -model_type: fast -output_format: json ---- - -# Role - -# Task - -# Inputs - -# Rules - -# Output Format - -# Safety Boundaries -``` - -## 6. Patient Agent 运行规则 - -输入: - -- `case_base` 基础病例。 -- `traditional_case` 或 `teaching_case` 扩展信息。 -- runtime memory 最近对话。 -- 已申请检查结果。 -- 医学生最新问题。 - -约束: - -- 只扮演患儿家属或 AI 病人。 -- 每次回答 1-3 句话。 -- 不输出 JSON、Markdown、思考过程。 -- 不主动泄露未被问到的隐藏信息。 -- 不给诊断或治疗方案。 -- 不编造检查结果。 - -## 7. Hint Agent 运行规则 - -调用入口:`POST /api/v1/sessions/{session_id}/hints`。 - -输入 JSON: - -```json -{ - "case": {}, - "session": {}, - "conversation_summary": [], - "ordered_results": [], - "last_user_message": "" -} -``` - -输出 JSON: - -```json -{ - "hints": [], - "missing_dimensions": [], - "next_questions": [], - "recommended_orders": [] -} -``` - -缺失字段由后端补空数组;LLM 返回非法 JSON 时使用稳定 fallback。提示只在练习模式中由用户手动点击触发,不自动弹出。 - -## 8. Scoring Agent 运行规则 - -评分上下文拼接顺序固定: - -1. 病例基础信息:`case_base`。 -2. 模式扩展信息:`traditional_case` 或 `teaching_case`。 -3. 基础评分规则:`scoring_rule`。 -4. 指南检索结果:`knowledge_chunks`,未命中时为空数组。 -5. 用户作答情况:runtime memory、`training_order`、`training_submission`。 -6. 输出格式约束:百分制或五分制。 - -Scoring Agent 必须输出结构化 JSON: - -```json -{ - "score_type": "percentage", - "total_score": 82, - "dimension_scores": [], - "errors": [], - "improvement_plan": [], - "evidence_summary": [], - "guideline_refs": [], - "overall_comment": "" -} -``` - -`ReportAgent` 只做字段校验和整理,不重新评分。 - -## 9. 安全边界 - -- 本系统只用于医学教学训练,不替代真实临床诊疗。 -- 检查结果只来自数据库。 -- LLM 不接收真实患者身份信息。 -- 历史记录只保存完整训练后的评价结果。 -- 未完成训练不会生成长期聊天历史。 diff --git a/docs/06_demo_testing_guide.md b/docs/06_demo_testing_guide.md deleted file mode 100644 index ac5cf0d..0000000 --- a/docs/06_demo_testing_guide.md +++ /dev/null @@ -1,215 +0,0 @@ -# Demo 前端测试指南 - -本文档用于本地演示和前后端联调。前端页面覆盖当前第一版 Demo 的完整训练闭环。 - -## 1. 启动服务 - -后端: - -```powershell -cd D:\Code\newfounder\medical-consultation-agent\backend -.\.venv\Scripts\activate -uvicorn app.main:app --reload --host 127.0.0.1 --port 8000 -``` - -前端: - -```powershell -cd D:\Code\newfounder\medical-consultation-agent\frontend -npm.cmd run dev -- --host 127.0.0.1 --port 5173 -``` - -访问: - -```text -http://127.0.0.1:5173 -``` - -## 2. 初始化数据库 - -```powershell -cd D:\Code\newfounder\medical-consultation-agent\backend -.\.venv\Scripts\python.exe scripts\migrate_to_new_schema.py -``` - -当前数据库为 `medical_consultation_agent`。 - -## 3. 入口页测试 - -1. 打开前端首页。 -2. 确认 `user_id` 为 `demo_user_001`。 -3. 确认 API Base 为 `http://127.0.0.1:8000/api/v1`。 -4. 点击连接或进入 Agent。 - -预期结果: - -- 页面显示后端地址、当前 user_id、入口场景和最近连接时间。 -- “已连接”只在 `GET /api/v1/agent/hello` 成功后显示。 -- 功能卡片显示流式问诊、PDF 导出、知识检索、评分类型、LLM mock/fallback 状态。 - -## 4. 病例 SQL 导入测试 - -进入: - -```text -http://127.0.0.1:5173/#/import -``` - -步骤: - -1. 选择接口解析后的 `.sql` 文件。 -2. 点击“解析检查”。 -3. 查看识别到的表、病例预览、警告和错误。 -4. `can_import=true` 后点击“确认导入”。 -5. 导入成功后刷新病例库。 - -预期结果: - -- 预检阶段不写数据库。 -- 后端只解析 `case_base`、`traditional_case`、`teaching_case`、`scoring_rule`。 -- 源 SQL 中的 `DROP TABLE`、`CREATE TABLE`、`ALTER TABLE`、`LOCK TABLES` 不会执行。 -- 缺少 `case_exam_item` 时,后端按当前业务规则生成基础检查项。 -- 新病例出现在病例列表中,可继续创建训练会话。 - -## 5. 病例列表与详情测试 - -1. 进入病例页。 -2. 点击病例卡片。 -3. 查看病例详情。 -4. 点击“开始训练”。 - -预期结果: - -- 点击病例卡片只选中病例,不直接创建会话。 -- 详情展示科室、年龄、性别、主诉、训练类型、教学资源和可申请检查类型。 -- 详情不展示标准答案、隐藏病史和评分细则。 - -## 6. 删除病例测试 - -该功能用于联调阶段清理导入错误或不再需要的病例。 - -步骤: - -1. 在病例详情中点击“删除病例”。 -2. 弹窗中查看删除影响范围。 -3. 输入 `确认删除`。 -4. 点击“确认删除”。 -5. 删除成功后刷新病例列表。 - -预期结果: - -- 前端先调用 `GET /api/v1/cases/{case_id}/delete-preview`。 -- 确认后调用 `DELETE /api/v1/cases/{case_id}`。 -- 后端删除病例主表、扩展表、评分规则、检查项、训练会话、检查申请、提交内容和训练评价记录。 -- `audit_logs` 只记录删除操作,不被反删。 -- 删除后访问病例详情返回 `CASE_NOT_FOUND`。 - -## 7. 创建训练会话 - -1. 选择训练模式:`练习模式` 或 `教学互动模式`。 -2. 选择评分类型:`百分制` 或 `五分制`。 -3. 点击“创建训练会话”。 - -预期结果: - -- 后端创建 `training_session`。 -- Redis 或进程内 memory 生成短期记忆。 -- Chat 页显示 AI 病人开场白。 - -## 8. 多轮问诊测试 - -推荐问题: - -```text -孩子发热几天了?最高体温多少? -咳嗽有没有痰?有没有喘息或呼吸困难? -精神状态和食欲怎么样? -以前有没有喘息、哮喘或过敏史? -最近有没有接触感冒、肺炎或发热的人? -``` - -预期结果: - -- 流式模式下逐步显示 AI 病人回复。 -- 收到 `message_done` 后停止“正在生成”。 -- 出错时显示错误,不会无限 pending。 - -## 9. 查看提示测试 - -练习模式中点击“查看提示”。 - -预期结果: - -- 调用 `POST /api/v1/sessions/{session_id}/hints`。 -- 展示缺失问诊维度、下一步问题和推荐检查。 -- 提示不自动弹出,不写入长期历史。 - -## 10. 检查/检验申请测试 - -建议申请: - -- 血常规 -- CRP -- 胸片 -- 血氧饱和度 -- 肺部体格检查 - -预期结果: - -- 检查结果来自 `case_exam_item`。 -- 同一 `item_code` 重复点击不会重复写入。 -- 页面提示检查结果已写入本次会话上下文和评分依据。 - -## 11. 诊断与治疗提交测试 - -1. 点击“完成问诊”。 -2. 填写诊断。 -3. 点击“提交诊断”。 -4. 填写治疗方案。 -5. 点击“提交治疗方案”。 - -演示时可以点击“填入演示模板”,正常测试时表单默认为空。 - -预期结果: - -- 完成问诊要求至少一轮医生提问。 -- 诊断提交后进入治疗阶段。 -- 治疗提交后进入评价阶段。 - -## 12. 评价、PDF 与历史记录测试 - -1. 点击“生成 AI 评价报告”。 -2. 查看维度评分、证据摘要、扣分点和改进计划。 -3. 点击“导出 PDF”。 -4. 进入历史记录页刷新。 - -预期结果: - -- 评价写入 `training_record`。 -- runtime memory 释放。 -- PDF 路径写入 `training_record.pdf_file_path`。 -- 历史记录按当前 `user_id` 查询。 - -## 13. LLM 测试 - -进入 LLM 测试页: - -- 点击“测试 Fast”。 -- 点击“测试 Reason”。 - -预期结果: - -- 页面展示模型名、总耗时、是否流式、是否 mock、是否 fallback。 -- 真实模型异常时显示错误,不静默伪装为真实响应。 - -## 14. 常见问题 - -| 问题 | 排查 | -|---|---| -| 页面显示未连接 | 检查后端是否在 `127.0.0.1:8000` 启动。 | -| 缺少 `X-User-Id` | 入口页填写 user_id 后重新进入。 | -| 病例为空 | 运行 `scripts\migrate_to_new_schema.py` 或导入病例 SQL。 | -| Chat 卡在生成中 | 查看浏览器控制台 SSE 是否收到 `message_done` 或 `error`。 | -| 检查重复出现 | 刷新页面后复测,同一 `item_code` 应只出现一次。 | -| 评价无法生成 | 确认已完成问诊、提交诊断、提交治疗。 | -| Redis 中有 TTL | 正常行为,短期 memory 使用 TTL 自动过期。 | diff --git a/docs/07_demo_function_traceability.md b/docs/07_demo_function_traceability.md deleted file mode 100644 index 650fc7e..0000000 --- a/docs/07_demo_function_traceability.md +++ /dev/null @@ -1,76 +0,0 @@ -# Demo 功能追踪表 - -本文档用于说明当前 Demo 功能、前端入口、后端接口、核心函数和数据表之间的对应关系。 - -## 1. 功能到代码映射 - -| 功能 | 前端页面/按钮 | API | Router 函数 | Service / Agent | 数据表 | 状态 | -|---|---|---|---|---|---|---| -| Agent 连接检查 | 入口页“连接/进入” | `GET /api/v1/agent/hello` | `agent.hello` | `AuditService.log` | `audit_logs` | 已实现 | -| 病例列表 | 病例页“刷新” | `GET /api/v1/cases` | `cases.list_cases` | `CaseService.list_cases` | `case_base` | 已实现 | -| 病例详情 | 点击病例卡片 | `GET /api/v1/cases/{case_id}` | `cases.get_case_detail` | `CaseService.get_case_detail` | `case_base`、`traditional_case`、`teaching_case`、`case_exam_item` | 已实现 | -| 删除病例预览 | 病例详情“删除病例” | `GET /api/v1/cases/{case_id}/delete-preview` | `cases.get_case_delete_preview` | `CaseService.get_delete_preview` | 病例表、训练表统计 | 已实现 | -| 删除病例 | 删除弹窗“确认删除” | `DELETE /api/v1/cases/{case_id}` | `cases.delete_case` | `CaseService.delete_case`、`CaseRepository.delete_case_cascade` | `case_base`、`traditional_case`、`teaching_case`、`scoring_rule`、`case_exam_item`、`training_session`、`training_order`、`training_submission`、`training_record` | 已实现 | -| 病例 SQL 预检 | 导入页“解析检查” | `POST /api/v1/imports/case-sql/preview` | `imports.preview_case_sql` | `CaseSqlImportService.preview` | 不写库 | 已实现 | -| 病例 SQL 导入 | 导入页“确认导入” | `POST /api/v1/imports/case-sql/apply` | `imports.apply_case_sql` | `CaseSqlImportService.apply`、`scripts.import_source_case_sql` | `case_base`、`traditional_case`、`teaching_case`、`scoring_rule`、`case_exam_item` | 已实现 | -| 创建训练会话 | 训练配置页“创建训练会话” | `POST /api/v1/sessions` | `sessions.create_session` | `SessionService.create_session` | `training_session`、runtime memory | 已实现 | -| 普通问诊 | Chat 页“发送问诊” | `POST /api/v1/sessions/{session_id}/chat` | `sessions.chat` | `SessionService.chat`、`PatientAgent.reply` | runtime memory | 已实现 | -| 流式问诊 | Chat 页流式开关开启 | `POST /api/v1/sessions/{session_id}/chat/stream` | `sessions.chat_stream` | `SessionService.stream_chat`、`PatientAgent.stream_reply` | runtime memory | 已实现 | -| 查看提示 | Chat 页“查看提示” | `POST /api/v1/sessions/{session_id}/hints` | `sessions.generate_hints` | `SessionService.generate_hints`、`HintAgent.generate` | runtime memory、`training_order` | 已实现 | -| 检查项目列表 | 检查面板刷新 | `GET /api/v1/sessions/{session_id}/order-items` | `sessions.list_order_items` | `OrderService.list_order_items` | `case_exam_item` | 已实现 | -| 申请检查 | 检查面板“申请该检查” | `POST /api/v1/sessions/{session_id}/orders` | `sessions.create_order` | `OrderService.create_order` | `case_exam_item`、`training_order` | 已实现 | -| 完成问诊 | Chat 页“完成问诊” | `POST /api/v1/sessions/{session_id}/complete-inquiry` | `sessions.complete_inquiry` | `SessionService.complete_inquiry` | `training_session` | 已实现 | -| 提交诊断 | 提交页“提交诊断” | `POST /api/v1/sessions/{session_id}/diagnosis` | `sessions.submit_diagnosis` | `SessionService.submit_diagnosis` | `training_submission` | 已实现 | -| 提交治疗 | 提交页“提交治疗方案” | `POST /api/v1/sessions/{session_id}/treatment` | `sessions.submit_treatment` | `SessionService.submit_treatment` | `training_submission` | 已实现 | -| 生成评价 | 报告页“生成 AI 评价报告” | `POST /api/v1/sessions/{session_id}/evaluation` | `sessions.create_evaluation` | `EvaluationService.create_evaluation`、`ScoringAgent.score`、`ReportAgent` | `scoring_rule`、`knowledge_chunks`、`training_record` | 已实现 | -| 历史评价列表 | 历史页刷新 | `GET /api/v1/evaluations` | `evaluations.list_evaluations` | `EvaluationService.list_history` | `training_record` | 已实现 | -| 评价详情 | 历史页详情/报告页 | `GET /api/v1/evaluations/{evaluation_id}` | `evaluations.get_evaluation_detail` | `EvaluationService.get_detail` | `training_record` | 已实现 | -| PDF 导出 | 报告页“导出 PDF” | `POST /api/v1/evaluations/{evaluation_id}/export-pdf` | `evaluations.export_pdf` | `PdfExportService.export` | `training_record` | 已实现 | -| 知识检索 | 后端预留/评价内部使用 | `GET /api/v1/knowledge/search` | `knowledge.search_knowledge` | `KnowledgeService.search_guidelines` | `knowledge_sources`、`knowledge_documents`、`knowledge_chunks` | 已实现 | -| LLM Fast 测试 | LLM 测试页“测试 Fast” | `POST /api/v1/llm/test/deepseek-fast` | `llm_test.test_deepseek_fast` | `OpenAICompatibleLLMClient.chat` | 无长期表 | 已实现 | -| LLM Reason 测试 | LLM 测试页“测试 Reason” | `POST /api/v1/llm/test/deepseek-reason` | `llm_test.test_deepseek_reason` | `OpenAICompatibleLLMClient.stream_chat/chat` | 无长期表 | 已实现 | - -## 2. 当前数据表状态 - -当前运行只依赖新表: - -```text -case_base -traditional_case -teaching_case -scoring_rule -case_exam_item -training_session -training_order -training_submission -training_record -prompt_templates -knowledge_sources -knowledge_documents -knowledge_chunks -user_learning_profiles -audit_logs -``` - -旧表不参与运行。旧表删除后,后端测试和前端构建均已通过。 - -## 3. 关键业务边界 - -- `X-User-Id` 是会话、提交、评价和历史记录的隔离依据。 -- Chat 消息只作为短期 memory 使用,不作为长期历史保存。 -- 检查/检验结果只来自 `case_exam_item`,不由 LLM 编造。 -- 完整训练结束后只沉淀 `training_record`。 -- 删除病例会删除病例相关业务数据和训练记录,但保留 `audit_logs`。 -- 病例详情接口不返回标准答案和隐藏病史。 - -## 4. 核心状态流 - -```text -inquiry -> diagnosis -> treatment -> evaluating -> evaluated -``` - -- `inquiry`:允许 Chat、查看提示、申请检查、完成问诊。 -- `diagnosis`:允许提交诊断。 -- `treatment`:允许提交治疗方案。 -- `evaluating`:允许生成 AI 评价。 -- `evaluated`:允许查看报告、导出 PDF、查看历史。 diff --git a/docs/08_pediatric_case_demo_script.md b/docs/08_pediatric_case_demo_script.md deleted file mode 100644 index 35045b4..0000000 --- a/docs/08_pediatric_case_demo_script.md +++ /dev/null @@ -1,119 +0,0 @@ -# 儿科支气管肺炎病例演示脚本 - -## 1. 演示目标 - -展示“病例选择 -> 多轮问诊 -> 检查申请 -> 诊断治疗提交 -> AI 评价 -> PDF 导出 -> 历史记录”的完整闭环。 - -## 2. 演示前准备 - -- MySQL、Redis、后端、前端均已启动。 -- 前端入口页 user_id 使用 `demo_user_001`。 -- 数据库已运行 `scripts\migrate_to_new_schema.py`。 -- 后端 `.env` 使用真实 LLM 或明确显示 mock 状态。 - -## 3. 病例简介 - -病例:支气管肺炎 - 6岁男性患儿。 - -主诉:发热、咳嗽 4 天,喘息 1 天。 - -训练重点: - -- 发热、咳嗽、喘息等现病史采集。 -- 既往喘息史、过敏史、接触史、用药史。 -- 血常规、CRP、胸片、血氧饱和度、肺部体格检查。 -- 支气管肺炎诊断与鉴别诊断。 -- 抗感染、平喘、氧合监测、风险预案和医患沟通。 - -## 4. 前端点击路径 - -1. 入口页:确认连接成功。 -2. 病例页:点击“支气管肺炎 - 6岁男性患儿”。 -3. 病例详情:点击“开始训练”。 -4. 训练配置:选择“练习模式”和“百分制”。 -5. Chat 页:进行多轮问诊。 -6. 检查面板:申请检查/检验。 -7. Chat 页:点击“完成问诊”。 -8. 提交页:填写诊断和治疗。 -9. 报告页:生成评价、导出 PDF。 -10. 历史页:刷新并查看记录详情。 - -## 5. 可复制问诊模板 - -```text -孩子发热几天了?最高体温多少? -咳嗽有没有痰?有没有喘息或呼吸困难? -精神状态和食欲怎么样? -有没有呕吐、腹泻、抽搐或皮疹? -以前有没有类似喘息、哮喘、过敏史? -最近有没有接触感冒、肺炎或发热的人? -有没有用过退烧药、抗生素或雾化? -夜间症状是否加重? -尿量怎么样?有没有脱水表现? -家属对治疗有什么担心? -``` - -## 6. 检查/检验申请 - -演示时依次点击: - -- 血常规 -- CRP -- 胸片 -- 血氧饱和度 -- 肺部体格检查 - -展示重点: - -- 检查结果来自数据库,不由 LLM 编造。 -- 重复申请同一检查不会重复插入。 -- 检查结果会进入本次会话上下文和评分依据。 - -## 7. 诊断填写模板 - -```text -主要诊断:支气管肺炎 - -鉴别诊断: -毛细支气管炎、支气管哮喘急性发作、肺结核、上呼吸道感染 - -诊断依据: -患儿发热、咳嗽、喘息,肺部可闻及湿啰音,结合血常规和 CRP 炎症指标升高、胸片异常及血氧饱和度情况,符合儿童支气管肺炎表现。 -``` - -## 8. 治疗填写模板 - -```text -治疗原则: -抗感染、止咳平喘、改善氧合、严密观察病情变化。 - -治疗措施: -根据病情选择抗感染治疗,必要时雾化吸入缓解喘息,监测体温、呼吸、血氧和精神反应。 - -风险预案: -关注低氧、呼吸困难加重、持续高热、精神反应差、脱水等情况。 - -医患沟通: -向家属说明肺炎病情、用药注意事项、观察指标和复诊/住院指征。 - -随访安排: -治疗后复查体温、呼吸、血氧和必要炎症指标,症状加重时及时就诊。 -``` - -## 9. 报告展示重点 - -生成报告后重点展示: - -- 总分和评分类型。 -- 信息采集、分析推理、处置决策、沟通人文、临床整合等维度评分。 -- 证据摘要:系统如何结合问诊、检查、诊断和治疗给出评分。 -- 扣分点:遗漏了哪些问诊或检查。 -- 改进计划:下一次训练如何提高。 - -## 10. 可强调的产品价值 - -- 将病例库、问诊训练、检查申请、诊断治疗、AI 评价串成闭环。 -- 用户身份由宿主系统传入,本 Agent 不做重复登录。 -- 短期 memory 支持多轮训练,但不沉淀未完成聊天历史。 -- 检查结果由数据库控制,避免模型编造。 -- 评分可结合病例评分规则、科室指南和人文关怀要求继续扩展。 diff --git a/docs/sql/schema.sql b/docs/sql/schema.sql deleted file mode 100644 index 77a3909..0000000 --- a/docs/sql/schema.sql +++ /dev/null @@ -1,516 +0,0 @@ -CREATE DATABASE IF NOT EXISTS `medical_consultation_agent` - DEFAULT CHARACTER SET utf8mb4 - COLLATE utf8mb4_unicode_ci; - -USE `medical_consultation_agent`; - -SET NAMES utf8mb4; -SET FOREIGN_KEY_CHECKS = 0; - -DROP TABLE IF EXISTS `audit_logs`; -DROP TABLE IF EXISTS `user_learning_profiles`; -DROP TABLE IF EXISTS `training_record`; -DROP TABLE IF EXISTS `scoring_rule`; -DROP TABLE IF EXISTS `teaching_case`; -DROP TABLE IF EXISTS `traditional_case`; -DROP TABLE IF EXISTS `case_base`; -DROP TABLE IF EXISTS `rubric_templates`; -DROP TABLE IF EXISTS `prompt_templates`; -DROP TABLE IF EXISTS `knowledge_chunks`; -DROP TABLE IF EXISTS `knowledge_documents`; -DROP TABLE IF EXISTS `knowledge_sources`; -DROP TABLE IF EXISTS `evaluation_report_exports`; -DROP TABLE IF EXISTS `evaluation_records`; -DROP TABLE IF EXISTS `session_submissions`; -DROP TABLE IF EXISTS `session_orders`; -DROP TABLE IF EXISTS `session_runtime_messages`; -DROP TABLE IF EXISTS `training_sessions`; -DROP TABLE IF EXISTS `case_exam_items`; -DROP TABLE IF EXISTS `cases`; -DROP TABLE IF EXISTS `users`; -DROP TABLE IF EXISTS `departments`; - -CREATE TABLE `departments` ( - `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '科室ID', - `name` varchar(100) NOT NULL COMMENT '科室名称', - `code` varchar(50) NOT NULL COMMENT '科室编码', - `parent_id` int(11) DEFAULT NULL COMMENT '父级科室ID', - `sort_order` int(11) DEFAULT 0 COMMENT '排序', - `is_active` tinyint(1) DEFAULT 1 COMMENT '是否激活', - `created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - PRIMARY KEY (`id`), - UNIQUE KEY `uk_department_code` (`code`), - KEY `idx_department_parent` (`parent_id`), - CONSTRAINT `fk_department_parent` FOREIGN KEY (`parent_id`) REFERENCES `departments` (`id`) ON DELETE SET NULL ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='科室表'; - -CREATE TABLE `users` ( - `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '内部占位用户ID', - `external_user_id` varchar(128) NOT NULL COMMENT '宿主系统用户ID', - `display_name` varchar(100) DEFAULT NULL COMMENT '展示名', - `created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - PRIMARY KEY (`id`), - UNIQUE KEY `uk_external_user_id` (`external_user_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='宿主用户引用占位表'; - -CREATE TABLE `case_base` ( - `id` bigint NOT NULL AUTO_INCREMENT COMMENT '源库病例主表ID', - `title` varchar(255) NOT NULL COMMENT '病例标题', - `case_type` varchar(30) NOT NULL COMMENT '病例分类/训练类别', - `difficulty` varchar(20) NOT NULL DEFAULT 'medium' COMMENT '难度', - `difficulty_score` int DEFAULT NULL COMMENT '难度分值', - `chief_complaint` longtext NOT NULL COMMENT '主诉', - `description` longtext NOT NULL COMMENT '病例描述', - `patient_age` int DEFAULT NULL COMMENT '患者年龄', - `patient_gender` varchar(10) NOT NULL COMMENT '患者性别', - `tags` varchar(500) NOT NULL DEFAULT '' COMMENT '标签', - `symptom_tags` json NOT NULL COMMENT '症状标签', - `disease_tags` json NOT NULL COMMENT '疾病标签', - `competency_tags` json NOT NULL COMMENT '能力标签', - `guideline_tags` json NOT NULL COMMENT '指南标签', - `knowledge_points` json NOT NULL COMMENT '知识点', - `icd_codes` varchar(500) NOT NULL DEFAULT '' COMMENT 'ICD编码', - `estimated_minutes` int DEFAULT NULL COMMENT '预计训练时长', - `osce_enabled` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否OSCE', - `rag_enabled` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否启用指南检索', - `ai_prompt_template` longtext NOT NULL COMMENT 'AI病人提示词模板引用', - `multimodal_assets` json NOT NULL COMMENT '多模态资源', - `vector_status` smallint NOT NULL DEFAULT 0 COMMENT '向量状态', - `publish_status` smallint NOT NULL DEFAULT 1 COMMENT '发布状态', - `status` smallint NOT NULL DEFAULT 1 COMMENT '业务状态', - `created_by_id` bigint DEFAULT NULL COMMENT '创建人ID', - `department_id` bigint DEFAULT NULL COMMENT '科室ID', - `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT '创建时间', - `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6) COMMENT '更新时间', - PRIMARY KEY (`id`), - KEY `idx_case_base_department` (`department_id`), - KEY `idx_case_base_case_type` (`case_type`), - KEY `idx_case_base_status` (`status`, `publish_status`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='源库病例主表'; - -CREATE TABLE `traditional_case` ( - `id` bigint NOT NULL AUTO_INCREMENT COMMENT '传统病例ID', - `standard_diagnosis` longtext NOT NULL COMMENT '标准诊断', - `standard_treatment` longtext NOT NULL COMMENT '标准治疗', - `guideline_reference` longtext NOT NULL COMMENT '指南参考', - `case_id` bigint NOT NULL COMMENT '病例主表ID', - `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT '创建时间', - `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6) COMMENT '更新时间', - PRIMARY KEY (`id`), - UNIQUE KEY `uk_traditional_case_id` (`case_id`), - CONSTRAINT `fk_traditional_case_base` FOREIGN KEY (`case_id`) REFERENCES `case_base` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='传统病例扩展表'; - -CREATE TABLE `teaching_case` ( - `id` bigint NOT NULL AUTO_INCREMENT COMMENT '教学互动病例ID', - `teaching_goal` longtext NOT NULL COMMENT '教学目标', - `discussion_questions` longtext NOT NULL COMMENT '讨论问题', - `teacher_guide` longtext NOT NULL COMMENT '教师引导', - `scoring_focus` longtext NOT NULL COMMENT '评分重点', - `case_id` bigint NOT NULL COMMENT '病例主表ID', - `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT '创建时间', - `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6) COMMENT '更新时间', - PRIMARY KEY (`id`), - UNIQUE KEY `uk_teaching_case_id` (`case_id`), - CONSTRAINT `fk_teaching_case_base` FOREIGN KEY (`case_id`) REFERENCES `case_base` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='教学互动病例扩展表'; - -CREATE TABLE `scoring_rule` ( - `id` bigint NOT NULL AUTO_INCREMENT COMMENT '评分规则ID', - `dimension` varchar(50) NOT NULL COMMENT '评分维度', - `competency_dimension` varchar(50) NOT NULL COMMENT '能力维度', - `score_weight` decimal(5,2) NOT NULL COMMENT '分值/权重', - `ai_auto_score` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否AI自动评分', - `osce_dimension` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否OSCE维度', - `scoring_standard` longtext NOT NULL COMMENT '评分标准', - `rubric_json` json NOT NULL COMMENT '结构化评分细则', - `case_id` bigint NOT NULL COMMENT '病例主表ID', - `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT '创建时间', - `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6) COMMENT '更新时间', - PRIMARY KEY (`id`), - UNIQUE KEY `uk_scoring_rule_case_dimension` (`case_id`, `dimension`, `competency_dimension`), - KEY `idx_scoring_rule_case` (`case_id`), - CONSTRAINT `fk_scoring_rule_case_base` FOREIGN KEY (`case_id`) REFERENCES `case_base` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='源库评分规则表'; - -CREATE TABLE `cases` ( - `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '病例ID', - `source_case_id` bigint DEFAULT NULL COMMENT '源库病例ID,对应 case_base.id', - `case_code` varchar(64) NOT NULL COMMENT '病例编码', - `department_id` int(11) NOT NULL COMMENT '所属科室ID', - `title` varchar(100) NOT NULL COMMENT '病例标题', - `difficulty` enum('easy','medium','hard') DEFAULT 'medium' COMMENT '难度', - `patient_name` varchar(50) DEFAULT NULL COMMENT '患者姓名', - `patient_age` int(11) DEFAULT NULL COMMENT '患者年龄', - `patient_gender` enum('male','female') DEFAULT NULL COMMENT '患者性别', - `patient_occupation` varchar(50) DEFAULT NULL COMMENT '职业', - `chief_complaint` text COMMENT '主诉', - `present_illness` text COMMENT '现病史', - `past_history` text COMMENT '既往史', - `personal_history` text COMMENT '个人史', - `family_history` text COMMENT '家族史', - `physical_exam` json DEFAULT NULL COMMENT '体格检查数据', - `auxiliary_exam` json DEFAULT NULL COMMENT '辅助检查数据', - `diagnosis_primary` text COMMENT '主要诊断', - `diagnosis_differential` json DEFAULT NULL COMMENT '鉴别诊断列表', - `diagnosis_basis` text COMMENT '诊断依据', - `treatment_plan` json DEFAULT NULL COMMENT '治疗方案', - `consultation_config` json DEFAULT NULL COMMENT '会诊配置', - `inquiry_options` json DEFAULT NULL COMMENT '问诊选项(互动模式)', - `knowledge_videos` json DEFAULT NULL COMMENT '知识点视频(互动模式)', - `quiz_questions` json DEFAULT NULL COMMENT '教学题库(互动模式)', - `key_symptoms` json DEFAULT NULL COMMENT '关键症状', - `key_exams` json DEFAULT NULL COMMENT '关键检查', - `key_points` json DEFAULT NULL COMMENT '考核要点', - `evidence_reasoning_chain` json DEFAULT NULL COMMENT '诊断循证思维链', - `assessment_config` json DEFAULT NULL COMMENT '评分与考核配置', - `ai_patient_profile` json DEFAULT NULL COMMENT 'AI病人人设', - `patient_opening` text COMMENT 'AI病人开场白', - `hidden_patient_info` json DEFAULT NULL COMMENT '问到才回答的隐藏信息', - `has_teaching_video` tinyint(1) DEFAULT 0 COMMENT '是否有教学视频', - `has_knowledge_points` tinyint(1) DEFAULT 0 COMMENT '是否有知识点', - `has_quiz` tinyint(1) DEFAULT 0 COMMENT '是否有教学题库', - `source_pdf_name` varchar(255) DEFAULT NULL COMMENT '来源PDF名称', - `supported_training_type` enum('case_analysis','diagnosis_treatment','consultation') DEFAULT 'case_analysis' COMMENT '支持的训练类别(单选)', - `supported_mode` enum('free_chat','interactive') DEFAULT 'free_chat' COMMENT '支持的交互模式(单选)', - `is_active` tinyint(1) DEFAULT 1 COMMENT '是否激活', - `created_by` int(11) DEFAULT NULL COMMENT '创建人ID', - `created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - PRIMARY KEY (`id`), - UNIQUE KEY `uk_case_code` (`case_code`), - KEY `idx_department_id` (`department_id`), - KEY `idx_difficulty` (`difficulty`), - KEY `idx_is_active` (`is_active`), - KEY `idx_created_by` (`created_by`), - KEY `idx_training_type` (`supported_training_type`), - KEY `idx_mode` (`supported_mode`), - KEY `idx_cases_source_case` (`source_case_id`), - CONSTRAINT `fk_case_department` FOREIGN KEY (`department_id`) REFERENCES `departments` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, - CONSTRAINT `fk_case_creator` FOREIGN KEY (`created_by`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='病例表'; - -CREATE TABLE `case_exam_items` ( - `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '检查项目ID', - `case_id` int(11) NOT NULL COMMENT '病例ID', - `source_case_id` bigint DEFAULT NULL COMMENT '源库病例ID,用于后续从 case_base 直接读取检查项目', - `item_code` varchar(64) NOT NULL COMMENT '项目编码', - `item_name` varchar(128) NOT NULL COMMENT '项目名称', - `item_type` enum('lab','imaging','physical_exam','vital_sign','other') NOT NULL COMMENT '项目类型', - `category` varchar(64) DEFAULT NULL COMMENT '分类', - `result_text` text NOT NULL COMMENT '结果文本', - `result_structured` json DEFAULT NULL COMMENT '结构化结果', - `is_key` tinyint(1) DEFAULT 0 COMMENT '是否关键检查', - `is_abnormal` tinyint(1) DEFAULT 0 COMMENT '是否异常', - `score_weight` decimal(5,2) DEFAULT 0.00 COMMENT '评分权重', - `display_order` int(11) DEFAULT 0 COMMENT '展示顺序', - `created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - PRIMARY KEY (`id`), - UNIQUE KEY `uk_case_item_code` (`case_id`,`item_code`), - KEY `idx_exam_case_type` (`case_id`,`item_type`), - KEY `idx_case_exam_source_case` (`source_case_id`), - CONSTRAINT `fk_exam_case` FOREIGN KEY (`case_id`) REFERENCES `cases` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='病例检查检验项目表'; - -CREATE TABLE `training_sessions` ( - `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '会话ID', - `session_code` varchar(64) NOT NULL COMMENT '会话编码', - `user_id` varchar(128) NOT NULL COMMENT '宿主系统用户ID', - `tenant_id` varchar(128) DEFAULT NULL COMMENT '租户或项目ID', - `class_id` varchar(128) DEFAULT NULL COMMENT '班级或课程ID', - `entry_scene` varchar(64) DEFAULT NULL COMMENT '入口场景', - `case_id` int(11) NOT NULL COMMENT '病例ID', - `source_case_id` bigint DEFAULT NULL COMMENT '源库病例ID,保留 case_id 兼容旧运行表', - `training_type` enum('case_analysis','diagnosis_treatment','consultation') NOT NULL COMMENT '训练类别', - `mode` enum('novice','practice','teaching') NOT NULL COMMENT '训练模式', - `score_type` enum('percentage','five_point') NOT NULL DEFAULT 'percentage' COMMENT '分数输出类型', - `status` enum('created','inquiry','diagnosis','treatment','evaluating','completed','aborted') NOT NULL DEFAULT 'created' COMMENT '会话状态', - `started_at` datetime DEFAULT NULL COMMENT '开始时间', - `inquiry_completed_at` datetime DEFAULT NULL COMMENT '问诊完成时间', - `completed_at` datetime DEFAULT NULL COMMENT '完成时间', - `memory_key` varchar(128) DEFAULT NULL COMMENT '短期memory key', - `metadata` json DEFAULT NULL COMMENT '扩展数据', - `created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - PRIMARY KEY (`id`), - UNIQUE KEY `uk_session_code` (`session_code`), - KEY `idx_session_user_status` (`user_id`,`status`), - KEY `idx_session_case` (`case_id`), - KEY `idx_session_created` (`created_at`), - KEY `idx_training_sessions_source_case` (`source_case_id`), - CONSTRAINT `fk_session_case` FOREIGN KEY (`case_id`) REFERENCES `cases` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='训练会话表'; - -CREATE TABLE `session_runtime_messages` ( - `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '短期消息ID', - `session_id` int(11) NOT NULL COMMENT '会话ID', - `user_id` varchar(128) NOT NULL COMMENT '宿主系统用户ID', - `role` enum('doctor','patient','system','tool') NOT NULL COMMENT '角色', - `content` text NOT NULL COMMENT '消息内容', - `content_structured` json DEFAULT NULL COMMENT '结构化内容', - `sequence_no` int(11) NOT NULL COMMENT '会话内序号', - `expires_at` datetime DEFAULT NULL COMMENT '过期时间', - `created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - PRIMARY KEY (`id`), - KEY `idx_runtime_session_seq` (`session_id`,`sequence_no`), - KEY `idx_runtime_expires` (`expires_at`), - CONSTRAINT `fk_runtime_session` FOREIGN KEY (`session_id`) REFERENCES `training_sessions` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='短期会话消息调试表'; - -CREATE TABLE `session_orders` ( - `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '申请ID', - `session_id` int(11) NOT NULL COMMENT '会话ID', - `user_id` varchar(128) NOT NULL COMMENT '宿主系统用户ID', - `case_exam_item_id` int(11) NOT NULL COMMENT '病例检查项目ID', - `item_code` varchar(64) NOT NULL COMMENT '项目编码', - `item_name` varchar(128) NOT NULL COMMENT '项目名称', - `item_type` varchar(32) NOT NULL COMMENT '项目类型', - `result_text` text NOT NULL COMMENT '结果文本', - `result_structured` json DEFAULT NULL COMMENT '结构化结果', - `is_key` tinyint(1) DEFAULT 0 COMMENT '是否关键检查', - `is_abnormal` tinyint(1) DEFAULT 0 COMMENT '是否异常', - `ordered_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '申请时间', - PRIMARY KEY (`id`), - KEY `idx_order_session` (`session_id`), - KEY `idx_order_user_session` (`user_id`,`session_id`), - CONSTRAINT `fk_order_session` FOREIGN KEY (`session_id`) REFERENCES `training_sessions` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, - CONSTRAINT `fk_order_exam_item` FOREIGN KEY (`case_exam_item_id`) REFERENCES `case_exam_items` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='会话检查检验申请表'; - -CREATE TABLE `session_submissions` ( - `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '提交ID', - `session_id` int(11) NOT NULL COMMENT '会话ID', - `user_id` varchar(128) NOT NULL COMMENT '宿主系统用户ID', - `primary_diagnosis` text COMMENT '主要诊断', - `differential_diagnoses` json DEFAULT NULL COMMENT '鉴别诊断', - `diagnosis_basis` text COMMENT '诊断依据', - `treatment_principle` text COMMENT '治疗原则', - `treatment_measures` text COMMENT '治疗措施', - `risk_plan` text COMMENT '风险预案', - `communication` text COMMENT '沟通告知与健康教育', - `follow_up` text COMMENT '随访计划', - `diagnosis_submitted_at` datetime DEFAULT NULL COMMENT '诊断提交时间', - `treatment_submitted_at` datetime DEFAULT NULL COMMENT '治疗提交时间', - `created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - PRIMARY KEY (`id`), - UNIQUE KEY `uk_submission_session` (`session_id`), - KEY `idx_submission_user` (`user_id`), - CONSTRAINT `fk_submission_session` FOREIGN KEY (`session_id`) REFERENCES `training_sessions` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='会话诊断治疗提交表'; - -CREATE TABLE `knowledge_sources` ( - `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '知识来源ID', - `source_code` varchar(64) NOT NULL COMMENT '来源编码', - `source_name` varchar(255) NOT NULL COMMENT '来源名称', - `source_type` enum('national_standard','department_expert','exam_requirement','clinical_guideline','humanistic_care','other') NOT NULL COMMENT '来源类型', - `authority_level` int(11) DEFAULT 1 COMMENT '权威等级', - `is_active` tinyint(1) DEFAULT 1 COMMENT '是否启用', - `created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - PRIMARY KEY (`id`), - UNIQUE KEY `uk_source_code` (`source_code`), - KEY `idx_source_type` (`source_type`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='知识来源表'; - -CREATE TABLE `knowledge_documents` ( - `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '知识文档ID', - `source_id` int(11) NOT NULL COMMENT '来源ID', - `department_id` int(11) DEFAULT NULL COMMENT '科室ID', - `title` varchar(255) NOT NULL COMMENT '文档标题', - `task_type` varchar(64) DEFAULT NULL COMMENT '任务类型', - `summary` text COMMENT '摘要', - `file_path` varchar(512) DEFAULT NULL COMMENT '文件路径', - `is_active` tinyint(1) DEFAULT 1 COMMENT '是否启用', - `created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - PRIMARY KEY (`id`), - KEY `idx_doc_source` (`source_id`), - KEY `idx_doc_department_task` (`department_id`,`task_type`), - CONSTRAINT `fk_doc_source` FOREIGN KEY (`source_id`) REFERENCES `knowledge_sources` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, - CONSTRAINT `fk_doc_department` FOREIGN KEY (`department_id`) REFERENCES `departments` (`id`) ON DELETE SET NULL ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='知识文档表'; - -CREATE TABLE `knowledge_chunks` ( - `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '知识片段ID', - `document_id` int(11) NOT NULL COMMENT '文档ID', - `department_id` int(11) DEFAULT NULL COMMENT '科室ID', - `task_type` varchar(64) DEFAULT NULL COMMENT '任务类型', - `chunk_text` text NOT NULL COMMENT '片段内容', - `keywords` json DEFAULT NULL COMMENT '关键词', - `weight` decimal(5,2) DEFAULT 1.00 COMMENT '权重', - `is_active` tinyint(1) DEFAULT 1 COMMENT '是否启用', - `created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - PRIMARY KEY (`id`), - KEY `idx_chunk_doc` (`document_id`), - KEY `idx_chunk_department_task` (`department_id`,`task_type`), - FULLTEXT KEY `ft_chunk_text` (`chunk_text`), - CONSTRAINT `fk_chunk_doc` FOREIGN KEY (`document_id`) REFERENCES `knowledge_documents` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, - CONSTRAINT `fk_chunk_department` FOREIGN KEY (`department_id`) REFERENCES `departments` (`id`) ON DELETE SET NULL ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='知识片段表'; - -CREATE TABLE `evaluation_records` ( - `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '评价记录ID', - `evaluation_code` varchar(64) NOT NULL COMMENT '评价记录编码', - `session_id` int(11) NOT NULL COMMENT '会话ID', - `user_id` varchar(128) NOT NULL COMMENT '宿主系统用户ID', - `tenant_id` varchar(128) DEFAULT NULL COMMENT '租户或项目ID', - `case_id` int(11) NOT NULL COMMENT '病例ID', - `source_case_id` bigint DEFAULT NULL COMMENT '源库病例ID,保留 case_id 兼容旧报告表', - `training_type` varchar(64) NOT NULL COMMENT '训练类别', - `mode` varchar(64) NOT NULL COMMENT '训练模式', - `score_type` enum('percentage','five_point') NOT NULL COMMENT '分数类型', - `total_score` decimal(6,2) NOT NULL COMMENT '总分', - `dimension_scores` json NOT NULL COMMENT '维度评分', - `errors` json DEFAULT NULL COMMENT '错误分析', - `improvement_plan` json DEFAULT NULL COMMENT '改进方案', - `evidence_summary` json DEFAULT NULL COMMENT '评分依据摘要', - `guideline_refs` json DEFAULT NULL COMMENT '参考指南来源', - `overall_comment` text COMMENT '总体评价', - `llm_model` varchar(100) DEFAULT NULL COMMENT 'LLM模型', - `latency_metrics` json DEFAULT NULL COMMENT '耗时指标', - `pdf_file_path` varchar(512) DEFAULT NULL COMMENT 'PDF报告路径', - `status` enum('generated','exported','failed') DEFAULT 'generated' COMMENT '状态', - `created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - PRIMARY KEY (`id`), - UNIQUE KEY `uk_evaluation_code` (`evaluation_code`), - UNIQUE KEY `uk_evaluation_session` (`session_id`), - KEY `idx_eval_user_created` (`user_id`,`created_at`), - KEY `idx_eval_case` (`case_id`), - KEY `idx_eval_source_case` (`source_case_id`), - CONSTRAINT `fk_eval_session` FOREIGN KEY (`session_id`) REFERENCES `training_sessions` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, - CONSTRAINT `fk_eval_case` FOREIGN KEY (`case_id`) REFERENCES `cases` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='AI评价记录表'; - -CREATE TABLE `evaluation_report_exports` ( - `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '导出记录ID', - `evaluation_id` int(11) NOT NULL COMMENT '评价记录ID', - `user_id` varchar(128) NOT NULL COMMENT '宿主系统用户ID', - `file_path` varchar(512) NOT NULL COMMENT 'PDF文件路径', - `file_name` varchar(255) NOT NULL COMMENT 'PDF文件名', - `score_type` enum('percentage','five_point') NOT NULL COMMENT '分数类型', - `export_status` enum('success','failed') DEFAULT 'success' COMMENT '导出状态', - `exported_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '导出时间', - PRIMARY KEY (`id`), - KEY `idx_export_eval` (`evaluation_id`), - KEY `idx_export_user` (`user_id`,`exported_at`), - CONSTRAINT `fk_export_eval` FOREIGN KEY (`evaluation_id`) REFERENCES `evaluation_records` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='评价报告PDF导出记录表'; - -CREATE TABLE `prompt_templates` ( - `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '提示词模板ID', - `template_code` varchar(64) NOT NULL COMMENT '模板编码', - `agent_type` enum('patient','scoring','report','polish','hint','knowledge') NOT NULL COMMENT 'Agent类型', - `scene` varchar(64) NOT NULL COMMENT '场景', - `version_no` varchar(32) NOT NULL COMMENT '版本', - `model_type` enum('fast','reason') NOT NULL COMMENT '模型类型', - `output_format` enum('text','json') NOT NULL COMMENT '输出格式', - `file_path` varchar(512) NOT NULL COMMENT 'Markdown文件路径', - `is_active` tinyint(1) DEFAULT 1 COMMENT '是否启用', - `created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - PRIMARY KEY (`id`), - UNIQUE KEY `uk_prompt_code_version` (`template_code`,`version_no`), - KEY `idx_prompt_agent_scene` (`agent_type`,`scene`,`is_active`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='提示词模板元数据表'; - -CREATE TABLE `rubric_templates` ( - `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '评分规则ID', - `rubric_code` varchar(64) NOT NULL COMMENT '规则编码', - `rubric_name` varchar(128) NOT NULL COMMENT '规则名称', - `version_no` varchar(32) NOT NULL COMMENT '版本', - `department_id` int(11) DEFAULT NULL COMMENT '科室ID', - `training_type` varchar(64) NOT NULL COMMENT '训练类别', - `score_type` enum('percentage','five_point') NOT NULL COMMENT '分数类型', - `dimensions` json NOT NULL COMMENT '评分维度', - `is_active` tinyint(1) DEFAULT 1 COMMENT '是否启用', - `created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - PRIMARY KEY (`id`), - UNIQUE KEY `uk_rubric_code_version` (`rubric_code`,`version_no`), - KEY `idx_rubric_department_type` (`department_id`,`training_type`,`score_type`), - CONSTRAINT `fk_rubric_department` FOREIGN KEY (`department_id`) REFERENCES `departments` (`id`) ON DELETE SET NULL ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='评分规则表'; - -CREATE TABLE `training_record` ( - `id` bigint NOT NULL AUTO_INCREMENT COMMENT '训练记录ID', - `training_mode` varchar(50) NOT NULL COMMENT '训练模式:practice/teaching', - `case_type` varchar(30) NOT NULL COMMENT '病例分类', - `start_time` datetime(6) NOT NULL COMMENT '开始时间', - `end_time` datetime(6) DEFAULT NULL COMMENT '结束时间', - `duration_seconds` int DEFAULT NULL COMMENT '训练耗时秒', - `total_score` decimal(5,2) DEFAULT NULL COMMENT '总分', - `ai_score` decimal(5,2) DEFAULT NULL COMMENT 'AI评分', - `teacher_score` decimal(5,2) DEFAULT NULL COMMENT '教师评分', - `evaluation_level` varchar(20) NOT NULL DEFAULT '' COMMENT '评价等级', - `status` varchar(30) NOT NULL COMMENT '记录状态', - `feedback` longtext NOT NULL COMMENT '总体反馈', - `thinking_chain` longtext NOT NULL COMMENT '评价证据链摘要', - `diagnosis_path` longtext NOT NULL COMMENT '诊断路径摘要', - `wrong_points` json NOT NULL COMMENT '错误点', - `missed_questions` json NOT NULL COMMENT '遗漏问题', - `recommendation_result` json NOT NULL COMMENT '改进建议', - `ai_feedback_structured` json NOT NULL COMMENT 'AI结构化反馈', - `osce_station_score` json NOT NULL COMMENT 'OSCE评分', - `interruption_count` int NOT NULL DEFAULT 0 COMMENT '中断次数', - `emotion_analysis` json NOT NULL COMMENT '情绪分析', - `prompt_version` varchar(50) NOT NULL DEFAULT 'v1' COMMENT '提示词版本', - `rag_context_version` varchar(50) NOT NULL DEFAULT 'none' COMMENT '指南检索版本', - `case_id` bigint NOT NULL COMMENT '病例ID,对应 case_base.id', - `teacher_id` bigint DEFAULT NULL COMMENT '教师ID', - `user_id` bigint DEFAULT NULL COMMENT '源系统数字用户ID;宿主传字符串时为空', - `external_user_id` varchar(128) NOT NULL COMMENT '宿主系统传入的 user_id', - `session_id` bigint DEFAULT NULL COMMENT '本系统训练会话ID', - `evaluation_record_id` bigint DEFAULT NULL COMMENT '兼容评价记录ID', - `score_type` varchar(20) NOT NULL DEFAULT 'percentage' COMMENT '分数类型', - `pdf_file_path` varchar(512) DEFAULT NULL COMMENT 'PDF报告路径', - `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT '创建时间', - `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6) COMMENT '更新时间', - PRIMARY KEY (`id`), - UNIQUE KEY `uk_training_record_session` (`session_id`), - KEY `idx_training_record_external_user` (`external_user_id`, `created_at`), - KEY `idx_training_record_case` (`case_id`), - KEY `idx_training_record_status` (`status`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='源库训练记录表'; - -CREATE TABLE `user_learning_profiles` ( - `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '学习档案ID', - `user_id` varchar(128) NOT NULL COMMENT '宿主系统用户ID', - `tenant_id` varchar(128) DEFAULT NULL COMMENT '租户或项目ID', - `total_evaluations` int(11) DEFAULT 0 COMMENT '评价次数', - `avg_score_percentage` decimal(6,2) DEFAULT NULL COMMENT '百分制平均分', - `avg_score_five_point` decimal(4,2) DEFAULT NULL COMMENT '五分制平均分', - `weak_dimensions` json DEFAULT NULL COMMENT '薄弱维度', - `last_evaluation_id` int(11) DEFAULT NULL COMMENT '最近评价记录ID', - `last_trained_at` datetime DEFAULT NULL COMMENT '最近训练时间', - `created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - PRIMARY KEY (`id`), - UNIQUE KEY `uk_profile_user_tenant` (`user_id`,`tenant_id`), - KEY `idx_profile_last_trained` (`last_trained_at`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户学习档案表'; - -CREATE TABLE `audit_logs` ( - `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '审计日志ID', - `user_id` varchar(128) DEFAULT NULL COMMENT '宿主系统用户ID', - `tenant_id` varchar(128) DEFAULT NULL COMMENT '租户或项目ID', - `session_id` int(11) DEFAULT NULL COMMENT '会话ID', - `action` varchar(64) NOT NULL COMMENT '动作', - `resource_type` varchar(64) NOT NULL COMMENT '资源类型', - `resource_id` varchar(128) DEFAULT NULL COMMENT '资源ID', - `request_id` varchar(128) DEFAULT NULL COMMENT '请求ID', - `ip_address` varchar(64) DEFAULT NULL COMMENT 'IP地址', - `user_agent` varchar(512) DEFAULT NULL COMMENT 'User-Agent', - `metadata` json DEFAULT NULL COMMENT '元数据', - `created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - PRIMARY KEY (`id`), - KEY `idx_audit_user_created` (`user_id`,`created_at`), - KEY `idx_audit_session` (`session_id`), - KEY `idx_audit_action_created` (`action`,`created_at`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='审计日志表'; - -SET FOREIGN_KEY_CHECKS = 1; diff --git a/docs/sql/seed_pediatric_pneumonia.sql b/docs/sql/seed_pediatric_pneumonia.sql deleted file mode 100644 index 7f3d907..0000000 --- a/docs/sql/seed_pediatric_pneumonia.sql +++ /dev/null @@ -1,197 +0,0 @@ -USE `medical_consultation_agent`; - -SET NAMES utf8mb4; - -INSERT INTO `departments` (`id`, `name`, `code`, `parent_id`, `sort_order`, `is_active`) -VALUES (1, '儿科', 'PEDIATRICS', NULL, 1, 1) -ON DUPLICATE KEY UPDATE `name` = VALUES(`name`), `is_active` = VALUES(`is_active`); - -INSERT INTO `users` (`id`, `external_user_id`, `display_name`) -VALUES (1, 'system_seed', '系统种子数据') -ON DUPLICATE KEY UPDATE `display_name` = VALUES(`display_name`); - -INSERT INTO `cases` ( - `id`, - `case_code`, - `department_id`, - `title`, - `difficulty`, - `patient_name`, - `patient_age`, - `patient_gender`, - `patient_occupation`, - `chief_complaint`, - `present_illness`, - `past_history`, - `personal_history`, - `family_history`, - `physical_exam`, - `auxiliary_exam`, - `diagnosis_primary`, - `diagnosis_differential`, - `diagnosis_basis`, - `treatment_plan`, - `consultation_config`, - `inquiry_options`, - `knowledge_videos`, - `quiz_questions`, - `key_symptoms`, - `key_exams`, - `key_points`, - `evidence_reasoning_chain`, - `assessment_config`, - `ai_patient_profile`, - `patient_opening`, - `hidden_patient_info`, - `has_teaching_video`, - `has_knowledge_points`, - `has_quiz`, - `source_pdf_name`, - `supported_training_type`, - `supported_mode`, - `is_active`, - `created_by` -) VALUES ( - 1, - 'PED_PNEUMONIA_001', - 1, - '支气管肺炎 - 6岁男性患儿', - 'medium', - '王**', - 6, - 'male', - '学龄前儿童', - '发热、咳嗽4天,喘息1天。', - '患儿4天前出现发热,体温最高39.2℃,伴阵发性咳嗽,夜间明显。1天前出现喘息及精神稍差,在社区门诊口服退热药后体温反复。无惊厥,无呕吐腹泻。', - '既往体健,出生足月,疫苗接种基本完整;有湿疹病史,无明确哮喘诊断史;无药物过敏史。', - '与父母同住,近期幼儿园同班多人有上呼吸道感染;被动吸烟暴露。', - '母亲有过敏性鼻炎史,否认家族遗传代谢病史。', - JSON_OBJECT( - 'general', JSON_OBJECT('temperature','38.7℃','pulse','126次/分','respiration','30次/分','blood_pressure','96/60mmHg','mental_status','精神稍差','nasal_flaring','轻度鼻翼煽动'), - 'lung', '双肺呼吸音粗,可闻及散在湿啰音及少量哮鸣音', - 'heart', '心音有力', - 'abdomen', '腹软,无压痛,肝脾未触及肿大,肠鸣音正常', - 'neuro', '神经系统查体未见定位体征' - ), - JSON_ARRAY( - JSON_OBJECT('name','血常规','result','WBC 12.4×10^9/L,中性粒细胞72%','is_abnormal',true,'is_key',false), - JSON_OBJECT('name','CRP','result','32 mg/L','is_abnormal',true,'is_key',false), - JSON_OBJECT('name','胸片','result','双下肺纹理增多,右下肺片状模糊影','is_abnormal',true,'is_key',true), - JSON_OBJECT('name','肺炎支原体抗体IgM','result','弱阳性','is_abnormal',true,'is_key',false), - JSON_OBJECT('name','血氧饱和度','result','室内空气 SpO2 94%','is_abnormal',true,'is_key',true) - ), - '支气管肺炎', - JSON_ARRAY('毛细支气管炎','支气管哮喘急性发作','肺结核'), - '患儿发热咳嗽伴喘息,肺部听诊有湿啰音,胸片示右下肺片状模糊影,炎症指标升高,符合支气管肺炎诊断。', - JSON_OBJECT( - 'principle', JSON_ARRAY('抗感染','止咳平喘','改善氧合','严密观察病情变化'), - 'measures', JSON_ARRAY('根据病情和病原学选择抗感染治疗','必要时雾化吸入缓解喘息','监测体温、呼吸、血氧饱和度','评估是否住院观察'), - 'risk_plan', JSON_ARRAY('关注低氧','关注呼吸困难加重','关注持续高热','关注精神反应差'), - 'communication', JSON_ARRAY('向家属说明病情','说明观察指标','说明用药注意事项','说明复诊指征') - ), - JSON_OBJECT(), - JSON_ARRAY(), - JSON_ARRAY(), - JSON_ARRAY( - JSON_OBJECT('question','该患儿的首要诊断是?','type','single','answer','支气管肺炎'), - JSON_OBJECT('question','本病例关键检查包括哪些?','type','multiple','answer',JSON_ARRAY('肺部湿啰音','胸片异常','血氧饱和度下降')) - ), - JSON_ARRAY('发热','咳嗽','喘息'), - JSON_ARRAY('肺部湿啰音','胸片异常','血氧饱和度下降'), - JSON_ARRAY('问诊完整性','儿科查体规范','关键症状识别','诊断准确性','治疗计划合理性'), - JSON_OBJECT( - 'clinical_prediction_rule','根据患儿症状(发热、咳嗽、喘息)及体征(肺部湿啰音),结合实验室炎症指标升高和胸部影像学典型新发浸润影,符合儿童支气管肺炎临床诊断标准,参考《儿童社区获得性肺炎诊疗规范(2019年版)》。', - 'severity_assessment', JSON_ARRAY( - '问题识别:判断患儿是否为重症肺炎,以决定治疗地点。', - '证据检索与整合:检索儿童肺炎严重程度评估工具,结合儿童CURB-65改良标准或WHO儿童肺炎分级。', - '应用评分:本例呼吸30次/分、SpO2 94%、无意识障碍、血压正常,尚未达到重症肺炎阈值。', - '决策:判断为非重症肺炎,因血氧偏低和喘息存在,纳入住院观察和严密随访讨论。' - ) - ), - JSON_OBJECT( - 'score_dimensions', JSON_ARRAY('信息获取','分析推理','处置决策','沟通人文','临床整合'), - 'must_cover', JSON_ARRAY('发热持续时间','咳嗽特点','喘息出现时间','精神食欲睡眠','疫苗接种','过敏史','既往喘息史','肺部听诊','胸片','血氧饱和度') - ), - JSON_OBJECT( - 'role','患儿家属', - 'personality','担心、焦虑,但配合问诊', - 'speech_style','以家长口吻回答,描述孩子症状,回答简洁', - 'visible_info', JSON_ARRAY('发热咳嗽4天','喘息1天','精神稍差'), - 'rules', JSON_ARRAY('只回答病例内事实','不主动给出诊断','不一次性泄露所有隐藏信息') - ), - '家长:医生,孩子发烧咳嗽好几天了,昨天开始喘得厉害,精神也不太好。', - JSON_ARRAY('近期幼儿园同班多人上呼吸道感染','被动吸烟暴露','母亲有过敏性鼻炎史','有湿疹病史','无明确哮喘诊断史'), - 0, - 1, - 1, - '儿科 病例样例(SOAP+循证).pdf', - 'diagnosis_treatment', - 'free_chat', - 1, - 1 -) ON DUPLICATE KEY UPDATE - `title` = VALUES(`title`), - `updated_at` = CURRENT_TIMESTAMP; - -INSERT INTO `case_exam_items` (`case_id`, `item_code`, `item_name`, `item_type`, `category`, `result_text`, `result_structured`, `is_key`, `is_abnormal`, `score_weight`, `display_order`) -VALUES -(1, 'blood_routine', '血常规', 'lab', '炎症指标', 'WBC 12.4×10^9/L,中性粒细胞72%。', JSON_OBJECT('WBC','12.4×10^9/L','neutrophil_percent','72%'), 0, 1, 3.00, 1), -(1, 'crp', 'CRP', 'lab', '炎症指标', 'CRP 32 mg/L。', JSON_OBJECT('CRP','32 mg/L'), 0, 1, 2.00, 2), -(1, 'chest_xray', '胸片', 'imaging', '影像学', '双下肺纹理增多,右下肺片状模糊影。', JSON_OBJECT('finding','右下肺片状模糊影'), 1, 1, 5.00, 3), -(1, 'mp_igm', '肺炎支原体抗体IgM', 'lab', '病原学', '肺炎支原体抗体IgM 弱阳性。', JSON_OBJECT('mp_igm','弱阳性'), 0, 1, 2.00, 4), -(1, 'spo2', '血氧饱和度', 'vital_sign', '生命体征', '室内空气 SpO2 94%。', JSON_OBJECT('SpO2','94%','condition','室内空气'), 1, 1, 5.00, 5) -ON DUPLICATE KEY UPDATE - `result_text` = VALUES(`result_text`), - `result_structured` = VALUES(`result_structured`); - -INSERT INTO `knowledge_sources` (`id`, `source_code`, `source_name`, `source_type`, `authority_level`, `is_active`) -VALUES -(1, 'CAP_2019', '儿童社区获得性肺炎诊疗规范(2019年版)', 'clinical_guideline', 5, 1), -(2, 'HUMANISTIC_CARE', '卫健委人文关怀与医患沟通要求', 'humanistic_care', 4, 1), -(3, 'PED_EXAM_REQ', '儿科期末考试问诊与诊疗评分要求', 'exam_requirement', 3, 1) -ON DUPLICATE KEY UPDATE `source_name` = VALUES(`source_name`); - -INSERT INTO `knowledge_documents` (`id`, `source_id`, `department_id`, `title`, `task_type`, `summary`, `is_active`) -VALUES -(1, 1, 1, '儿童社区获得性肺炎诊疗规范(2019年版)评分参考', 'diagnosis_treatment', '儿童肺炎诊断、严重程度评估和治疗原则参考。', 1), -(2, 2, 1, '儿科场景人文关怀评分参考', 'diagnosis_treatment', '儿童患者家属沟通、病情告知和健康教育评分参考。', 1), -(3, 3, 1, '儿科问诊考试评分要求', 'diagnosis_treatment', '儿科病史采集、查体、诊断依据和治疗计划评分参考。', 1) -ON DUPLICATE KEY UPDATE `summary` = VALUES(`summary`); - -INSERT INTO `knowledge_chunks` (`document_id`, `department_id`, `task_type`, `chunk_text`, `keywords`, `weight`, `is_active`) -VALUES -(1, 1, 'diagnosis_treatment', '儿童支气管肺炎诊断需结合发热、咳嗽、喘息等症状,肺部湿啰音等体征,炎症指标升高以及胸部影像学新发浸润影。', JSON_ARRAY('支气管肺炎','发热','咳嗽','喘息','胸片'), 5.00, 1), -(1, 1, 'diagnosis_treatment', '儿童肺炎严重程度评估关注呼吸频率、血氧饱和度、意识状态、血压和全身情况。SpO2 94%属于需要密切观察的临界表现。', JSON_ARRAY('严重程度','SpO2','呼吸频率','血氧'), 4.50, 1), -(2, 1, 'diagnosis_treatment', '儿科沟通需面向家属解释病情、观察指标、用药注意事项、复诊指征,并回应家属焦虑。', JSON_ARRAY('人文关怀','家属沟通','健康教育'), 4.00, 1), -(3, 1, 'diagnosis_treatment', '儿科病例考核重点包括问诊完整性、儿科查体规范、关键症状识别、诊断准确性、治疗计划合理性。', JSON_ARRAY('考核','问诊完整性','治疗计划'), 4.00, 1); - -INSERT INTO `prompt_templates` (`template_code`, `agent_type`, `scene`, `version_no`, `model_type`, `output_format`, `file_path`, `is_active`) -VALUES -('patient_free_chat', 'patient', 'free_chat', 'v1', 'fast', 'text', 'backend/app/prompts/patient/free_chat.md', 1), -('patient_practice', 'patient', 'practice', 'v1', 'fast', 'text', 'backend/app/prompts/patient/practice.md', 1), -('patient_novice', 'patient', 'novice', 'v1', 'fast', 'text', 'backend/app/prompts/patient/novice.md', 1), -('patient_teaching', 'patient', 'teaching', 'v1', 'fast', 'text', 'backend/app/prompts/patient/teaching.md', 1), -('scoring_default_percentage', 'scoring', 'default_percentage', 'v1', 'reason', 'json', 'backend/app/prompts/scoring/default_percentage.md', 1), -('scoring_default_five_point', 'scoring', 'default_five_point', 'v1', 'reason', 'json', 'backend/app/prompts/scoring/default_five_point.md', 1), -('scoring_pediatrics_pneumonia', 'scoring', 'pediatrics_pneumonia', 'v1', 'reason', 'json', 'backend/app/prompts/scoring/pediatrics_pneumonia.md', 1), -('report_evaluation', 'report', 'evaluation_report', 'v1', 'fast', 'json', 'backend/app/prompts/report/evaluation_report.md', 1), -('novice_hint', 'hint', 'novice', 'v1', 'fast', 'text', 'backend/app/prompts/hint/novice_hint.md', 1), -('doctor_question_polish', 'polish', 'doctor_question', 'v1', 'fast', 'text', 'backend/app/prompts/polish/doctor_question_polish.md', 1), -('guideline_search_query', 'knowledge', 'guideline_search', 'v1', 'fast', 'json', 'backend/app/prompts/knowledge/guideline_search_query.md', 1) -ON DUPLICATE KEY UPDATE `file_path` = VALUES(`file_path`); - -INSERT INTO `rubric_templates` (`rubric_code`, `rubric_name`, `version_no`, `department_id`, `training_type`, `score_type`, `dimensions`, `is_active`) -VALUES -('PED_DIAG_TX_PERCENTAGE', '儿科诊疗训练百分制评分规则', 'v1', 1, 'diagnosis_treatment', 'percentage', - JSON_ARRAY( - JSON_OBJECT('dimension','信息获取','max_score',25,'items',JSON_ARRAY('问诊完整性','查体针对性','辅助检查')), - JSON_OBJECT('dimension','分析推理','max_score',25,'items',JSON_ARRAY('病例归纳','诊断准确性','鉴别诊断')), - JSON_OBJECT('dimension','处置决策','max_score',20,'items',JSON_ARRAY('检查计划','治疗方案','风险预案')), - JSON_OBJECT('dimension','沟通人文','max_score',15,'items',JSON_ARRAY('信息告知','共情回应','共识达成')), - JSON_OBJECT('dimension','临床整合','max_score',15,'items',JSON_ARRAY('时间管理','流程连贯','整体思维')) - ), 1), -('PED_DIAG_TX_FIVE_POINT', '儿科诊疗训练五分制评分规则', 'v1', 1, 'diagnosis_treatment', 'five_point', - JSON_ARRAY( - JSON_OBJECT('dimension','综合表现','max_score',5,'conversion','percentage / 20') - ), 1) -ON DUPLICATE KEY UPDATE `dimensions` = VALUES(`dimensions`); diff --git a/frontend/.gitignore b/frontend/.gitignore deleted file mode 100644 index cbc94a7..0000000 --- a/frontend/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -node_modules/ -dist/ -.npm-cache/ -*.log -*.tsbuildinfo -vite.config.js -vite.config.d.ts diff --git a/frontend/README.md b/frontend/README.md deleted file mode 100644 index fe7a044..0000000 --- a/frontend/README.md +++ /dev/null @@ -1,81 +0,0 @@ -# 医疗问诊 Agent Vue Demo - -这是第一版 Demo 的 Vue 3 测试前端,用于展示和验证当前 FastAPI 后端已实现的问诊训练闭环。它不是最终生产 UI,但页面结构、接口封装和状态管理已按后续正式前端扩展保留边界。 - -## 技术栈 - -- Vue 3 + Vite + TypeScript -- Pinia -- Vue Router -- Axios + Fetch SSE -- 原生 CSS,移动端优先 - -## 启动 - -```powershell -cd D:\Code\newfounder\medical-consultation-agent\frontend -npm.cmd install -npm.cmd run dev -- --host 127.0.0.1 --port 5173 -``` - -访问: - -```text -http://127.0.0.1:5173 -``` - -构建: - -```powershell -npm.cmd run build -``` - -## 后端配置 - -默认 API: - -```text -http://127.0.0.1:8000/api/v1 -``` - -所有业务请求都会携带: - -- `X-User-Id` -- `X-Entry-Scene` - -入口页可修改 `user_id`、API Base 和入口场景。默认 `user_id` 为 `demo_user_001`。 - -## 页面与功能 - -| 页面 | 路由 | 主要功能 | -|---|---|---| -| 入口 | `#/` | 配置 user_id/API Base,调用 Agent Hello | -| 病例 | `#/cases` | 病例列表、病例详情、开始训练 | -| 病例导入 | `#/import` | 上传接口解析后的 SQL,预检并确认写入病例源表 | -| 会话配置 | `#/session` | 选择模式、评分类型并创建会话 | -| 问诊 | `#/chat` | 普通/流式 Chat、查看提示、检查申请、完成问诊 | -| 提交 | `#/submit` | 提交诊断和治疗方案 | -| 报告 | `#/report` | 生成评价、查看评分、导出 PDF | -| 历史 | `#/history` | 查询当前 user_id 的历史评价 | -| LLM 测试 | `#/llm-test` | 测试 Fast/Reason 模型耗时 | - -## 测试流程 - -1. 入口页确认连接成功。 -2. 需要新增病例时进入 `#/import`,上传 `.sql` 文件并先执行“解析检查”,确认通过后再“确认导入”。 -3. 病例页选择“支气管肺炎 - 6岁男性患儿”或刚导入的新病例。 -4. 点击“开始训练”,创建练习模式会话。 -5. Chat 中提问并查看 AI 病人流式回复。 -6. 点击“查看提示”验证 Hint Agent。 -7. 申请血常规、CRP、胸片、血氧饱和度、肺部体格检查。 -8. 完成问诊,提交诊断和治疗方案。 -9. 生成 AI 评价报告并导出 PDF。 -10. 历史页刷新并查看报告详情。 - -## 交互边界 - -- 前端不保存长期聊天历史。 -- Chat 消息只保存在 Pinia 当前状态中,刷新页面后以当前后端会话状态为准。 -- 诊断和治疗表单默认清空;演示模板需要手动点击填入。 -- 检查项目按 `item_code` 去重,同一会话不会重复申请同一检查。 -- 真实模型、mock 和 fallback 状态由后端 `.env` 决定,前端只展示后端返回结果。 diff --git a/frontend/index.html b/frontend/index.html deleted file mode 100644 index 1ea8518..0000000 --- a/frontend/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - 医疗问诊 Agent Demo - - -
- - - diff --git a/frontend/package-lock.json b/frontend/package-lock.json deleted file mode 100644 index fa2e2ea..0000000 --- a/frontend/package-lock.json +++ /dev/null @@ -1,1853 +0,0 @@ -{ - "name": "medical-consultation-agent-frontend", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "medical-consultation-agent-frontend", - "version": "0.1.0", - "dependencies": { - "@vitejs/plugin-vue": "^5.2.4", - "axios": "^1.7.9", - "lucide-vue-next": "^0.468.0", - "pinia": "^2.3.0", - "vue": "^3.5.13", - "vue-router": "^4.5.0" - }, - "devDependencies": { - "typescript": "^5.7.2", - "vite": "^6.0.3", - "vue-tsc": "^2.1.10" - } - }, - "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/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/@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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "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/@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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "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==", - "license": "MIT" - }, - "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==", - "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/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/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/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/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/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/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/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/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/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.2", - "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz", - "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.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/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", - "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/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/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/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==", - "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/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/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/lucide-vue-next": { - "version": "0.468.0", - "resolved": "https://registry.npmmirror.com/lucide-vue-next/-/lucide-vue-next-0.468.0.tgz", - "integrity": "sha512-quV/6T8YB1XK0VOEnebg3Byd8Rsan5/m95cvjnuHV4vcS3qEnLAybkrSh0hk3ppavx+V7R1PjNW+mGDvcBdz4A==", - "license": "ISC", - "peerDependencies": { - "vue": ">=3.0.1" - } - }, - "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/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/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-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/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/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/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/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==", - "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/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/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/rollup": { - "version": "4.60.4", - "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.60.4.tgz", - "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", - "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/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/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/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/vite": { - "version": "6.4.2", - "resolved": "https://registry.npmmirror.com/vite/-/vite-6.4.2.tgz", - "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", - "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-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-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" - } - } - } -} diff --git a/frontend/package.json b/frontend/package.json deleted file mode 100644 index 78bfda6..0000000 --- a/frontend/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "medical-consultation-agent-frontend", - "version": "0.1.0", - "private": true, - "type": "module", - "scripts": { - "dev": "vite --host 127.0.0.1", - "build": "vue-tsc --noEmit && vite build", - "preview": "vite preview --host 127.0.0.1" - }, - "dependencies": { - "@vitejs/plugin-vue": "^5.2.4", - "axios": "^1.7.9", - "lucide-vue-next": "^0.468.0", - "pinia": "^2.3.0", - "vue": "^3.5.13", - "vue-router": "^4.5.0" - }, - "devDependencies": { - "typescript": "^5.7.2", - "vite": "^6.0.3", - "vue-tsc": "^2.1.10" - } -} diff --git a/frontend/src/App.vue b/frontend/src/App.vue deleted file mode 100644 index f6a759b..0000000 --- a/frontend/src/App.vue +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/frontend/src/components/AppShell.vue b/frontend/src/components/AppShell.vue deleted file mode 100644 index 7c0d31b..0000000 --- a/frontend/src/components/AppShell.vue +++ /dev/null @@ -1,81 +0,0 @@ - - - diff --git a/frontend/src/components/CaseCard.vue b/frontend/src/components/CaseCard.vue deleted file mode 100644 index 4e6159a..0000000 --- a/frontend/src/components/CaseCard.vue +++ /dev/null @@ -1,37 +0,0 @@ - - - diff --git a/frontend/src/components/ChatBubble.vue b/frontend/src/components/ChatBubble.vue deleted file mode 100644 index d54b11d..0000000 --- a/frontend/src/components/ChatBubble.vue +++ /dev/null @@ -1,49 +0,0 @@ - - - diff --git a/frontend/src/components/ExamOrderPanel.vue b/frontend/src/components/ExamOrderPanel.vue deleted file mode 100644 index 7cd6276..0000000 --- a/frontend/src/components/ExamOrderPanel.vue +++ /dev/null @@ -1,75 +0,0 @@ - - - diff --git a/frontend/src/components/FlowStepper.vue b/frontend/src/components/FlowStepper.vue deleted file mode 100644 index 4dbfc7a..0000000 --- a/frontend/src/components/FlowStepper.vue +++ /dev/null @@ -1,46 +0,0 @@ - - - diff --git a/frontend/src/components/ReportPanel.vue b/frontend/src/components/ReportPanel.vue deleted file mode 100644 index 9fa0076..0000000 --- a/frontend/src/components/ReportPanel.vue +++ /dev/null @@ -1,144 +0,0 @@ - - - diff --git a/frontend/src/main.ts b/frontend/src/main.ts deleted file mode 100644 index eb942fe..0000000 --- a/frontend/src/main.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { createPinia } from "pinia"; -import { createApp } from "vue"; - -import App from "./App.vue"; -import router from "./router"; -import "./styles/main.css"; - -createApp(App).use(createPinia()).use(router).mount("#app"); diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts deleted file mode 100644 index 5f3987a..0000000 --- a/frontend/src/router/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { createRouter, createWebHashHistory } from "vue-router"; - -import CasesView from "../views/CasesView.vue"; -import ChatView from "../views/ChatView.vue"; -import HistoryView from "../views/HistoryView.vue"; -import HomeView from "../views/HomeView.vue"; -import ImportCaseView from "../views/ImportCaseView.vue"; -import LlmTestView from "../views/LlmTestView.vue"; -import ReportView from "../views/ReportView.vue"; -import SessionView from "../views/SessionView.vue"; -import SubmitView from "../views/SubmitView.vue"; - -const router = createRouter({ - history: createWebHashHistory(), - routes: [ - { path: "/", name: "home", component: HomeView }, - { path: "/cases", name: "cases", component: CasesView }, - { path: "/import", name: "import", component: ImportCaseView }, - { path: "/session", name: "session", component: SessionView }, - { path: "/chat", name: "chat", component: ChatView }, - { path: "/submit", name: "submit", component: SubmitView }, - { path: "/report", name: "report", component: ReportView }, - { path: "/history", name: "history", component: HistoryView }, - { path: "/llm-test", name: "llm-test", component: LlmTestView }, - ], -}); - -export default router; diff --git a/frontend/src/services/apiClient.ts b/frontend/src/services/apiClient.ts deleted file mode 100644 index c2bac62..0000000 --- a/frontend/src/services/apiClient.ts +++ /dev/null @@ -1,263 +0,0 @@ -import axios, { AxiosError } from "axios"; - -import type { - AgentHello, - ApiEnvelope, - CaseDetail, - CaseListItem, - ChatResponse, - CreateSessionPayload, - DiagnosisPayload, - EvaluationDetail, - EvaluationListItem, - EvaluationReport, - ExportPdfResponse, - LlmTestResponse, - OrderItem, - OrderResult, - RequestContext, - ScoreType, - TrainingSession, - TreatmentPayload, - HintResponse, - CaseDeletePreview, - CaseDeleteResponse, - CaseSqlImportApply, - CaseSqlImportPreview, -} from "../types/api"; - -function headers(ctx: RequestContext) { - return { - "Content-Type": "application/json", - "X-User-Id": ctx.userId, - "X-Entry-Scene": ctx.entryScene, - }; -} - -function normalizeBaseUrl(baseUrl: string) { - return baseUrl.replace(/\/$/, ""); -} - -function unwrap(envelope: ApiEnvelope): T { - if (envelope.code !== "OK") { - throw new Error(envelope.message || envelope.code); - } - return envelope.data; -} - -function toUserFacingError(error: unknown): Error { - if (axios.isAxiosError(error)) { - const axiosError = error as AxiosError>; - const message = axiosError.response?.data?.message || axiosError.message || "接口请求失败"; - return new Error(message); - } - if (error instanceof Error) { - return error; - } - return new Error("未知错误"); -} - -async function get(ctx: RequestContext, path: string, params?: Record): Promise { - try { - const response = await axios.get>(`${normalizeBaseUrl(ctx.apiBaseUrl)}${path}`, { - headers: headers(ctx), - params, - timeout: 30000, - }); - return unwrap(response.data); - } catch (error) { - throw toUserFacingError(error); - } -} - -async function post(ctx: RequestContext, path: string, body?: unknown): Promise { - try { - const response = await axios.post>(`${normalizeBaseUrl(ctx.apiBaseUrl)}${path}`, body ?? {}, { - headers: headers(ctx), - timeout: 90000, - }); - return unwrap(response.data); - } catch (error) { - throw toUserFacingError(error); - } -} - -async function del(ctx: RequestContext, path: string, body?: unknown): Promise { - try { - const response = await axios.delete>(`${normalizeBaseUrl(ctx.apiBaseUrl)}${path}`, { - headers: headers(ctx), - data: body ?? {}, - timeout: 30000, - }); - return unwrap(response.data); - } catch (error) { - throw toUserFacingError(error); - } -} - -async function postFile(ctx: RequestContext, path: string, file: File): Promise { - const form = new FormData(); - form.append("file", file); - try { - const response = await axios.post>(`${normalizeBaseUrl(ctx.apiBaseUrl)}${path}`, form, { - headers: { - "X-User-Id": ctx.userId, - "X-Entry-Scene": ctx.entryScene, - }, - timeout: 90000, - }); - return unwrap(response.data); - } catch (error) { - throw toUserFacingError(error); - } -} - -export const apiClient = { - hello: (ctx: RequestContext) => get(ctx, "/agent/hello"), - listCases: (ctx: RequestContext) => get<{ items: CaseListItem[] }>(ctx, "/cases"), - getCaseDetail: (ctx: RequestContext, caseId: number) => get(ctx, `/cases/${caseId}`), - getCaseDeletePreview: (ctx: RequestContext, caseId: number) => - get(ctx, `/cases/${caseId}/delete-preview`), - deleteCase: (ctx: RequestContext, caseId: number) => - del(ctx, `/cases/${caseId}`, { confirm: true, delete_training_data: true }), - createSession: (ctx: RequestContext, payload: CreateSessionPayload) => - post(ctx, "/sessions", payload), - chat: (ctx: RequestContext, sessionId: number, message: string) => - post(ctx, `/sessions/${sessionId}/chat`, { message }), - hints: (ctx: RequestContext, sessionId: number, lastUserMessage?: string) => - post(ctx, `/sessions/${sessionId}/hints`, { - last_user_message: lastUserMessage || null, - scope: "current_conversation", - }), - listOrderItems: (ctx: RequestContext, sessionId: number) => - get<{ items: OrderItem[] }>(ctx, `/sessions/${sessionId}/order-items`), - createOrder: (ctx: RequestContext, sessionId: number, itemCode: string) => - post(ctx, `/sessions/${sessionId}/orders`, { item_code: itemCode }), - completeInquiry: (ctx: RequestContext, sessionId: number) => - post<{ session_id: number; status: string }>(ctx, `/sessions/${sessionId}/complete-inquiry`), - submitDiagnosis: (ctx: RequestContext, sessionId: number, payload: DiagnosisPayload) => - post<{ status: string }>(ctx, `/sessions/${sessionId}/diagnosis`, payload), - submitTreatment: (ctx: RequestContext, sessionId: number, payload: TreatmentPayload) => - post<{ status: string }>(ctx, `/sessions/${sessionId}/treatment`, payload), - createEvaluation: (ctx: RequestContext, sessionId: number, scoreType: ScoreType) => - post(ctx, `/sessions/${sessionId}/evaluation`, { score_type: scoreType }), - listEvaluations: (ctx: RequestContext) => get<{ items: EvaluationListItem[] }>(ctx, "/evaluations"), - getEvaluationDetail: (ctx: RequestContext, evaluationId: number) => - get(ctx, `/evaluations/${evaluationId}`), - exportPdf: (ctx: RequestContext, evaluationId: number) => - post(ctx, `/evaluations/${evaluationId}/export-pdf`), - llmFast: (ctx: RequestContext, message: string) => post(ctx, "/llm/test/deepseek-fast", { message }), - llmReason: (ctx: RequestContext, message: string) => - post(ctx, "/llm/test/deepseek-reason", { message }), - previewCaseSql: (ctx: RequestContext, file: File) => - postFile(ctx, "/imports/case-sql/preview", file), - applyCaseSql: (ctx: RequestContext, file: File) => - postFile(ctx, "/imports/case-sql/apply", file), -}; - -export interface StreamChatResult { - firstTokenMs?: number | null; - latencyMs?: number | null; - model?: string | null; - fallbackUsed?: boolean; -} - -interface ParsedSseEvent { - event: string; - data: Record; -} - -export function parseSseEvents(buffer: string): { events: ParsedSseEvent[]; rest: string } { - const normalized = buffer.replace(/\r\n/g, "\n"); - const blocks = normalized.split("\n\n"); - const rest = blocks.pop() ?? ""; - const events: ParsedSseEvent[] = []; - for (const block of blocks) { - const lines = block.split("\n").filter(Boolean); - const event = lines.find((line) => line.startsWith("event:"))?.replace("event:", "").trim() || "message"; - const dataText = lines - .filter((line) => line.startsWith("data:")) - .map((line) => line.replace("data:", "").trim()) - .join("\n"); - if (!dataText) { - continue; - } - try { - events.push({ event, data: JSON.parse(dataText) as Record }); - } catch { - events.push({ event: "error", data: { message: "流式问诊返回数据解析失败" } }); - } - } - return { events, rest }; -} - -export async function streamChat( - ctx: RequestContext, - sessionId: number, - message: string, - onDelta: (delta: string) => void, -): Promise { - const controller = new AbortController(); - const timeoutId = window.setTimeout(() => controller.abort(), 60000); - try { - console.info("[chat] stream start", { sessionId, messageLength: message.length }); - const response = await fetch(`${normalizeBaseUrl(ctx.apiBaseUrl)}/sessions/${sessionId}/chat/stream`, { - method: "POST", - headers: headers(ctx), - body: JSON.stringify({ message }), - signal: controller.signal, - }); - if (!response.ok || !response.body) { - throw new Error(`流式问诊请求失败:${response.status}`); - } - - const reader = response.body.getReader(); - const decoder = new TextDecoder("utf-8"); - let buffer = ""; - const result: StreamChatResult = {}; - let sawDone = false; - let sawDelta = false; - - while (true) { - const { done, value } = await reader.read(); - if (done) { - break; - } - buffer += decoder.decode(value, { stream: true }); - const parsed = parseSseEvents(buffer); - buffer = parsed.rest; - for (const { event: eventName, data } of parsed.events) { - if (eventName === "error") { - console.error("[chat] stream error event", data); - throw new Error(typeof data.message === "string" ? data.message : "流式问诊失败"); - } - if (eventName === "message_delta" && typeof data.delta === "string") { - sawDelta = true; - console.info("[chat] stream delta", { length: data.delta.length }); - onDelta(data.delta); - } - if (eventName === "message_done") { - sawDone = true; - result.firstTokenMs = typeof data.first_token_ms === "number" ? data.first_token_ms : null; - result.latencyMs = typeof data.latency_ms === "number" ? data.latency_ms : null; - result.model = typeof data.model === "string" ? data.model : null; - result.fallbackUsed = data.fallback_used === true; - console.info("[chat] stream done", result); - } - } - } - - if (!sawDone) { - throw new Error(sawDelta ? "流式问诊连接中断,未收到完成事件" : "流式问诊连接已结束,但未收到任何回复"); - } - return result; - } catch (error) { - console.error("[chat] stream exception", error); - if (error instanceof DOMException && error.name === "AbortError") { - throw new Error("流式问诊超时,请重试或关闭流式模式"); - } - throw error; - } finally { - window.clearTimeout(timeoutId); - } -} diff --git a/frontend/src/stores/consultationStore.ts b/frontend/src/stores/consultationStore.ts deleted file mode 100644 index c1b8152..0000000 --- a/frontend/src/stores/consultationStore.ts +++ /dev/null @@ -1,486 +0,0 @@ -import { defineStore } from "pinia"; - -import { apiClient, streamChat } from "../services/apiClient"; -import type { - AgentHello, - CaseDetail, - CaseListItem, - ChatMessage, - DiagnosisPayload, - EvaluationDetail, - EvaluationListItem, - EvaluationReport, - LlmTestResponse, - OrderItem, - OrderResult, - RequestContext, - ScoreType, - TrainingMode, - TrainingSession, - TrainingType, - TreatmentPayload, - HintResponse, - CaseDeletePreview, -} from "../types/api"; - -const defaultApiBaseUrl = "http://127.0.0.1:8000/api/v1"; -const defaultUserId = "demo_user_001"; - -function uid(prefix: string) { - return `${prefix}_${Date.now()}_${Math.random().toString(16).slice(2)}`; -} - -function loadString(key: string, fallback: string) { - return localStorage.getItem(key) || fallback; -} - -const emptyDiagnosis = (): DiagnosisPayload => ({ - primary_diagnosis: "", - differential_diagnoses: [], - diagnosis_basis: "", -}); - -const emptyTreatment = (): TreatmentPayload => ({ - treatment_principle: "", - treatment_measures: "", - risk_plan: "", - communication: "", - follow_up: "", -}); - -const demoDiagnosis = (): DiagnosisPayload => ({ - primary_diagnosis: "支气管肺炎", - differential_diagnoses: ["毛细支气管炎", "支气管哮喘急性发作", "肺结核", "上呼吸道感染"], - diagnosis_basis: "结合发热、咳嗽、喘息、肺部湿啰音、胸片异常、炎症指标升高和血氧情况,符合儿童支气管肺炎表现。", -}); - -const demoTreatment = (): TreatmentPayload => ({ - treatment_principle: "抗感染、止咳平喘、改善氧合、严密观察病情变化。", - treatment_measures: "根据病情选择抗感染治疗,必要时雾化吸入缓解喘息,监测体温、呼吸、血氧和精神反应。", - risk_plan: "关注低氧、呼吸困难加重、持续高热、精神反应差、脱水等情况。", - communication: "向家属说明肺炎病情、用药注意事项、观察指标和复诊/住院指征。", - follow_up: "治疗后复查体温、呼吸、血氧和必要炎症指标,症状加重时及时就诊。", -}); - -export const useConsultationStore = defineStore("consultation", { - state: () => ({ - apiBaseUrl: loadString("mca_api_base_url", defaultApiBaseUrl), - userId: loadString("mca_user_id", defaultUserId), - entryScene: loadString("mca_entry_scene", "vue_demo"), - hello: null as AgentHello | null, - connectedAt: "", - cases: [] as CaseListItem[], - selectedCase: null as CaseDetail | null, - caseDeletePreview: null as CaseDeletePreview | null, - session: null as TrainingSession | null, - trainingType: "diagnosis_treatment" as TrainingType, - mode: "practice" as TrainingMode, - scoreType: "percentage" as ScoreType, - messages: [] as ChatMessage[], - orderItems: [] as OrderItem[], - orderedResults: [] as OrderResult[], - diagnosis: emptyDiagnosis(), - treatment: emptyTreatment(), - evaluation: null as EvaluationReport | EvaluationDetail | null, - history: [] as EvaluationListItem[], - hints: null as HintResponse | null, - hintVisible: false, - llmFastResult: null as LlmTestResponse | null, - llmReasonResult: null as LlmTestResponse | null, - loading: false, - streaming: false, - orderingItemCode: "", - highlightedOrderCode: "", - pendingAction: "", - evaluationStage: "", - evaluationSlowNotice: false, - error: "", - }), - getters: { - context(state): RequestContext { - return { - apiBaseUrl: state.apiBaseUrl, - userId: state.userId, - entryScene: state.entryScene, - }; - }, - activeSessionId(state): number | null { - return state.session?.session_id ?? null; - }, - flowStatus(state): string { - return state.session?.status || "not_started"; - }, - isConnected(state): boolean { - return Boolean(state.hello?.user.user_id && state.hello.user.user_id === state.userId); - }, - isNoviceMode(state): boolean { - return state.session?.status === "inquiry" && state.mode === "practice"; - }, - canCompleteInquiry(state): boolean { - return state.session?.status === "inquiry" && state.messages.some((message) => message.role === "doctor"); - }, - canSubmitDiagnosis(state): boolean { - return state.session?.status === "diagnosis"; - }, - canSubmitTreatment(state): boolean { - return state.session?.status === "treatment"; - }, - canCreateEvaluation(state): boolean { - return state.session?.status === "evaluating" || state.session?.status === "completed"; - }, - completeInquiryDisabledReason(state): string { - if (!state.session) return "请先创建训练会话"; - if (state.session.status !== "inquiry") return "当前阶段不能继续完成问诊"; - if (!state.messages.some((message) => message.role === "doctor")) return "至少完成一轮问诊后可进入诊断"; - return ""; - }, - diagnosisDisabledReason(state): string { - if (!state.session) return "请先创建训练会话"; - if (state.session.status !== "diagnosis") return "完成问诊后才能提交诊断"; - return ""; - }, - treatmentDisabledReason(state): string { - if (!state.session) return "请先创建训练会话"; - if (state.session.status !== "treatment") return "提交诊断后才能提交治疗"; - return ""; - }, - evaluationDisabledReason(state): string { - if (!state.session) return "请先创建训练会话"; - if (state.session.status !== "evaluating" && state.session.status !== "completed") return "提交治疗后才能生成评价"; - return ""; - }, - }, - actions: { - saveConfig() { - localStorage.setItem("mca_api_base_url", this.apiBaseUrl); - localStorage.setItem("mca_user_id", this.userId); - localStorage.setItem("mca_entry_scene", this.entryScene); - }, - clearError() { - this.error = ""; - }, - async run(task: () => Promise, pendingAction = ""): Promise { - this.loading = true; - this.error = ""; - this.pendingAction = pendingAction; - try { - return await task(); - } catch (error) { - this.error = error instanceof Error ? error.message : "操作失败"; - return null; - } finally { - this.loading = false; - this.pendingAction = ""; - } - }, - async loadHello() { - return this.run(async () => { - this.saveConfig(); - this.hello = await apiClient.hello(this.context); - this.connectedAt = new Date().toLocaleString(); - return this.hello; - }, "正在连接后端"); - }, - async loadCases() { - return this.run(async () => { - this.cases = (await apiClient.listCases(this.context)).items; - return this.cases; - }, "正在加载病例"); - }, - async selectCase(caseId: number) { - return this.run(async () => { - this.selectedCase = await apiClient.getCaseDetail(this.context, caseId); - this.caseDeletePreview = null; - this.trainingType = this.selectedCase.supported_training_type; - return this.selectedCase; - }, "正在读取病例详情"); - }, - async loadCaseDeletePreview(caseId: number) { - return this.run(async () => { - this.caseDeletePreview = await apiClient.getCaseDeletePreview(this.context, caseId); - return this.caseDeletePreview; - }, "正在计算删除影响"); - }, - async deleteCase(caseId: number) { - return this.run(async () => { - const response = await apiClient.deleteCase(this.context, caseId); - this.cases = this.cases.filter((item) => item.id !== caseId); - if (this.selectedCase?.id === caseId) { - this.selectedCase = null; - this.session = null; - this.messages = []; - this.orderItems = []; - this.orderedResults = []; - this.resetTrainingDrafts(); - } - this.caseDeletePreview = null; - await this.loadCases(); - return response; - }, "正在删除病例"); - }, - resetTrainingDrafts() { - this.diagnosis = emptyDiagnosis(); - this.treatment = emptyTreatment(); - this.evaluation = null; - this.hints = null; - this.hintVisible = false; - this.evaluationStage = ""; - this.evaluationSlowNotice = false; - this.orderingItemCode = ""; - this.highlightedOrderCode = ""; - }, - fillDemoTemplate() { - this.diagnosis = demoDiagnosis(); - this.treatment = demoTreatment(); - }, - async createSession() { - if (!this.selectedCase) { - this.error = "请先选择病例"; - return null; - } - return this.run(async () => { - this.resetTrainingDrafts(); - this.orderedResults = []; - this.orderItems = []; - this.session = await apiClient.createSession(this.context, { - case_id: this.selectedCase!.id, - training_type: this.trainingType, - mode: this.mode, - score_type: this.scoreType, - }); - this.messages = [ - { - id: uid("patient"), - role: "patient", - content: this.session.patient_opening, - }, - ]; - this.orderedResults = []; - await this.loadOrderItems(); - return this.session; - }, "正在创建训练会话"); - }, - async sendChat(message: string, useStream: boolean) { - if (!this.session) { - this.error = "请先创建训练会话"; - return null; - } - const normalized = message.trim(); - if (!normalized) { - return null; - } - console.info("[chat] send", { text: normalized, useStream, sessionId: this.session.session_id }); - this.messages.push({ id: uid("doctor"), role: "doctor", content: normalized }); - if (!useStream) { - return this.run(async () => { - const response = await apiClient.chat(this.context, this.session!.session_id, normalized); - this.messages.push({ - id: uid("patient"), - role: "patient", - content: response.reply, - }); - return response; - }, "AI 病人正在回复"); - } - - this.streaming = true; - this.error = ""; - const patientMessage: ChatMessage = { id: uid("patient"), role: "patient", content: "", pending: true }; - this.messages.push(patientMessage); - const patientMessageId = patientMessage.id; - const updatePatientMessage = (patch: Partial) => { - const index = this.messages.findIndex((item) => item.id === patientMessageId); - if (index < 0) return; - this.messages[index] = { ...this.messages[index], ...patch }; - }; - const appendPatientDelta = (delta: string) => { - const index = this.messages.findIndex((item) => item.id === patientMessageId); - if (index < 0) return; - const current = this.messages[index]; - this.messages[index] = { ...current, content: `${current.content}${delta}`, pending: true }; - }; - try { - await streamChat(this.context, this.session.session_id, normalized, (delta) => { - appendPatientDelta(delta); - }); - const finalMessage = this.messages.find((item) => item.id === patientMessageId) || patientMessage; - updatePatientMessage({ pending: false }); - console.info("[chat] stream finished", { chars: finalMessage.content.length }); - return finalMessage; - } catch (error) { - const current = this.messages.find((item) => item.id === patientMessageId); - if (!current?.content.trim()) { - updatePatientMessage({ - content: "AI 病人回复超时或失败,请重试;演示时可关闭“流式”后再发送。", - pending: false, - }); - } else { - updatePatientMessage({ pending: false }); - } - console.error("[chat] stream failed", error); - this.error = error instanceof Error ? error.message : "流式问诊失败"; - return null; - } finally { - updatePatientMessage({ pending: false }); - this.streaming = false; - } - }, - async loadOrderItems() { - if (!this.session) { - return null; - } - return this.run(async () => { - this.orderItems = (await apiClient.listOrderItems(this.context, this.session!.session_id)).items; - return this.orderItems; - }, "正在加载检查项目"); - }, - async createOrder(itemCode: string) { - if (!this.session) { - this.error = "请先创建训练会话"; - return null; - } - const existing = this.orderedResults.find((item) => item.item_code === itemCode); - if (existing) { - this.highlightedOrderCode = itemCode; - window.setTimeout(() => { - if (this.highlightedOrderCode === itemCode) { - this.highlightedOrderCode = ""; - } - }, 1200); - return existing; - } - if (this.orderingItemCode) { - return null; - } - this.orderingItemCode = itemCode; - try { - return await this.run(async () => { - const result = await apiClient.createOrder(this.context, this.session!.session_id, itemCode); - const resultIndex = this.orderedResults.findIndex((item) => item.item_code === result.item_code); - if (resultIndex >= 0) { - this.orderedResults[resultIndex] = result; - } else { - this.orderedResults.unshift(result); - } - this.highlightedOrderCode = result.item_code; - window.setTimeout(() => { - if (this.highlightedOrderCode === result.item_code) { - this.highlightedOrderCode = ""; - } - }, 1200); - if (!result.already_ordered) { - this.messages.push({ - id: uid("system"), - role: "system", - content: `检查结果:${result.item_name} - ${result.result_text}。该结果已写入本次会话上下文和评分依据。`, - }); - } - return result; - }, "正在申请检查"); - } finally { - this.orderingItemCode = ""; - } - }, - async requestHints() { - if (!this.session) { - this.error = "请先创建训练会话"; - return null; - } - const lastUserMessage = [...this.messages].reverse().find((message) => message.role === "doctor")?.content; - return this.run(async () => { - this.hints = await apiClient.hints(this.context, this.session!.session_id, lastUserMessage); - this.hintVisible = true; - return this.hints; - }, "正在生成新手提示"); - }, - async completeInquiry() { - if (!this.session) { - this.error = "请先创建训练会话"; - return null; - } - return this.run(async () => { - const response = await apiClient.completeInquiry(this.context, this.session!.session_id); - this.session!.status = response.status; - return response; - }, "正在完成问诊"); - }, - async submitDiagnosis() { - if (!this.session) { - this.error = "请先创建训练会话"; - return null; - } - return this.run(async () => { - const response = await apiClient.submitDiagnosis(this.context, this.session!.session_id, this.diagnosis); - this.session!.status = response.status; - return response; - }, "正在提交诊断"); - }, - async submitTreatment() { - if (!this.session) { - this.error = "请先创建训练会话"; - return null; - } - return this.run(async () => { - const response = await apiClient.submitTreatment(this.context, this.session!.session_id, this.treatment); - this.session!.status = response.status; - return response; - }, "正在提交治疗方案"); - }, - async createEvaluation() { - if (!this.session) { - this.error = "请先创建训练会话"; - return null; - } - this.evaluationStage = "正在整理问诊记录"; - this.evaluationSlowNotice = false; - const timers = [ - window.setTimeout(() => (this.evaluationStage = "正在读取评分规则和检查申请"), 700), - window.setTimeout(() => (this.evaluationStage = "正在调用评分模型,真实 LLM 可能需要等待"), 1400), - window.setTimeout(() => (this.evaluationStage = "正在生成报告结构"), 5000), - window.setTimeout(() => (this.evaluationSlowNotice = true), 9000), - ]; - return this.run(async () => { - try { - this.evaluation = await apiClient.createEvaluation(this.context, this.session!.session_id, this.scoreType); - this.session!.status = "evaluated"; - await this.loadHistory(); - return this.evaluation; - } finally { - timers.forEach((timer) => window.clearTimeout(timer)); - this.evaluationStage = ""; - this.evaluationSlowNotice = false; - } - }, "正在生成评价报告"); - }, - async loadHistory() { - return this.run(async () => { - this.history = (await apiClient.listEvaluations(this.context)).items; - return this.history; - }); - }, - async loadEvaluationDetail(evaluationId: number) { - return this.run(async () => { - this.evaluation = await apiClient.getEvaluationDetail(this.context, evaluationId); - return this.evaluation; - }); - }, - async exportPdf(evaluationId?: number) { - const targetId = evaluationId ?? this.evaluation?.evaluation_id; - if (!targetId) { - this.error = "请先生成或选择评价报告"; - return null; - } - return this.run(async () => apiClient.exportPdf(this.context, targetId)); - }, - async testLlm(kind: "fast" | "reason", message: string) { - return this.run(async () => { - if (kind === "fast") { - this.llmFastResult = await apiClient.llmFast(this.context, message); - return this.llmFastResult; - } - this.llmReasonResult = await apiClient.llmReason(this.context, message); - return this.llmReasonResult; - }); - }, - }, -}); diff --git a/frontend/src/styles/main.css b/frontend/src/styles/main.css deleted file mode 100644 index 0e7a633..0000000 --- a/frontend/src/styles/main.css +++ /dev/null @@ -1,1409 +0,0 @@ -:root { - --bg: #f3f7fb; - --panel: #ffffff; - --panel-soft: #f8fbff; - --text: #253047; - --muted: #6b778c; - --line: #dce6f2; - --blue: #2167c8; - --blue-soft: #e9f1ff; - --orange: #ff6a2a; - --orange-soft: #fff0e8; - --green: #19a974; - --red: #d14343; - --shadow: 0 10px 30px rgba(31, 77, 124, 0.08); - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; -} - -* { - box-sizing: border-box; -} - -html, -body, -#app { - min-height: 100%; -} - -body { - margin: 0; - background: var(--bg); - color: var(--text); -} - -button, -input, -textarea, -select { - font: inherit; -} - -button, -a { - -webkit-tap-highlight-color: transparent; -} - -a { - color: inherit; - text-decoration: none; -} - -.app-shell { - min-height: 100vh; - display: grid; - grid-template-columns: 260px 1fr; -} - -.side-nav { - position: sticky; - top: 0; - height: 100vh; - padding: 22px 18px; - background: #183966; - color: #ffffff; -} - -.brand-block { - display: flex; - align-items: center; - gap: 12px; - margin-bottom: 24px; -} - -.brand-block span { - display: block; - color: rgba(255, 255, 255, 0.72); - font-size: 13px; - margin-top: 2px; -} - -.brand-icon { - width: 44px; - height: 44px; - border-radius: 8px; - display: grid; - place-items: center; - background: linear-gradient(135deg, #2b76dc, #ff6a2a); -} - -.nav-link { - display: flex; - align-items: center; - gap: 12px; - height: 44px; - padding: 0 12px; - border-radius: 8px; - color: rgba(255, 255, 255, 0.78); - margin-bottom: 6px; -} - -.nav-link.router-link-active { - background: rgba(255, 255, 255, 0.13); - color: #ffffff; -} - -.main-panel { - min-width: 0; - padding: 24px 24px 96px; -} - -.top-bar { - display: flex; - align-items: center; - justify-content: space-between; - gap: 16px; - margin-bottom: 20px; -} - -.top-bar h1 { - margin: 0; - font-size: 24px; -} - -.eyebrow { - margin: 0 0 4px; - color: var(--blue); - font-size: 12px; - font-weight: 700; - text-transform: uppercase; -} - -.user-chip, -.pill { - display: inline-flex; - align-items: center; - gap: 6px; - min-height: 30px; - padding: 5px 10px; - border-radius: 999px; - background: var(--blue-soft); - color: var(--blue); - font-size: 13px; - font-weight: 600; -} - -.pill.ok { - background: rgba(25, 169, 116, 0.12); - color: var(--green); -} - -.bottom-nav { - display: none; -} - -.page-grid { - display: grid; - grid-template-columns: 1fr; - gap: 16px; -} - -.page-grid.two-column, -.chat-layout { - display: grid; - grid-template-columns: minmax(0, 1fr) 420px; - gap: 16px; - align-items: start; -} - -.panel, -.mode-card, -.case-card, -.history-item, -.metric-card { - background: var(--panel); - border: 1px solid var(--line); - border-radius: 8px; - box-shadow: var(--shadow); -} - -.panel { - padding: 18px; -} - -.panel h2, -.panel h3, -.mode-card h3 { - margin: 0; -} - -.panel-title-row, -.case-card-head, -.score-header, -.dimension-label { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 12px; -} - -.hero-workbench { - background: linear-gradient(135deg, #ffffff 0%, #f7fbff 62%, #fff4ee 100%); -} - -.hero-copy h2 { - font-size: 28px; - margin: 0 0 8px; -} - -.hero-copy p, -.muted, -.mode-card p, -.case-card p, -.summary-box p, -.dimension-row p { - color: var(--muted); - line-height: 1.7; -} - -.settings-grid, -.form-grid, -.info-grid, -.metric-grid, -.connection-grid { - display: grid; - gap: 12px; -} - -.settings-grid { - grid-template-columns: repeat(3, minmax(0, 1fr)); - margin: 18px 0; -} - -.connection-grid { - grid-template-columns: repeat(3, minmax(0, 1fr)); - margin-top: 14px; -} - -.form-grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); - margin: 16px 0; -} - -.form-grid.single { - grid-template-columns: 1fr; -} - -label span { - display: block; - margin-bottom: 6px; - color: var(--muted); - font-size: 13px; - font-weight: 600; -} - -input, -textarea, -select { - width: 100%; - border: 1px solid var(--line); - border-radius: 8px; - background: #ffffff; - color: var(--text); - padding: 10px 12px; - outline: none; -} - -textarea { - resize: vertical; -} - -input:focus, -textarea:focus, -select:focus { - border-color: var(--blue); - box-shadow: 0 0 0 3px rgba(33, 103, 200, 0.12); -} - -.action-row { - display: flex; - flex-wrap: wrap; - gap: 10px; - margin-top: 14px; -} - -.primary-button, -.secondary-button, -.ghost-button, -.primary-icon-button { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 8px; - border: 0; - border-radius: 8px; - min-height: 42px; - padding: 0 14px; - cursor: pointer; - font-weight: 700; -} - -.primary-button, -.primary-icon-button { - background: linear-gradient(135deg, var(--blue), #2f80ed); - color: #ffffff; -} - -.secondary-button { - background: var(--orange-soft); - color: var(--orange); -} - -.ghost-button { - background: var(--blue-soft); - color: var(--blue); -} - -.danger-button { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 8px; - border: 0; - border-radius: 8px; - min-height: 42px; - padding: 0 14px; - cursor: pointer; - font-weight: 700; - background: #fff0e8; - color: var(--orange); -} - -.icon-button { - display: inline-flex; - align-items: center; - justify-content: center; - width: 38px; - height: 38px; - border: 0; - border-radius: 8px; - background: var(--blue-soft); - color: var(--blue); - cursor: pointer; -} - -.danger-text { - color: var(--red); -} - -.compact { - min-height: 34px; - padding: 0 10px; - font-size: 13px; -} - -.full { - width: 100%; -} - -button:disabled { - cursor: not-allowed; - opacity: 0.55; -} - -.field { - display: grid; - gap: 8px; - margin-top: 14px; -} - -.field span { - color: var(--muted); - font-weight: 700; -} - -.mode-grid { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 12px; -} - -.mode-card { - padding: 16px; -} - -.mode-card div { - width: 42px; - height: 42px; - border-radius: 8px; - display: grid; - place-items: center; - margin-bottom: 12px; -} - -.mode-card.blue div { - background: var(--blue-soft); - color: var(--blue); -} - -.mode-card.orange div { - background: var(--orange-soft); - color: var(--orange); -} - -.json-box { - white-space: pre-wrap; - word-break: break-word; - max-height: 240px; - overflow: auto; - background: #0f223a; - color: #dbeafe; - border-radius: 8px; - padding: 14px; -} - -.status-card { - border: 1px solid var(--line); - border-radius: 8px; - padding: 12px; - background: var(--panel-soft); -} - -.status-card span, -.status-card small { - display: block; - color: var(--muted); - line-height: 1.5; -} - -.status-card strong { - display: block; - margin: 6px 0; - word-break: break-word; -} - -.status-card.ok { - border-color: rgba(25, 169, 116, 0.24); - background: rgba(25, 169, 116, 0.08); -} - -.status-card.warning { - border-color: rgba(255, 106, 42, 0.28); - background: var(--orange-soft); -} - -.feature-list { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 12px; - margin-top: 14px; -} - -.feature-list article { - border: 1px solid var(--line); - border-radius: 8px; - padding: 12px; - background: var(--panel-soft); -} - -.feature-list span { - display: block; - color: var(--blue); - font-weight: 700; - margin: 6px 0; -} - -.feature-list p { - margin: 0; - color: var(--muted); - line-height: 1.6; -} - -.flow-stepper { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 8px; - margin: 14px 0; -} - -.flow-step { - display: flex; - align-items: center; - gap: 8px; - min-height: 42px; - padding: 8px; - border: 1px solid var(--line); - border-radius: 8px; - background: var(--panel-soft); - color: var(--muted); -} - -.flow-step span { - width: 24px; - height: 24px; - display: grid; - place-items: center; - border-radius: 50%; - background: #e6edf7; - font-weight: 700; -} - -.flow-step.active { - border-color: var(--blue); - background: var(--blue-soft); - color: var(--blue); -} - -.flow-step.done { - border-color: rgba(25, 169, 116, 0.24); - background: rgba(25, 169, 116, 0.1); - color: var(--green); -} - -.case-list, -.history-list, -.detail-stack, -.result-list, -.dimension-list { - display: grid; - gap: 12px; - margin-top: 14px; -} - -.case-card, -.history-item { - width: 100%; - text-align: left; - padding: 14px; - cursor: pointer; -} - -.case-card.selected, -.case-card:hover, -.history-item:hover { - border-color: var(--blue); -} - -.status-dot { - width: 9px; - height: 9px; - border-radius: 50%; - background: var(--green); - flex: 0 0 auto; - margin-top: 7px; -} - -.difficulty { - padding: 3px 8px; - border-radius: 999px; - background: var(--orange-soft); - color: var(--orange); - font-size: 12px; - white-space: nowrap; -} - -.case-tags { - display: flex; - flex-wrap: wrap; - gap: 8px; - margin-top: 10px; -} - -.case-tags span { - border-radius: 999px; - background: var(--blue-soft); - color: var(--blue); - padding: 4px 8px; - font-size: 12px; - font-weight: 600; -} - -.info-grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); -} - -.info-grid span, -.summary-box { - background: var(--panel-soft); - border: 1px solid var(--line); - border-radius: 8px; - padding: 10px; -} - -.summary-box strong { - display: block; - margin-bottom: 4px; -} - -.selected-case { - margin: 14px 0; - padding: 14px; - border-radius: 8px; - background: var(--panel-soft); - border: 1px solid var(--line); -} - -.mode-explain, -.training-context article, -.hint-panel, -.progress-panel { - border: 1px solid var(--line); - border-radius: 8px; - background: var(--panel-soft); - padding: 12px; -} - -.mode-explain p, -.training-context small, -.hint-toolbar span, -.button-hint, -.progress-panel p { - color: var(--muted); - line-height: 1.6; -} - -.training-context { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 10px; - margin: 12px 0; -} - -.training-context span { - display: block; - color: var(--muted); - font-size: 12px; - margin-bottom: 4px; -} - -.training-context article.novice { - border-color: rgba(255, 106, 42, 0.26); - background: var(--orange-soft); -} - -.hint-toolbar { - display: flex; - align-items: center; - gap: 10px; - margin: 10px 0; -} - -.hint-panel { - display: grid; - gap: 12px; - margin: 10px 0; -} - -.hint-panel ul { - margin: 8px 0 0; - padding-left: 18px; - color: var(--muted); - line-height: 1.7; -} - -.button-hint { - margin: 8px 0 0; - font-size: 13px; -} - -.progress-panel { - margin-top: 14px; - border-color: rgba(33, 103, 200, 0.25); - background: var(--blue-soft); -} - -.chat-layout { - grid-template-columns: minmax(0, 1fr) 380px; -} - -.chat-panel { - min-height: calc(100vh - 150px); -} - -.chat-window { - display: flex; - flex-direction: column; - gap: 12px; - min-height: 340px; - max-height: 52vh; - overflow-y: auto; - padding: 14px 4px; -} - -.chat-bubble { - display: flex; - align-items: flex-start; - gap: 10px; - max-width: 86%; -} - -.chat-bubble.doctor { - align-self: flex-end; - flex-direction: row-reverse; -} - -.chat-bubble.system { - max-width: 100%; -} - -.avatar { - width: 34px; - height: 34px; - border-radius: 8px; - background: var(--blue-soft); - color: var(--blue); - display: grid; - place-items: center; - flex: 0 0 auto; -} - -.chat-bubble.doctor .avatar { - background: var(--orange-soft); - color: var(--orange); -} - -.bubble-body { - border: 1px solid var(--line); - border-radius: 8px; - padding: 10px 12px; - background: var(--panel-soft); -} - -.chat-bubble.doctor .bubble-body { - background: var(--blue); - color: #ffffff; -} - -.chat-bubble.system .bubble-body { - background: #f7fbf8; - border-color: rgba(25, 169, 116, 0.22); -} - -.bubble-meta { - font-size: 12px; - font-weight: 700; - color: var(--muted); - margin-bottom: 4px; -} - -.chat-bubble.doctor .bubble-meta { - color: rgba(255, 255, 255, 0.78); -} - -.bubble-body p { - margin: 0; - line-height: 1.7; - white-space: pre-wrap; -} - -.quick-bar { - display: flex; - gap: 8px; - overflow-x: auto; - padding-bottom: 8px; -} - -.quick-bar button { - flex: 0 0 auto; - border: 1px solid var(--line); - border-radius: 999px; - background: #ffffff; - color: var(--blue); - padding: 7px 10px; -} - -.chat-input-bar { - display: grid; - grid-template-columns: 1fr auto; - gap: 8px; - align-items: stretch; -} - -.primary-icon-button { - min-height: 100%; - padding: 0; -} - -.segmented { - display: inline-flex; - padding: 3px; - border-radius: 8px; - background: var(--blue-soft); -} - -.segmented button { - border: 0; - background: transparent; - color: var(--blue); - padding: 6px 10px; - border-radius: 6px; - cursor: pointer; -} - -.segmented button.active { - background: #ffffff; - box-shadow: 0 2px 8px rgba(33, 103, 200, 0.16); -} - -.order-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 8px; - margin: 14px 0; -} - -.order-item { - display: grid; - gap: 4px; - justify-items: start; - border: 1px solid var(--line); - border-radius: 8px; - background: #ffffff; - padding: 10px; - cursor: pointer; -} - -.order-item svg { - color: var(--blue); -} - -.order-item.ordered { - background: #f8fbff; - border-color: #b7cff8; -} - -.order-item.active, -.result-card.active { - border-color: var(--blue); - box-shadow: 0 0 0 3px rgba(39, 111, 216, 0.12); -} - -.order-item:disabled { - opacity: 0.82; -} - -.order-item span { - font-weight: 700; -} - -.order-item small { - color: var(--muted); -} - -.order-action, -.context-note { - color: var(--green); - font-size: 12px; -} - -.result-card { - border: 1px solid var(--line); - border-radius: 8px; - padding: 12px; - background: var(--panel-soft); -} - -.result-card div { - display: flex; - justify-content: space-between; - gap: 8px; -} - -.result-flag { - font-size: 12px; - border-radius: 999px; - padding: 3px 8px; -} - -.result-flag.abnormal { - background: var(--orange-soft); - color: var(--orange); -} - -.result-flag.normal { - background: rgba(25, 169, 116, 0.12); - color: var(--green); -} - -.score-ring { - width: 118px; - height: 118px; - border-radius: 50%; - border: 10px solid var(--blue-soft); - display: grid; - place-items: center; - text-align: center; - color: var(--blue); - flex: 0 0 auto; -} - -.score-ring strong { - display: block; - font-size: 30px; - line-height: 1; -} - -.score-ring span { - display: block; - font-size: 12px; - color: var(--muted); -} - -.report-hero { - padding-bottom: 14px; - border-bottom: 1px solid var(--line); -} - -.report-summary-grid { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 10px; - margin: 14px 0; -} - -.report-summary-card { - border: 1px solid var(--line); - border-radius: 8px; - background: var(--panel-soft); - padding: 12px; -} - -.report-summary-card span { - display: block; - color: var(--muted); - font-size: 12px; -} - -.report-summary-card strong { - display: block; - margin-top: 4px; - color: var(--blue); - font-size: 22px; -} - -.dimension-row { - border: 1px solid var(--line); - border-radius: 8px; - padding: 12px; -} - -.dimension-row.detailed { - background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%); -} - -.dimension-detail-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 10px; - margin-top: 10px; -} - -.dimension-detail-grid div, -.report-issues article { - border: 1px solid var(--line); - border-radius: 8px; - background: #ffffff; - padding: 10px; -} - -.dimension-detail-grid h4, -.report-issues h3 { - margin: 0 0 8px; -} - -.dimension-detail-grid ul, -.report-columns ol { - margin: 0; - padding-left: 18px; - color: var(--muted); - line-height: 1.7; -} - -.improvement-note { - margin: 10px 0 0; - padding: 8px 10px; - border-radius: 8px; - background: var(--orange-soft); - color: var(--orange) !important; -} - -.bar-track { - height: 8px; - border-radius: 999px; - background: #edf2f8; - overflow: hidden; - margin: 8px 0; -} - -.bar-track span { - display: block; - height: 100%; - border-radius: inherit; - background: linear-gradient(90deg, var(--blue), var(--orange)); -} - -.report-columns { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 12px; -} - -.report-columns div { - border: 1px solid var(--line); - border-radius: 8px; - padding: 12px; - background: var(--panel-soft); -} - -.report-columns ul { - margin: 0; - padding-left: 18px; - color: var(--muted); - line-height: 1.7; -} - -.report-columns.improved { - margin-top: 14px; -} - -.report-issues { - margin-top: 14px; - display: grid; - gap: 10px; -} - -.report-issues article div { - display: flex; - justify-content: space-between; - gap: 8px; -} - -.report-issues article span { - color: var(--orange); - font-size: 12px; - white-space: nowrap; -} - -.report-issues article p { - margin: 8px 0 0; - color: var(--muted); - line-height: 1.7; -} - -.empty-panel, -.empty-inline { - display: grid; - place-items: center; - text-align: center; - gap: 8px; - color: var(--muted); -} - -.upload-box { - margin-top: 16px; - min-height: 180px; - border: 1px dashed #abc3e5; - border-radius: 8px; - background: var(--panel-soft); - display: grid; - place-items: center; - gap: 8px; - text-align: center; - padding: 18px; - cursor: pointer; -} - -.upload-box svg { - color: var(--blue); -} - -.upload-box strong, -.upload-box span { - display: block; -} - -.upload-box span { - color: var(--muted); -} - -.upload-box input { - display: none; -} - -.import-grid { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 12px; -} - -.import-message { - margin-top: 14px; - border-radius: 8px; - padding: 10px 12px; - display: flex; - gap: 8px; - line-height: 1.6; -} - -.import-message.error, -.error-box { - border: 1px solid rgba(209, 67, 67, 0.24); - background: #fff5f5; - color: var(--red); -} - -.import-message.success { - border: 1px solid rgba(25, 169, 116, 0.24); - background: rgba(25, 169, 116, 0.08); - color: var(--green); -} - -.warning-box { - display: flex; - gap: 10px; - align-items: flex-start; - border: 1px solid rgba(255, 106, 42, 0.28); - border-radius: 8px; - padding: 12px; - background: var(--orange-soft); - color: var(--orange); -} - -.warning-box p { - margin: 0; - color: var(--text); -} - -.modal-backdrop { - position: fixed; - inset: 0; - z-index: 50; - display: flex; - align-items: center; - justify-content: center; - padding: 18px; - background: rgba(20, 35, 55, 0.42); -} - -.modal-card { - width: min(560px, 100%); - max-height: calc(100vh - 36px); - overflow: auto; - border-radius: 8px; - border: 1px solid var(--line); - background: var(--panel); - box-shadow: var(--shadow); - padding: 18px; -} - -.delete-impact-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 8px; - margin-top: 10px; -} - -.delete-impact-grid span { - border-radius: 8px; - background: #ffffff; - border: 1px solid var(--line); - padding: 8px 10px; - color: var(--muted); -} - -.summary-box ul { - margin: 8px 0 0; - padding-left: 18px; - color: var(--muted); - line-height: 1.7; -} - -.compact-list { - margin-top: 8px; -} - -.import-case-row { - display: flex; - align-items: flex-start; - gap: 10px; - border: 1px solid var(--line); - border-radius: 8px; - padding: 10px; - background: #ffffff; -} - -.import-case-row svg { - color: var(--blue); -} - -.import-case-row span { - display: block; - margin-top: 3px; - color: var(--muted); - font-size: 12px; -} - -.empty-panel { - min-height: 300px; -} - -.history-item { - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - gap: 12px; - align-items: center; -} - -.history-item span, -.history-score small { - display: block; - color: var(--muted); - font-size: 12px; - margin-top: 4px; -} - -.history-score { - text-align: right; -} - -.history-score strong { - font-size: 24px; - color: var(--blue); -} - -.metric-grid { - grid-template-columns: repeat(3, minmax(0, 1fr)); -} - -.metric-card { - padding: 14px; -} - -.metric-card span, -.metric-card small { - display: block; - color: var(--muted); -} - -.metric-card strong { - display: block; - font-size: 24px; - color: var(--blue); - margin: 8px 0; -} - -.metric-card em { - display: block; - color: var(--orange); - font-style: normal; - font-size: 12px; -} - -.error-banner { - display: flex; - align-items: center; - gap: 8px; - margin-bottom: 16px; - padding: 10px 12px; - border: 1px solid rgba(209, 67, 67, 0.24); - border-radius: 8px; - background: #fff5f5; - color: var(--red); -} - -.error-banner button { - margin-left: auto; - border: 0; - background: transparent; - color: var(--red); - cursor: pointer; - font-weight: 700; -} - -.safe-area-bottom { - padding-bottom: env(safe-area-inset-bottom); -} - -@media (max-width: 980px) { - .app-shell { - display: block; - } - - .side-nav { - display: none; - } - - .main-panel { - padding: 16px 12px 92px; - } - - .top-bar { - align-items: flex-start; - } - - .top-bar h1 { - font-size: 20px; - } - - .user-chip { - max-width: 44%; - overflow: hidden; - } - - .page-grid.two-column, - .chat-layout, - .settings-grid, - .form-grid, - .mode-grid, - .import-grid, - .report-summary-grid, - .dimension-detail-grid, - .report-columns, - .metric-grid, - .connection-grid, - .feature-list, - .training-context { - grid-template-columns: 1fr; - } - - .mode-grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - - .flow-stepper { - grid-template-columns: repeat(4, minmax(72px, 1fr)); - overflow-x: auto; - } - - .chat-panel { - min-height: auto; - } - - .chat-window { - min-height: 300px; - max-height: 48vh; - } - - .chat-bubble { - max-width: 94%; - } - - .bottom-nav { - position: fixed; - left: 0; - right: 0; - bottom: 0; - z-index: 10; - display: grid; - grid-template-columns: repeat(6, 1fr); - gap: 2px; - padding: 7px 6px; - background: rgba(255, 255, 255, 0.96); - border-top: 1px solid var(--line); - backdrop-filter: blur(14px); - } - - .bottom-link { - min-height: 50px; - display: grid; - place-items: center; - gap: 2px; - color: var(--muted); - font-size: 11px; - border-radius: 8px; - } - - .bottom-link.router-link-active { - color: var(--blue); - background: var(--blue-soft); - } -} - -@media (max-width: 520px) { - .panel { - padding: 14px; - } - - .hero-copy h2 { - font-size: 23px; - } - - .mode-grid, - .order-grid, - .info-grid { - grid-template-columns: 1fr; - } - - .score-header { - display: grid; - } - - .score-ring { - width: 104px; - height: 104px; - } - - .action-row { - display: grid; - } - - .primary-button, - .secondary-button, - .ghost-button, - .danger-button { - width: 100%; - } - - .delete-impact-grid { - grid-template-columns: 1fr; - } -} diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts deleted file mode 100644 index edb6781..0000000 --- a/frontend/src/types/api.ts +++ /dev/null @@ -1,241 +0,0 @@ -export type ScoreType = "percentage" | "five_point"; -export type TrainingMode = "practice" | "teaching"; -export type TrainingType = "case_analysis" | "diagnosis_treatment" | "consultation"; - -export interface ApiEnvelope { - code: string; - message: string; - data: T; -} - -export interface AgentHello { - user: { - user_id: string; - tenant_id?: string | null; - role?: string | null; - }; - features: { - stream_chat?: boolean; - score_types?: ScoreType[]; - pdf_export?: boolean; - knowledge_search?: boolean; - llm_mock_enabled?: boolean; - llm_mode?: "mock" | "real"; - llm_fallback_to_mock?: boolean; - llm_fast_model?: string; - llm_reason_model?: string; - llm_fast_thinking_enabled?: boolean; - llm_reason_thinking_enabled?: boolean; - llm_reasoning_effort?: string; - llm_fast_max_tokens?: number; - runtime_memory_backend?: string; - [key: string]: unknown; - }; -} - -export interface CaseListItem { - id: number; - case_code: string; - department_id: number; - title: string; - difficulty: string; - chief_complaint?: string | null; - supported_training_type: TrainingType; - supported_mode: string; - has_teaching_video: boolean; - has_knowledge_points: boolean; - has_quiz: boolean; -} - -export interface CaseDetail { - id: number; - case_code: string; - title: string; - department: string; - difficulty: string; - patient: { - name?: string | null; - age?: number | null; - gender?: string | null; - occupation?: string | null; - }; - chief_complaint?: string | null; - supported_training_type: TrainingType; - supported_mode: string; - has_teaching_video: boolean; - has_knowledge_points: boolean; - has_quiz: boolean; - order_item_types: string[]; -} - -export interface CreateSessionPayload { - case_id: number; - training_type: TrainingType; - mode: TrainingMode; - score_type: ScoreType; -} - -export interface TrainingSession { - session_id: number; - session_code: string; - status: string; - patient_opening: string; -} - -export interface ChatMessage { - id: string; - role: "doctor" | "patient" | "system"; - content: string; - pending?: boolean; -} - -export interface ChatResponse { - reply: string; - latency_ms: number; - model: string; - fallback_used?: boolean; -} - -export interface OrderItem { - item_code: string; - item_name: string; - item_type: string; -} - -export interface OrderResult extends OrderItem { - result_text: string; - result_structured?: Record | null; - is_key: boolean; - is_abnormal: boolean; - context_written?: boolean; - already_ordered?: boolean; -} - -export interface RecommendedOrder { - item_code: string; - reason: string; -} - -export interface HintResponse { - hints: string[]; - missing_dimensions: string[]; - next_questions: string[]; - recommended_orders: RecommendedOrder[]; -} - -export interface DiagnosisPayload { - primary_diagnosis: string; - differential_diagnoses: string[]; - diagnosis_basis: string; -} - -export interface TreatmentPayload { - treatment_principle: string; - treatment_measures: string; - risk_plan?: string | null; - communication?: string | null; - follow_up?: string | null; -} - -export interface DimensionScore { - dimension: string; - score: number; - max_score: number; - comment: string; - evidence?: string[]; - deductions?: string[]; - improvement?: string; -} - -export interface EvaluationReport { - evaluation_id: number; - score_type: ScoreType; - total_score: number; - dimension_scores: DimensionScore[]; - errors: Record[]; - improvement_plan: string[]; - evidence_summary: string[]; - guideline_refs: Record[]; - overall_comment: string; -} - -export interface EvaluationDetail extends EvaluationReport { - session_id: number; - case_id: number; - case_title: string; - created_at: string; - pdf_file_path?: string | null; -} - -export interface EvaluationListItem { - evaluation_id: number; - case_title: string; - score_type: ScoreType; - total_score: number; - created_at: string; - pdf_exported: boolean; -} - -export interface ExportPdfResponse { - export_id: number; - file_path: string; -} - -export interface LlmTestResponse { - model: string; - first_token_ms?: number | null; - total_latency_ms: number; - stream: boolean; - mock_mode?: boolean; - fallback_used?: boolean; - thinking_enabled?: boolean | null; - reasoning_effort?: string | null; -} - -export interface CaseSqlPreviewCase { - id: number; - title: string; - case_type: string; - difficulty: string; -} - -export interface CaseSqlImportPreview { - file_name: string; - encoding?: string | null; - tables: Record; - can_import: boolean; - warnings: string[]; - errors: string[]; - preview_cases: CaseSqlPreviewCase[]; -} - -export interface CaseSqlImportApply { - imported: boolean; - file_name: string; - encoding: string; - inserted_or_updated_cases: number; - imported_traditional_cases: number; - imported_teaching_cases: number; - imported_scoring_rules: number; - generated_exam_items: number; - warnings: string[]; -} - -export interface CaseDeletePreview { - case_id: number; - case_title: string; - can_delete: boolean; - affected: Record; -} - -export interface CaseDeleteResponse { - deleted: boolean; - case_id: number; - deleted_counts: Record; -} - -export interface RequestContext { - apiBaseUrl: string; - userId: string; - entryScene: string; -} diff --git a/frontend/src/views/CasesView.vue b/frontend/src/views/CasesView.vue deleted file mode 100644 index 6400e4a..0000000 --- a/frontend/src/views/CasesView.vue +++ /dev/null @@ -1,217 +0,0 @@ - - - diff --git a/frontend/src/views/ChatView.vue b/frontend/src/views/ChatView.vue deleted file mode 100644 index eb8e899..0000000 --- a/frontend/src/views/ChatView.vue +++ /dev/null @@ -1,178 +0,0 @@ - - - diff --git a/frontend/src/views/HistoryView.vue b/frontend/src/views/HistoryView.vue deleted file mode 100644 index dff1cbe..0000000 --- a/frontend/src/views/HistoryView.vue +++ /dev/null @@ -1,53 +0,0 @@ - - - diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue deleted file mode 100644 index 90568c3..0000000 --- a/frontend/src/views/HomeView.vue +++ /dev/null @@ -1,184 +0,0 @@ - - - diff --git a/frontend/src/views/ImportCaseView.vue b/frontend/src/views/ImportCaseView.vue deleted file mode 100644 index 607f608..0000000 --- a/frontend/src/views/ImportCaseView.vue +++ /dev/null @@ -1,192 +0,0 @@ - - - diff --git a/frontend/src/views/LlmTestView.vue b/frontend/src/views/LlmTestView.vue deleted file mode 100644 index 16ab46a..0000000 --- a/frontend/src/views/LlmTestView.vue +++ /dev/null @@ -1,71 +0,0 @@ - - - diff --git a/frontend/src/views/ReportView.vue b/frontend/src/views/ReportView.vue deleted file mode 100644 index a40cd15..0000000 --- a/frontend/src/views/ReportView.vue +++ /dev/null @@ -1,64 +0,0 @@ - - - diff --git a/frontend/src/views/SessionView.vue b/frontend/src/views/SessionView.vue deleted file mode 100644 index f619b23..0000000 --- a/frontend/src/views/SessionView.vue +++ /dev/null @@ -1,103 +0,0 @@ - - - diff --git a/frontend/src/views/SubmitView.vue b/frontend/src/views/SubmitView.vue deleted file mode 100644 index 4198a04..0000000 --- a/frontend/src/views/SubmitView.vue +++ /dev/null @@ -1,114 +0,0 @@ - - - diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts deleted file mode 100644 index 11f02fe..0000000 --- a/frontend/src/vite-env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json deleted file mode 100644 index 8006521..0000000 --- a/frontend/tsconfig.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "useDefineForClassFields": true, - "module": "ESNext", - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "allowJs": false, - "skipLibCheck": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "moduleResolution": "Bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "preserve" - }, - "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], - "references": [{ "path": "./tsconfig.node.json" }] -} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json deleted file mode 100644 index 3adda81..0000000 --- a/frontend/tsconfig.node.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "compilerOptions": { - "composite": true, - "skipLibCheck": true, - "module": "ESNext", - "moduleResolution": "Bundler", - "allowSyntheticDefaultImports": true - }, - "include": ["vite.config.ts"] -} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts deleted file mode 100644 index e0566bd..0000000 --- a/frontend/vite.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { defineConfig } from "vite"; -import vue from "@vitejs/plugin-vue"; - -export default defineConfig({ - plugins: [vue()], - server: { - host: "127.0.0.1", - port: 5173, - }, -}); diff --git a/scripts/check_mysql_demo.ps1 b/scripts/check_mysql_demo.ps1 deleted file mode 100644 index 41ffc83..0000000 --- a/scripts/check_mysql_demo.ps1 +++ /dev/null @@ -1,40 +0,0 @@ -param( - [string]$HostName = "127.0.0.1", - [int]$Port = 3306, - [string]$User = "root", - [string]$Password = "", - [string]$DatabaseName = "medical_consultation_agent", - [string]$MysqlExe = "mysql" -) - -$ErrorActionPreference = "Stop" - -if ([string]::IsNullOrWhiteSpace($Password)) { - $securePassword = Read-Host "MySQL password for $User@$HostName" -AsSecureString - $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($securePassword) - try { - $Password = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr) - } - finally { - [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) - } -} - -$env:MYSQL_PWD = $Password - -try { - & $MysqlExe -h $HostName -P $Port -u $User --default-character-set=utf8mb4 -D $DatabaseName -e @" -SELECT TABLE_NAME, TABLE_COMMENT -FROM information_schema.tables -WHERE table_schema = '$DatabaseName' -ORDER BY TABLE_NAME; -SELECT COUNT(*) AS case_count FROM cases; -SELECT COUNT(*) AS exam_item_count FROM case_exam_items; -SELECT COUNT(*) AS prompt_template_count FROM prompt_templates; -SELECT COUNT(*) AS rubric_template_count FROM rubric_templates; -SELECT COUNT(*) AS knowledge_chunk_count FROM knowledge_chunks; -"@ -} -finally { - Remove-Item Env:\MYSQL_PWD -ErrorAction SilentlyContinue -} diff --git a/scripts/init_mysql_demo.ps1 b/scripts/init_mysql_demo.ps1 deleted file mode 100644 index 8d9460e..0000000 --- a/scripts/init_mysql_demo.ps1 +++ /dev/null @@ -1,95 +0,0 @@ -param( - [string]$HostName = "127.0.0.1", - [int]$Port = 3306, - [string]$User = "root", - [string]$Password = "", - [string]$DatabaseName = "medical_consultation_agent", - [string]$MysqlExe = "mysql" -) - -$ErrorActionPreference = "Stop" - -function Resolve-ProjectPath { - $scriptDir = Split-Path -Parent $MyInvocation.ScriptName - return (Resolve-Path -Path (Join-Path $scriptDir "..")).Path -} - -function Convert-ToMysqlSourcePath { - param([string]$Path) - return $Path.Replace("\", "/") -} - -function Invoke-MysqlCommand { - param([string]$Sql) - & $MysqlExe -h $HostName -P $Port -u $User --default-character-set=utf8mb4 -e $Sql - if ($LASTEXITCODE -ne 0) { - throw "MySQL command failed: $Sql" - } -} - -function Invoke-MysqlDatabaseCommand { - param([string]$Sql) - & $MysqlExe -h $HostName -P $Port -u $User --default-character-set=utf8mb4 -D $DatabaseName -e $Sql - if ($LASTEXITCODE -ne 0) { - throw "MySQL database command failed: $Sql" - } -} - -$projectRoot = Resolve-ProjectPath -$schemaPath = Convert-ToMysqlSourcePath((Resolve-Path -Path (Join-Path $projectRoot "docs/sql/schema.sql")).Path) -$seedPath = Convert-ToMysqlSourcePath((Resolve-Path -Path (Join-Path $projectRoot "docs/sql/seed_pediatric_pneumonia.sql")).Path) -$tempDir = Join-Path $projectRoot "storage/mysql_import" -New-Item -ItemType Directory -Force -Path $tempDir | Out-Null -$tempSchemaPath = Join-Path $tempDir "schema.$DatabaseName.sql" -$tempSeedPath = Join-Path $tempDir "seed.$DatabaseName.sql" - -(Get-Content -Path $schemaPath -Raw -Encoding UTF8).Replace("medical_consultation_agent", $DatabaseName) | - Set-Content -Path $tempSchemaPath -Encoding UTF8 -(Get-Content -Path $seedPath -Raw -Encoding UTF8).Replace("medical_consultation_agent", $DatabaseName) | - Set-Content -Path $tempSeedPath -Encoding UTF8 - -$tempSchemaSource = Convert-ToMysqlSourcePath((Resolve-Path -Path $tempSchemaPath).Path) -$tempSeedSource = Convert-ToMysqlSourcePath((Resolve-Path -Path $tempSeedPath).Path) - -if ([string]::IsNullOrWhiteSpace($Password)) { - $securePassword = Read-Host "MySQL password for $User@$HostName" -AsSecureString - $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($securePassword) - try { - $Password = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr) - } - finally { - [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) - } -} - -$env:MYSQL_PWD = $Password - -try { - Write-Host "Checking MySQL connection..." - Invoke-MysqlCommand "SELECT VERSION() AS mysql_version;" - - Write-Host "Creating schema in database $DatabaseName from docs/sql/schema.sql..." - Invoke-MysqlCommand "source $tempSchemaSource" - - Write-Host "Seeding demo data into database $DatabaseName..." - Invoke-MysqlCommand "source $tempSeedSource" - - Write-Host "Verifying tables and seed data..." - $verifySql = @" -SELECT COUNT(*) AS table_count -FROM information_schema.tables -WHERE table_schema = 'medical_consultation_agent'; -SELECT id, case_code, title, difficulty -FROM cases; -SELECT item_code, item_name, item_type, is_key -FROM case_exam_items -ORDER BY display_order; -"@ - $verifySql = $verifySql.Replace("medical_consultation_agent", $DatabaseName) - Invoke-MysqlDatabaseCommand $verifySql - - Write-Host "MySQL demo schema initialized successfully." -} -finally { - Remove-Item Env:\MYSQL_PWD -ErrorAction SilentlyContinue -}