From 96290d2f767197b58dba71ab72980802f25c2628 Mon Sep 17 00:00:00 2001 From: HaHafeng Date: Sat, 24 Jan 2026 17:29:24 +0800 Subject: [PATCH] feat(aia): Implement Protocol Agent MVP with reusable Agent framework Sprint 1-3 Completed (Backend + Frontend): Backend (Sprint 1-2): - Implement 5-layer Agent framework (Query->Planner->Executor->Tools->Reflection) - Create agent_schema with 6 tables (agent_definitions, stages, prompts, sessions, traces, reflexion_rules) - Create protocol_schema with 2 tables (protocol_contexts, protocol_generations) - Implement Protocol Agent core services (Orchestrator, ContextService, PromptBuilder) - Integrate LLM service adapter (DeepSeek/Qwen/GPT-5/Claude) - 6 API endpoints with full authentication - 10/10 API tests passed Frontend (Sprint 3): - Add Protocol Agent entry in AgentHub (indigo theme card) - Implement ProtocolAgentPage with 3-column layout - Collapsible sidebar (Gemini style, 48px <-> 280px) - StatePanel with 5 stage cards (scientific_question, pico, study_design, sample_size, endpoints) - ChatArea with sync button and action cards integration - 100% prototype design restoration (608 lines CSS) - Detailed endpoints structure: baseline, exposure, outcomes, confounders Features: - 5-stage dialogue flow for research protocol design - Conversation-driven interaction with sync-to-protocol button - Real-time context state management - One-click protocol generation button (UI ready, backend pending) Database: - agent_schema: 6 tables for reusable Agent framework - protocol_schema: 2 tables for Protocol Agent - Seed data: 1 agent + 5 stages + 9 prompts + 4 reflexion rules Code Stats: - Backend: 13 files, 4338 lines - Frontend: 14 files, 2071 lines - Total: 27 files, 6409 lines Status: MVP core functionality completed, pending frontend-backend integration testing Next: Sprint 4 - One-click protocol generation + Word export --- COMMIT_DAY1.txt | 2 + DC模块代码恢复指南.md | 2 + Dockerfile.postgres-with-extensions | 2 + SAE_WECHAT_MP_DEPLOY_STEPS.md | 2 + backend/DEPLOY_TO_SAE_FOR_WECHAT_MP.md | 2 + backend/RESTART_SERVER_NOW.md | 2 + backend/WECHAT_MP_CONFIG_READY.md | 2 + backend/WECHAT_MP_QUICK_FIX.md | 2 + backend/check_db.ts | 2 + backend/check_db_data.ts | 2 + backend/check_iit.ts | 2 + backend/check_iit_asl_data.ts | 2 + backend/check_queue_table.ts | 2 + backend/check_rvw_issue.ts | 2 + backend/check_tables.ts | 2 + backend/compare_db.ts | 2 + backend/compare_dc_asl.ts | 2 + backend/compare_pkb_aia_rvw.ts | 2 + backend/compare_schema_db.ts | 2 + backend/create_mock_user.sql | 2 + backend/create_mock_user_platform.sql | 2 + backend/env.example.md | 2 + .../add_data_stats_to_tool_c_session.sql | 2 + .../001_add_postgres_cache_and_checkpoint.sql | 2 + .../manual-migrations/run-migration-002.ts | 2 + .../20251208_add_column_mapping/migration.sql | 2 + .../migrations/create_tool_c_session.sql | 2 + .../migrations/manual/ekb_create_indexes.sql | 2 + .../manual/ekb_create_indexes_mvp.sql | 2 + backend/prisma/schema.prisma | 302 ++++- backend/prisma/seeds/protocol-agent-seed.ts | 560 ++++++++ backend/rebuild-and-push.ps1 | 2 + backend/recover-code-from-cursor-db.js | 2 + backend/restore_job_common.sql | 2 + backend/restore_pgboss_functions.sql | 2 + backend/scripts/check-dc-tables.mjs | 2 + backend/scripts/create-capability-schema.sql | 2 + .../create-tool-c-ai-history-table.mjs | 2 + backend/scripts/create-tool-c-table.js | 2 + backend/scripts/create-tool-c-table.mjs | 2 + backend/scripts/migrate-aia-prompts.ts | 2 + backend/scripts/setup-prompt-system.ts | 2 + backend/scripts/test-pkb-apis-simple.ts | 2 + backend/scripts/test-prompt-api.ts | 2 + backend/scripts/test-unifuncs-deepsearch.ts | 2 + backend/scripts/verify-pkb-rvw-schema.ts | 2 + backend/src/common/auth/jwt.service.ts | 2 + backend/src/common/jobs/utils.ts | 2 + backend/src/common/prompt/prompt.types.ts | 2 + backend/src/common/rag/ChunkService.ts | 2 + backend/src/common/rag/DifyClient.ts | 2 + .../common/streaming/OpenAIStreamAdapter.ts | 2 + .../src/common/streaming/StreamingService.ts | 2 + backend/src/common/streaming/index.ts | 2 + backend/src/common/streaming/types.ts | 2 + backend/src/index.ts | 9 + .../src/modules/admin/routes/tenantRoutes.ts | 2 + .../src/modules/admin/types/tenant.types.ts | 2 + backend/src/modules/admin/types/user.types.ts | 2 + backend/src/modules/agent/index.ts | 25 + .../controllers/ProtocolAgentController.ts | 287 ++++ backend/src/modules/agent/protocol/index.ts | 22 + .../modules/agent/protocol/routes/index.ts | 153 +++ .../protocol/services/LLMServiceAdapter.ts | 184 +++ .../agent/protocol/services/PromptBuilder.ts | 286 ++++ .../services/ProtocolContextService.ts | 289 ++++ .../protocol/services/ProtocolOrchestrator.ts | 321 +++++ .../modules/agent/protocol/services/index.ts | 11 + .../agent/services/BaseAgentOrchestrator.ts | 561 ++++++++ .../modules/agent/services/ConfigLoader.ts | 266 ++++ .../modules/agent/services/QueryAnalyzer.ts | 270 ++++ .../modules/agent/services/StageManager.ts | 283 ++++ .../src/modules/agent/services/TraceLogger.ts | 349 +++++ backend/src/modules/agent/services/index.ts | 12 + backend/src/modules/agent/types/index.ts | 382 ++++++ .../aia/controllers/agentController.ts | 2 + .../aia/controllers/attachmentController.ts | 2 + backend/src/modules/aia/index.ts | 2 + .../__tests__/api-integration-test.ts | 2 + .../__tests__/e2e-real-test-v2.ts | 2 + .../__tests__/fulltext-screening-api.http | 2 + .../services/ConflictDetectionService.ts | 2 + backend/src/modules/dc/tool-c/README.md | 2 + .../tool-c/controllers/StreamAIController.ts | 2 + .../iit-manager/agents/SessionMemory.ts | 2 + .../iit-manager/check-iit-table-structure.ts | 2 + .../iit-manager/check-project-config.ts | 2 + .../iit-manager/check-test-project-in-db.ts | 2 + .../iit-manager/docs/微信服务号接入指南.md | 2 + .../iit-manager/generate-wechat-tokens.ts | 2 + .../services/PatientWechatService.ts | 2 + .../iit-manager/test-chatservice-dify.ts | 2 + .../modules/iit-manager/test-iit-database.ts | 2 + .../iit-manager/test-patient-wechat-config.ts | 2 + .../test-patient-wechat-url-verify.ts | 2 + .../iit-manager/test-redcap-query-from-db.ts | 2 + .../iit-manager/test-wechat-mp-local.ps1 | 2 + .../src/modules/iit-manager/types/index.ts | 2 + backend/src/modules/pkb/routes/health.ts | 2 + backend/src/modules/rvw/__tests__/api.http | 2 + .../src/modules/rvw/__tests__/test-api.ps1 | 2 + backend/src/modules/rvw/index.ts | 2 + backend/src/modules/rvw/services/utils.ts | 2 + backend/src/tests/README.md | 2 + .../src/tests/test-cross-language-search.ts | 2 + backend/src/tests/test-query-rewrite.ts | 2 + backend/src/tests/test-rerank.ts | 2 + backend/src/tests/verify-test1-database.sql | 2 + backend/src/tests/verify-test1-database.ts | 2 + backend/src/types/global.d.ts | 2 + backend/sync-dc-database.ps1 | 2 + backend/temp_check.sql | 2 + backend/test-pkb-migration.http | 2 + backend/test-tool-c-advanced-scenarios.mjs | 2 + backend/test-tool-c-day2.mjs | 2 + backend/test-tool-c-day3.mjs | 2 + backend/verify_all_users.ts | 2 + backend/verify_functions.ts | 2 + backend/verify_job_common.ts | 2 + backend/verify_mock_user.ts | 2 + backend/verify_system.ts | 2 + deploy-to-sae.ps1 | 2 + .../00-系统当前状态与开发指南.md | 18 +- .../02-存储服务/OSS账号与配置信息.md | 2 + .../02-存储服务/OSS集成开发记录-2026-01-22.md | 2 + .../03-RAG引擎/05-RAG引擎使用指南.md | 2 + .../03-RAG引擎/06-pg_bigm安装指南.md | 2 + docs/02-通用能力层/快速引用卡片.md | 2 + docs/02-通用能力层/通用能力层技术债务清单.md | 2 + .../ADMIN-运营管理端/00-Phase3.5完成总结.md | 2 + .../2026-01-16_用户管理功能与模块权限系统完成.md | 2 + docs/03-业务模块/ADMIN-运营管理端/README.md | 2 + .../ADMIN运营与INST机构管理端-文档体系建立完成.md | 2 + .../AIA-AI智能问答/00-模块当前状态与开发指南.md | 30 +- .../Protocol_Agent_Development_Simplification_Guide.md | 90 ++ .../00-系统设计/Protocol_Agent_PRD_v1.0.md | 158 +++ .../00-系统设计/后端配置原型图V3.html | 381 ++++++ .../00-系统设计/研究方案原型图0119.html | 442 ++++++ .../Protocol_Agent_Architecture_Design_V3.md | 163 +++ .../Protocol_Agent_Backend_Config_Design.md | 116 ++ .../Protocol_Agent_Technical_Implementation_V3.md | 244 ++++ .../AIA-AI智能问答/04-开发计划/03-前端组件设计.md | 2 + .../04-Protocol_Agent开发计划/00-开发计划总览.md | 183 +++ .../04-Protocol_Agent开发计划/01-架构设计.md | 498 +++++++ .../04-Protocol_Agent开发计划/02-数据库设计.md | 846 ++++++++++++ .../04-Protocol_Agent开发计划/03-代码结构设计.md | 1127 +++++++++++++++ .../04-Protocol_Agent开发计划/04-分阶段实施计划.md | 632 +++++++++ .../04-开发计划/04-Protocol_Agent开发计划/README.md | 79 ++ .../06-开发记录/2026-01-18-Prompt管理系统集成.md | 2 + .../2026-01-24-Protocol_Agent_MVP开发完成.md | 293 ++++ .../04-开发计划/05-全文复筛前端开发计划.md | 2 + .../05-开发记录/2025-01-23_全文复筛前端开发完成.md | 2 + .../05-开发记录/2025-01-23_全文复筛前端逻辑调整.md | 2 + .../05-开发记录/2025-11-23_Day5_全文复筛API开发.md | 2 + .../2026-01-18_智能文献检索DeepSearch集成.md | 2 + .../04-开发计划/工具C_AI_Few-shot示例库.md | 2 + .../04-开发计划/工具C_Bug修复总结_2025-12-08.md | 2 + .../04-开发计划/工具C_Day3开发计划.md | 2 + .../04-开发计划/工具C_Day4-5前端开发计划.md | 2 + .../04-开发计划/工具C_Pivot列顺序优化总结.md | 2 + .../04-开发计划/工具C_方案B实施总结_2025-12-09.md | 2 + .../04-开发计划/工具C_缺失值处理_开发进度_2025-12-10.md | 2 + .../04-开发计划/工具C_缺失值处理功能_更新说明.md | 2 + .../06-开发记录/2025-12-02_工作总结.md | 2 + .../06-开发记录/2025-12-06_工具C_Day1开发完成总结.md | 2 + .../06-开发记录/2025-12-06_工具C_Day2开发完成总结.md | 2 + .../06-开发记录/2025-12-07_AI对话核心功能增强总结.md | 2 + .../2025-12-07_Bug修复_DataGrid空数据防御.md | 2 + .../06-开发记录/2025-12-07_Day5_Ant-Design-X重构完成.md | 2 + .../06-开发记录/2025-12-07_Day5最终总结.md | 2 + .../06-开发记录/2025-12-07_UI优化与Bug修复.md | 2 + .../06-开发记录/2025-12-07_后端API完整对接完成.md | 2 + .../06-开发记录/2025-12-07_完整UI优化与功能增强.md | 2 + .../06-开发记录/2025-12-07_工具C_Day4前端基础完成.md | 2 + .../06-开发记录/DC模块重建完成总结-Day1.md | 2 + .../06-开发记录/Phase1-Portal页面开发完成-2025-12-02.md | 2 + .../Phase2-ToolB-Step1-2开发完成-2025-12-03.md | 2 + .../06-开发记录/Portal页面UI优化-2025-12-02.md | 2 + .../06-开发记录/Tool-B-MVP完成总结-2025-12-03.md | 2 + .../06-开发记录/ToolB-UI优化-2025-12-03.md | 2 + .../06-开发记录/ToolB-UI优化-Round2-2025-12-03.md | 2 + .../06-开发记录/ToolB浏览器测试计划-2025-12-03.md | 2 + .../06-开发记录/后端API测试报告-2025-12-02.md | 2 + .../06-开发记录/待办事项-下一步工作.md | 2 + .../06-开发记录/数据库验证报告-2025-12-02.md | 2 + .../07-技术债务/Tool-B技术债务清单.md | 2 + .../IIT Manager Agent 技术路径与架构设计.md | 2 + .../04-开发计划/REDCap对接技术方案与实施指南.md | 2 + .../04-开发计划/企业微信注册指南.md | 2 + .../2026-01-04-Dify知识库集成开发记录.md | 2 + .../Day2-REDCap实时集成开发完成记录.md | 2 + .../Day3-企业微信集成与端到端测试完成记录.md | 2 + .../06-开发记录/Day3-企业微信集成开发完成记录.md | 2 + .../Phase1.5-AI对话集成REDCap完成记录.md | 2 + .../06-开发记录/V1.1更新完成报告.md | 2 + .../07-技术债务/IIT Manager Agent 技术债务清单.md | 2 + .../INST-机构管理端/00-模块当前状态与开发指南.md | 2 + docs/03-业务模块/INST-机构管理端/README.md | 2 + .../06-开发记录/2026-01-07-前端迁移与批处理功能完善.md | 2 + .../06-开发记录/2026-01-07_PKB模块前端V3设计实现.md | 2 + .../01-部署与配置/10-REDCap_Docker部署操作手册.md | 2 + docs/03-业务模块/Redcap/README.md | 2 + docs/04-开发规范/09-数据库开发规范.md | 2 + docs/04-开发规范/10-模块认证规范.md | 2 + .../02-SAE部署完全指南(产品经理版).md | 2 + .../07-前端Nginx-SAE部署操作手册.md | 2 + .../08-PostgreSQL数据库部署操作手册.md | 2 + .../10-Node.js后端-Docker镜像构建手册.md | 2 + .../11-Node.js后端-SAE部署配置清单.md | 2 + .../12-Node.js后端-SAE部署操作手册.md | 2 + .../13-Node.js后端-镜像修复记录.md | 2 + .../14-Node.js后端-pino-pretty问题修复.md | 2 + docs/05-部署文档/16-前端Nginx-部署成功总结.md | 2 + .../05-部署文档/17-完整部署实战手册-2025版.md | 2 + docs/05-部署文档/18-部署文档使用指南.md | 2 + docs/05-部署文档/19-日常更新快速操作手册.md | 2 + docs/05-部署文档/文档修正报告-20251214.md | 2 + docs/07-运维文档/03-SAE环境变量配置指南.md | 2 + .../05-Redis缓存与队列的区别说明.md | 2 + docs/07-运维文档/06-长时间任务可靠性分析.md | 2 + .../07-Redis使用需求分析(按模块).md | 2 + docs/07-运维文档/全自动巡检系统设计方案.md | 193 +++ .../架构优化建议:2人团队SAE降本增效方案.md | 124 ++ docs/07-运维文档/运营体系设计方案-MVP-V3.0.md | 293 ++++ .../2025-12-13-Postgres-Only架构改造完成.md | 2 + .../05-技术债务/通用对话服务抽取计划.md | 2 + docs/08-项目管理/2026-01-11-数据库事故总结.md | 2 + docs/08-项目管理/PKB前端问题修复报告.md | 2 + docs/08-项目管理/PKB前端验证指南.md | 2 + docs/08-项目管理/PKB功能审查报告-阶段0.md | 2 + docs/08-项目管理/PKB和RVW功能迁移计划.md | 2 + docs/08-项目管理/PKB精细化优化报告.md | 2 + docs/08-项目管理/PKB迁移-超级安全执行计划.md | 2 + docs/08-项目管理/PKB迁移-阶段1完成报告.md | 2 + docs/08-项目管理/PKB迁移-阶段2完成报告.md | 2 + docs/08-项目管理/PKB迁移-阶段2进行中.md | 2 + docs/08-项目管理/PKB迁移-阶段3完成报告.md | 2 + docs/08-项目管理/PKB迁移-阶段4完成报告.md | 2 + extraction_service/.dockerignore | 2 + extraction_service/operations/__init__.py | 2 + extraction_service/operations/dropna.py | 2 + extraction_service/operations/filter.py | 2 + extraction_service/operations/unpivot.py | 2 + .../services/pdf_markdown_processor.py | 2 + extraction_service/test_dc_api.py | 2 + extraction_service/test_execute_simple.py | 2 + extraction_service/test_module.py | 2 + frontend-v2/.dockerignore | 2 + frontend-v2/docker-entrypoint.sh | 2 + frontend-v2/nginx.conf | 2 + frontend-v2/src/common/api/axios.ts | 2 + frontend-v2/src/framework/auth/api.ts | 2 + frontend-v2/src/framework/auth/index.ts | 2 + frontend-v2/src/framework/auth/moduleApi.ts | 2 + .../components/ModulePermissionModal.tsx | 2 + frontend-v2/src/modules/admin/index.tsx | 2 + frontend-v2/src/modules/admin/types/user.ts | 2 + .../src/modules/aia/components/AgentCard.tsx | 12 +- .../src/modules/aia/components/AgentHub.tsx | 33 + .../src/modules/aia/components/index.ts | 2 + frontend-v2/src/modules/aia/constants.ts | 19 + frontend-v2/src/modules/aia/index.tsx | 85 +- .../aia/protocol-agent/ProtocolAgentPage.tsx | 245 ++++ .../protocol-agent/components/ActionCard.tsx | 66 + .../protocol-agent/components/ChatArea.tsx | 310 +++++ .../components/ReflexionMessage.tsx | 52 + .../protocol-agent/components/StageCard.tsx | 237 ++++ .../protocol-agent/components/StatePanel.tsx | 90 ++ .../protocol-agent/components/SyncButton.tsx | 50 + .../aia/protocol-agent/components/index.ts | 12 + .../modules/aia/protocol-agent/hooks/index.ts | 8 + .../hooks/useProtocolContext.ts | 79 ++ .../hooks/useProtocolConversations.ts | 144 ++ .../src/modules/aia/protocol-agent/index.ts | 10 + .../protocol-agent/styles/protocol-agent.css | 1208 +++++++++++++++++ .../src/modules/aia/protocol-agent/types.ts | 175 +++ .../src/modules/aia/styles/agent-card.css | 56 + .../src/modules/aia/styles/agent-hub.css | 39 + frontend-v2/src/modules/aia/types.ts | 8 +- .../asl/components/FulltextDetailDrawer.tsx | 2 + frontend-v2/src/modules/dc/hooks/useAssets.ts | 2 + .../src/modules/dc/hooks/useRecentTasks.ts | 2 + .../dc/pages/tool-c/hooks/useSessionStatus.ts | 2 + .../modules/dc/pages/tool-c/types/index.ts | 2 + frontend-v2/src/modules/dc/types/portal.ts | 2 + .../src/modules/pkb/pages/KnowledgePage.tsx | 2 + .../src/modules/pkb/types/workspace.ts | 2 + .../src/modules/rvw/components/AgentModal.tsx | 2 + .../modules/rvw/components/BatchToolbar.tsx | 2 + .../modules/rvw/components/FilterChips.tsx | 2 + .../src/modules/rvw/components/Header.tsx | 2 + .../modules/rvw/components/ReportDetail.tsx | 2 + .../src/modules/rvw/components/ScoreRing.tsx | 2 + .../src/modules/rvw/components/Sidebar.tsx | 2 + .../src/modules/rvw/components/index.ts | 2 + .../src/modules/rvw/pages/Dashboard.tsx | 2 + frontend-v2/src/modules/rvw/styles/index.css | 2 + .../pages/admin/tenants/TenantListPage.tsx | 2 + .../src/pages/admin/tenants/api/tenantApi.ts | 2 + .../shared/components/Chat/AIStreamChat.tsx | 2 + .../components/Chat/ConversationList.tsx | 2 + .../src/shared/components/Chat/hooks/index.ts | 2 + .../components/Chat/hooks/useAIStream.ts | 2 + .../components/Chat/hooks/useConversations.ts | 2 + .../components/Chat/styles/ai-stream-chat.css | 2 + .../Chat/styles/conversation-list.css | 2 + .../components/Chat/styles/thinking.css | 2 + frontend-v2/src/shared/components/index.ts | 2 + frontend-v2/src/vite-env.d.ts | 2 + .../src/pages/rvw/components/BatchToolbar.tsx | 2 + .../pages/rvw/components/EditorialReport.tsx | 2 + .../src/pages/rvw/components/FilterChips.tsx | 2 + frontend/src/pages/rvw/components/Header.tsx | 2 + .../src/pages/rvw/components/ReportDetail.tsx | 2 + .../src/pages/rvw/components/ScoreRing.tsx | 2 + frontend/src/pages/rvw/components/Sidebar.tsx | 2 + frontend/src/pages/rvw/index.ts | 2 + frontend/src/pages/rvw/styles.css | 2 + git-cleanup-redcap.ps1 | 2 + git-commit-day1.ps1 | 2 + git-fix-lock.ps1 | 2 + python-microservice/operations/__init__.py | 2 + python-microservice/operations/binning.py | 2 + python-microservice/operations/filter.py | 2 + python-microservice/operations/recode.py | 2 + recover_dc_code.py | 2 + redcap-docker-dev/.gitattributes | 2 + redcap-docker-dev/.gitignore | 2 + redcap-docker-dev/README.md | 2 + redcap-docker-dev/docker-compose.prod.yml | 2 + redcap-docker-dev/docker-compose.yml | 2 + redcap-docker-dev/env.template | 2 + redcap-docker-dev/scripts/clean-redcap.ps1 | 2 + .../scripts/create-redcap-password.php | 2 + redcap-docker-dev/scripts/logs-redcap.ps1 | 2 + .../scripts/reset-admin-password.php | 2 + redcap-docker-dev/scripts/start-redcap.ps1 | 2 + redcap-docker-dev/scripts/stop-redcap.ps1 | 2 + run_recovery.ps1 | 2 + tests/QUICKSTART_快速开始.md | 2 + tests/README_测试说明.md | 2 + tests/run_tests.bat | 2 + tests/run_tests.sh | 2 + 快速部署到SAE.md | 2 + 部署检查清单.md | 2 + 345 files changed, 13945 insertions(+), 47 deletions(-) create mode 100644 backend/prisma/seeds/protocol-agent-seed.ts create mode 100644 backend/src/modules/agent/index.ts create mode 100644 backend/src/modules/agent/protocol/controllers/ProtocolAgentController.ts create mode 100644 backend/src/modules/agent/protocol/index.ts create mode 100644 backend/src/modules/agent/protocol/routes/index.ts create mode 100644 backend/src/modules/agent/protocol/services/LLMServiceAdapter.ts create mode 100644 backend/src/modules/agent/protocol/services/PromptBuilder.ts create mode 100644 backend/src/modules/agent/protocol/services/ProtocolContextService.ts create mode 100644 backend/src/modules/agent/protocol/services/ProtocolOrchestrator.ts create mode 100644 backend/src/modules/agent/protocol/services/index.ts create mode 100644 backend/src/modules/agent/services/BaseAgentOrchestrator.ts create mode 100644 backend/src/modules/agent/services/ConfigLoader.ts create mode 100644 backend/src/modules/agent/services/QueryAnalyzer.ts create mode 100644 backend/src/modules/agent/services/StageManager.ts create mode 100644 backend/src/modules/agent/services/TraceLogger.ts create mode 100644 backend/src/modules/agent/services/index.ts create mode 100644 backend/src/modules/agent/types/index.ts create mode 100644 docs/03-业务模块/AIA-AI智能问答/00-系统设计/Protocol_Agent_Development_Simplification_Guide.md create mode 100644 docs/03-业务模块/AIA-AI智能问答/00-系统设计/Protocol_Agent_PRD_v1.0.md create mode 100644 docs/03-业务模块/AIA-AI智能问答/00-系统设计/后端配置原型图V3.html create mode 100644 docs/03-业务模块/AIA-AI智能问答/00-系统设计/研究方案原型图0119.html create mode 100644 docs/03-业务模块/AIA-AI智能问答/02-技术设计/Protocol_Agent_Architecture_Design_V3.md create mode 100644 docs/03-业务模块/AIA-AI智能问答/02-技术设计/Protocol_Agent_Backend_Config_Design.md create mode 100644 docs/03-业务模块/AIA-AI智能问答/02-技术设计/Protocol_Agent_Technical_Implementation_V3.md create mode 100644 docs/03-业务模块/AIA-AI智能问答/04-开发计划/04-Protocol_Agent开发计划/00-开发计划总览.md create mode 100644 docs/03-业务模块/AIA-AI智能问答/04-开发计划/04-Protocol_Agent开发计划/01-架构设计.md create mode 100644 docs/03-业务模块/AIA-AI智能问答/04-开发计划/04-Protocol_Agent开发计划/02-数据库设计.md create mode 100644 docs/03-业务模块/AIA-AI智能问答/04-开发计划/04-Protocol_Agent开发计划/03-代码结构设计.md create mode 100644 docs/03-业务模块/AIA-AI智能问答/04-开发计划/04-Protocol_Agent开发计划/04-分阶段实施计划.md create mode 100644 docs/03-业务模块/AIA-AI智能问答/04-开发计划/04-Protocol_Agent开发计划/README.md create mode 100644 docs/03-业务模块/AIA-AI智能问答/06-开发记录/2026-01-24-Protocol_Agent_MVP开发完成.md create mode 100644 docs/07-运维文档/全自动巡检系统设计方案.md create mode 100644 docs/07-运维文档/架构优化建议:2人团队SAE降本增效方案.md create mode 100644 docs/07-运维文档/运营体系设计方案-MVP-V3.0.md create mode 100644 frontend-v2/src/modules/aia/protocol-agent/ProtocolAgentPage.tsx create mode 100644 frontend-v2/src/modules/aia/protocol-agent/components/ActionCard.tsx create mode 100644 frontend-v2/src/modules/aia/protocol-agent/components/ChatArea.tsx create mode 100644 frontend-v2/src/modules/aia/protocol-agent/components/ReflexionMessage.tsx create mode 100644 frontend-v2/src/modules/aia/protocol-agent/components/StageCard.tsx create mode 100644 frontend-v2/src/modules/aia/protocol-agent/components/StatePanel.tsx create mode 100644 frontend-v2/src/modules/aia/protocol-agent/components/SyncButton.tsx create mode 100644 frontend-v2/src/modules/aia/protocol-agent/components/index.ts create mode 100644 frontend-v2/src/modules/aia/protocol-agent/hooks/index.ts create mode 100644 frontend-v2/src/modules/aia/protocol-agent/hooks/useProtocolContext.ts create mode 100644 frontend-v2/src/modules/aia/protocol-agent/hooks/useProtocolConversations.ts create mode 100644 frontend-v2/src/modules/aia/protocol-agent/index.ts create mode 100644 frontend-v2/src/modules/aia/protocol-agent/styles/protocol-agent.css create mode 100644 frontend-v2/src/modules/aia/protocol-agent/types.ts diff --git a/COMMIT_DAY1.txt b/COMMIT_DAY1.txt index 1e61f8bb..1d4e84ae 100644 --- a/COMMIT_DAY1.txt +++ b/COMMIT_DAY1.txt @@ -58,6 +58,8 @@ Status: Day 1 complete (11/11 tasks), ready for Day 2 + + diff --git a/DC模块代码恢复指南.md b/DC模块代码恢复指南.md index 9f1403c4..2d38e4db 100644 --- a/DC模块代码恢复指南.md +++ b/DC模块代码恢复指南.md @@ -288,6 +288,8 @@ + + diff --git a/Dockerfile.postgres-with-extensions b/Dockerfile.postgres-with-extensions index 9998d23c..17e47f91 100644 --- a/Dockerfile.postgres-with-extensions +++ b/Dockerfile.postgres-with-extensions @@ -52,3 +52,5 @@ COPY docker-init-extensions.sql /docker-entrypoint-initdb.d/ # 暴露端口 EXPOSE 5432 + + diff --git a/SAE_WECHAT_MP_DEPLOY_STEPS.md b/SAE_WECHAT_MP_DEPLOY_STEPS.md index 6e19166b..f71565e8 100644 --- a/SAE_WECHAT_MP_DEPLOY_STEPS.md +++ b/SAE_WECHAT_MP_DEPLOY_STEPS.md @@ -234,6 +234,8 @@ https://iit.xunzhengyixue.com/api/v1/iit/health + + diff --git a/backend/DEPLOY_TO_SAE_FOR_WECHAT_MP.md b/backend/DEPLOY_TO_SAE_FOR_WECHAT_MP.md index 47bc263d..1d1f3c33 100644 --- a/backend/DEPLOY_TO_SAE_FOR_WECHAT_MP.md +++ b/backend/DEPLOY_TO_SAE_FOR_WECHAT_MP.md @@ -163,6 +163,8 @@ https://iit.xunzhengyixue.com/api/v1/iit/health + + diff --git a/backend/RESTART_SERVER_NOW.md b/backend/RESTART_SERVER_NOW.md index 8f5f5d54..a51bf44a 100644 --- a/backend/RESTART_SERVER_NOW.md +++ b/backend/RESTART_SERVER_NOW.md @@ -64,6 +64,8 @@ + + diff --git a/backend/WECHAT_MP_CONFIG_READY.md b/backend/WECHAT_MP_CONFIG_READY.md index 7f6f4c6a..1bd1c363 100644 --- a/backend/WECHAT_MP_CONFIG_READY.md +++ b/backend/WECHAT_MP_CONFIG_READY.md @@ -324,6 +324,8 @@ npx tsx src/modules/iit-manager/test-patient-wechat-url-verify.ts + + diff --git a/backend/WECHAT_MP_QUICK_FIX.md b/backend/WECHAT_MP_QUICK_FIX.md index 0cf0c435..5df7dd82 100644 --- a/backend/WECHAT_MP_QUICK_FIX.md +++ b/backend/WECHAT_MP_QUICK_FIX.md @@ -186,6 +186,8 @@ npm run dev + + diff --git a/backend/check_db.ts b/backend/check_db.ts index 0c7fabeb..bc8aa607 100644 --- a/backend/check_db.ts +++ b/backend/check_db.ts @@ -64,5 +64,7 @@ main() + + diff --git a/backend/check_db_data.ts b/backend/check_db_data.ts index 39c81b3f..5f4833ba 100644 --- a/backend/check_db_data.ts +++ b/backend/check_db_data.ts @@ -58,5 +58,7 @@ main() + + diff --git a/backend/check_iit.ts b/backend/check_iit.ts index b5123c7f..c98f5109 100644 --- a/backend/check_iit.ts +++ b/backend/check_iit.ts @@ -53,5 +53,7 @@ main() + + diff --git a/backend/check_iit_asl_data.ts b/backend/check_iit_asl_data.ts index 838efd92..6b6dd5be 100644 --- a/backend/check_iit_asl_data.ts +++ b/backend/check_iit_asl_data.ts @@ -85,5 +85,7 @@ main() + + diff --git a/backend/check_queue_table.ts b/backend/check_queue_table.ts index fce065bc..37be8453 100644 --- a/backend/check_queue_table.ts +++ b/backend/check_queue_table.ts @@ -48,5 +48,7 @@ main() + + diff --git a/backend/check_rvw_issue.ts b/backend/check_rvw_issue.ts index a9bfc829..e0b62e05 100644 --- a/backend/check_rvw_issue.ts +++ b/backend/check_rvw_issue.ts @@ -89,5 +89,7 @@ main() + + diff --git a/backend/check_tables.ts b/backend/check_tables.ts index 444fcf8b..32bbd63b 100644 --- a/backend/check_tables.ts +++ b/backend/check_tables.ts @@ -36,5 +36,7 @@ main() + + diff --git a/backend/compare_db.ts b/backend/compare_db.ts index b1b119e2..d2377b46 100644 --- a/backend/compare_db.ts +++ b/backend/compare_db.ts @@ -124,5 +124,7 @@ main() + + diff --git a/backend/compare_dc_asl.ts b/backend/compare_dc_asl.ts index c65d175c..04be1381 100644 --- a/backend/compare_dc_asl.ts +++ b/backend/compare_dc_asl.ts @@ -95,5 +95,7 @@ main() + + diff --git a/backend/compare_pkb_aia_rvw.ts b/backend/compare_pkb_aia_rvw.ts index 57fbb0cf..e11b1950 100644 --- a/backend/compare_pkb_aia_rvw.ts +++ b/backend/compare_pkb_aia_rvw.ts @@ -81,5 +81,7 @@ main() + + diff --git a/backend/compare_schema_db.ts b/backend/compare_schema_db.ts index 58e53161..466c1a08 100644 --- a/backend/compare_schema_db.ts +++ b/backend/compare_schema_db.ts @@ -123,5 +123,7 @@ main() + + diff --git a/backend/create_mock_user.sql b/backend/create_mock_user.sql index 9bfa95c9..b15741f3 100644 --- a/backend/create_mock_user.sql +++ b/backend/create_mock_user.sql @@ -34,5 +34,7 @@ ON CONFLICT (id) DO NOTHING; + + diff --git a/backend/create_mock_user_platform.sql b/backend/create_mock_user_platform.sql index 55ef3b19..e649278c 100644 --- a/backend/create_mock_user_platform.sql +++ b/backend/create_mock_user_platform.sql @@ -66,5 +66,7 @@ ON CONFLICT (id) DO NOTHING; + + diff --git a/backend/env.example.md b/backend/env.example.md index 5303def9..7e2c007a 100644 --- a/backend/env.example.md +++ b/backend/env.example.md @@ -78,3 +78,5 @@ OSS_SIGNED_URL_EXPIRES=3600 ``` + + diff --git a/backend/migrations/add_data_stats_to_tool_c_session.sql b/backend/migrations/add_data_stats_to_tool_c_session.sql index da798ff2..438e3b14 100644 --- a/backend/migrations/add_data_stats_to_tool_c_session.sql +++ b/backend/migrations/add_data_stats_to_tool_c_session.sql @@ -83,6 +83,8 @@ WHERE table_schema = 'dc_schema' + + diff --git a/backend/prisma/manual-migrations/001_add_postgres_cache_and_checkpoint.sql b/backend/prisma/manual-migrations/001_add_postgres_cache_and_checkpoint.sql index a90297aa..9f0c7907 100644 --- a/backend/prisma/manual-migrations/001_add_postgres_cache_and_checkpoint.sql +++ b/backend/prisma/manual-migrations/001_add_postgres_cache_and_checkpoint.sql @@ -121,6 +121,8 @@ ORDER BY ordinal_position; + + diff --git a/backend/prisma/manual-migrations/run-migration-002.ts b/backend/prisma/manual-migrations/run-migration-002.ts index f1371a47..b8af7b84 100644 --- a/backend/prisma/manual-migrations/run-migration-002.ts +++ b/backend/prisma/manual-migrations/run-migration-002.ts @@ -134,6 +134,8 @@ runMigration() + + diff --git a/backend/prisma/migrations/20251208_add_column_mapping/migration.sql b/backend/prisma/migrations/20251208_add_column_mapping/migration.sql index 7859499b..2c5aab38 100644 --- a/backend/prisma/migrations/20251208_add_column_mapping/migration.sql +++ b/backend/prisma/migrations/20251208_add_column_mapping/migration.sql @@ -68,6 +68,8 @@ COMMENT ON COLUMN "dc_schema"."dc_tool_c_sessions"."column_mapping" IS '列名 + + diff --git a/backend/prisma/migrations/create_tool_c_session.sql b/backend/prisma/migrations/create_tool_c_session.sql index 3f82701f..11e77de7 100644 --- a/backend/prisma/migrations/create_tool_c_session.sql +++ b/backend/prisma/migrations/create_tool_c_session.sql @@ -95,6 +95,8 @@ COMMENT ON COLUMN dc_schema.dc_tool_c_sessions.expires_at IS '过期时间(创 + + diff --git a/backend/prisma/migrations/manual/ekb_create_indexes.sql b/backend/prisma/migrations/manual/ekb_create_indexes.sql index a6c8443a..3921c287 100644 --- a/backend/prisma/migrations/manual/ekb_create_indexes.sql +++ b/backend/prisma/migrations/manual/ekb_create_indexes.sql @@ -66,3 +66,5 @@ USING gin (metadata jsonb_path_ops); + + diff --git a/backend/prisma/migrations/manual/ekb_create_indexes_mvp.sql b/backend/prisma/migrations/manual/ekb_create_indexes_mvp.sql index 38c78a73..9f8df0ea 100644 --- a/backend/prisma/migrations/manual/ekb_create_indexes_mvp.sql +++ b/backend/prisma/migrations/manual/ekb_create_indexes_mvp.sql @@ -33,3 +33,5 @@ USING gin (tags); + + diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 9ffc6178..1a555e4b 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -6,7 +6,7 @@ generator client { datasource db { provider = "postgresql" url = env("DATABASE_URL") - schemas = ["admin_schema", "aia_schema", "asl_schema", "capability_schema", "common_schema", "dc_schema", "ekb_schema", "iit_schema", "pkb_schema", "platform_schema", "public", "rvw_schema", "ssa_schema", "st_schema"] + schemas = ["admin_schema", "agent_schema", "aia_schema", "asl_schema", "capability_schema", "common_schema", "dc_schema", "ekb_schema", "iit_schema", "pkb_schema", "platform_schema", "protocol_schema", "public", "rvw_schema", "ssa_schema", "st_schema"] } /// 应用缓存表 - Postgres-Only架构 @@ -1393,3 +1393,303 @@ model EkbChunk { @@map("ekb_chunk") @@schema("ekb_schema") } + +// ============================================================ +// Agent Framework Schema (agent_schema) +// 通用Agent框架 - 可复用于多种Agent类型 +// ============================================================ + +/// Agent定义表 - 存储Agent的基本配置 +model AgentDefinition { + id String @id @default(uuid()) + code String @unique /// 唯一标识: protocol_agent, stat_agent + name String /// 显示名称 + description String? /// 描述 + version String @default("1.0.0") /// 版本号 + + /// Agent配置 + config Json? @db.JsonB /// 全局配置 { defaultModel, maxTurns, timeout } + + /// 状态 + isActive Boolean @default(true) @map("is_active") + + /// 关联 + stages AgentStage[] + prompts AgentPrompt[] + sessions AgentSession[] + reflexionRules ReflexionRule[] + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@index([code], map: "idx_agent_def_code") + @@index([isActive], map: "idx_agent_def_active") + @@map("agent_definitions") + @@schema("agent_schema") +} + +/// Agent阶段配置表 - 定义Agent的工作流阶段 +model AgentStage { + id String @id @default(uuid()) + agentId String @map("agent_id") /// 关联的Agent + + /// 阶段标识 + stageCode String @map("stage_code") /// 阶段代码: scientific_question, pico + stageName String @map("stage_name") /// 阶段名称: 科学问题梳理 + sortOrder Int @map("sort_order") /// 排序顺序 + + /// 阶段配置 + config Json? @db.JsonB /// 阶段特定配置 + + /// 状态机配置 + nextStages String[] @map("next_stages") /// 可转换的下一阶段列表 + isInitial Boolean @default(false) @map("is_initial") /// 是否为起始阶段 + isFinal Boolean @default(false) @map("is_final") /// 是否为结束阶段 + + /// 关联 + agent AgentDefinition @relation(fields: [agentId], references: [id], onDelete: Cascade) + prompts AgentPrompt[] + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@unique([agentId, stageCode], map: "unique_agent_stage") + @@index([agentId], map: "idx_agent_stage_agent") + @@index([sortOrder], map: "idx_agent_stage_order") + @@map("agent_stages") + @@schema("agent_schema") +} + +/// Agent Prompt模板表 - 存储各阶段的Prompt +model AgentPrompt { + id String @id @default(uuid()) + agentId String @map("agent_id") /// 关联的Agent + stageId String? @map("stage_id") /// 关联的阶段(可选,null表示通用Prompt) + + /// Prompt标识 + promptType String @map("prompt_type") /// system, stage, extraction, reflexion + promptCode String @map("prompt_code") /// 唯一代码 + + /// Prompt内容 + content String @db.Text /// Prompt模板内容(支持变量) + variables String[] /// 预期变量列表 + + /// 版本控制 + version Int @default(1) /// 版本号 + isActive Boolean @default(true) @map("is_active") + + /// 关联 + agent AgentDefinition @relation(fields: [agentId], references: [id], onDelete: Cascade) + stage AgentStage? @relation(fields: [stageId], references: [id], onDelete: SetNull) + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@unique([agentId, promptCode, version], map: "unique_agent_prompt_version") + @@index([agentId, promptType], map: "idx_agent_prompt_type") + @@index([stageId], map: "idx_agent_prompt_stage") + @@map("agent_prompts") + @@schema("agent_schema") +} + +/// Agent会话表 - 存储Agent的运行时会话状态 +model AgentSession { + id String @id @default(uuid()) + agentId String @map("agent_id") /// 关联的Agent定义 + conversationId String @map("conversation_id") /// 关联的对话ID(aia_schema.conversations) + userId String @map("user_id") /// 用户ID + + /// 当前状态 + currentStage String @map("current_stage") /// 当前阶段代码 + status String @default("active") /// active, completed, paused, error + + /// 上下文数据(具体Agent的上下文存储在对应schema) + contextRef String? @map("context_ref") /// 上下文引用ID(如protocol_schema.protocol_contexts.id) + + /// 统计信息 + turnCount Int @default(0) @map("turn_count") /// 对话轮数 + totalTokens Int @default(0) @map("total_tokens") /// 总Token数 + + /// 关联 + agent AgentDefinition @relation(fields: [agentId], references: [id]) + traces AgentTrace[] + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@unique([conversationId], map: "unique_agent_session_conv") + @@index([agentId], map: "idx_agent_session_agent") + @@index([userId], map: "idx_agent_session_user") + @@index([status], map: "idx_agent_session_status") + @@map("agent_sessions") + @@schema("agent_schema") +} + +/// Agent执行追踪表 - 记录每一步的执行详情 +model AgentTrace { + id String @id @default(uuid()) + sessionId String @map("session_id") /// 关联的会话 + + /// 追踪信息 + traceId String @map("trace_id") /// 请求追踪ID(用于关联日志) + stepIndex Int @map("step_index") /// 步骤序号 + stepType String @map("step_type") /// query, plan, execute, reflect, tool_call + + /// 输入输出 + input Json? @db.JsonB /// 步骤输入 + output Json? @db.JsonB /// 步骤输出 + + /// 执行信息 + stageCode String? @map("stage_code") /// 执行时的阶段 + modelUsed String? @map("model_used") /// 使用的模型 + tokensUsed Int? @map("tokens_used") /// 消耗的Token + durationMs Int? @map("duration_ms") /// 执行时长(毫秒) + + /// 错误信息 + errorType String? @map("error_type") /// 错误类型 + errorMsg String? @map("error_msg") /// 错误信息 + + /// 关联 + session AgentSession @relation(fields: [sessionId], references: [id], onDelete: Cascade) + + createdAt DateTime @default(now()) @map("created_at") + + @@index([sessionId, stepIndex], map: "idx_agent_trace_session_step") + @@index([traceId], map: "idx_agent_trace_trace_id") + @@index([stepType], map: "idx_agent_trace_step_type") + @@map("agent_traces") + @@schema("agent_schema") +} + +/// Reflexion规则表 - 定义质量检查规则 +model ReflexionRule { + id String @id @default(uuid()) + agentId String @map("agent_id") /// 关联的Agent + + /// 规则标识 + ruleCode String @map("rule_code") /// 规则代码 + ruleName String @map("rule_name") /// 规则名称 + + /// 触发条件 + triggerStage String? @map("trigger_stage") /// 触发阶段(null表示全局) + triggerTiming String @map("trigger_timing") /// on_sync, on_stage_complete, on_generate + + /// 规则类型 + ruleType String @map("rule_type") /// rule_based, prompt_based + + /// 规则内容 + conditions Json? @db.JsonB /// 规则条件(rule_based时使用) + promptTemplate String? @map("prompt_template") @db.Text /// Prompt模板(prompt_based时使用) + + /// 行为配置 + severity String @default("warning") /// error, warning, info + failureAction String @default("warn") @map("failure_action") /// block, warn, log + + /// 状态 + isActive Boolean @default(true) @map("is_active") + sortOrder Int @default(0) @map("sort_order") + + /// 关联 + agent AgentDefinition @relation(fields: [agentId], references: [id], onDelete: Cascade) + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@unique([agentId, ruleCode], map: "unique_agent_rule") + @@index([agentId, triggerStage], map: "idx_reflexion_rule_agent_stage") + @@index([isActive], map: "idx_reflexion_rule_active") + @@map("reflexion_rules") + @@schema("agent_schema") +} + +// ============================================================ +// Protocol Agent Schema (protocol_schema) +// Protocol Agent专用 - 研究方案制定 +// ============================================================ + +/// Protocol Context表 - 存储研究方案的核心上下文数据 +model ProtocolContext { + id String @id @default(uuid()) + conversationId String @unique @map("conversation_id") /// 关联的对话ID + userId String @map("user_id") + + /// 当前状态 + currentStage String @default("scientific_question") @map("current_stage") + status String @default("in_progress") /// in_progress, completed, abandoned + + /// ===== 5个核心阶段数据 ===== + + /// 阶段1: 科学问题 + scientificQuestion Json? @map("scientific_question") @db.JsonB + /// { content, background, significance, confirmed, confirmedAt } + + /// 阶段2: PICO + pico Json? @db.JsonB + /// { P: {value, details}, I: {}, C: {}, O: {}, confirmed, confirmedAt } + + /// 阶段3: 研究设计 + studyDesign Json? @map("study_design") @db.JsonB + /// { type, blinding, randomization, duration, multiCenter, confirmed } + + /// 阶段4: 样本量 + sampleSize Json? @map("sample_size") @db.JsonB + /// { total, perGroup, alpha, power, effectSize, dropoutRate, justification, confirmed } + + /// 阶段5: 观察指标(终点指标) + endpoints Json? @db.JsonB + /// { primary: [{name, definition, method, timePoint}], secondary: [], safety: [], confirmed } + + /// ===== 元数据 ===== + completedStages String[] @default([]) @map("completed_stages") /// 已完成的阶段列表 + lastActiveAt DateTime @default(now()) @map("last_active_at") + + /// 关联 + generations ProtocolGeneration[] + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@index([userId], map: "idx_protocol_context_user") + @@index([status], map: "idx_protocol_context_status") + @@index([currentStage], map: "idx_protocol_context_stage") + @@map("protocol_contexts") + @@schema("protocol_schema") +} + +/// Protocol生成记录表 - 存储一键生成的研究方案 +model ProtocolGeneration { + id String @id @default(uuid()) + contextId String @map("context_id") /// 关联的Context + userId String @map("user_id") + + /// 生成内容 + generatedContent String @map("generated_content") @db.Text /// 生成的研究方案全文(Markdown) + contentVersion Int @default(1) @map("content_version") /// 版本号 + + /// 使用的Prompt + promptUsed String @map("prompt_used") @db.Text /// 实际使用的Prompt + + /// 生成参数 + modelUsed String @map("model_used") /// 使用的模型 + tokensUsed Int? @map("tokens_used") /// 消耗的Token + durationMs Int? @map("duration_ms") /// 生成耗时(毫秒) + + /// 导出记录 + wordFileKey String? @map("word_file_key") /// Word文件OSS Key + lastExportedAt DateTime? @map("last_exported_at") + + /// 状态 + status String @default("completed") /// generating, completed, failed + errorMessage String? @map("error_message") + + /// 关联 + context ProtocolContext @relation(fields: [contextId], references: [id], onDelete: Cascade) + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@index([contextId], map: "idx_protocol_gen_context") + @@index([userId, createdAt], map: "idx_protocol_gen_user_time") + @@map("protocol_generations") + @@schema("protocol_schema") +} diff --git a/backend/prisma/seeds/protocol-agent-seed.ts b/backend/prisma/seeds/protocol-agent-seed.ts new file mode 100644 index 00000000..0f10fa68 --- /dev/null +++ b/backend/prisma/seeds/protocol-agent-seed.ts @@ -0,0 +1,560 @@ +/** + * Protocol Agent 初始配置数据种子 + * + * 运行方式: npx tsx prisma/seeds/protocol-agent-seed.ts + */ + +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +async function main() { + console.log('🌱 Seeding Protocol Agent configuration...'); + + // 1. 创建Agent定义 + const agentDefinition = await prisma.agentDefinition.upsert({ + where: { code: 'protocol_agent' }, + update: {}, + create: { + code: 'protocol_agent', + name: '研究方案制定助手', + description: '帮助研究者系统地制定临床研究方案,覆盖科学问题、PICO、研究设计、样本量和观察指标5个核心阶段', + version: '1.0.0', + config: { + defaultModel: 'deepseek-v3', + maxTurns: 100, + timeout: 60000, + enableTrace: true, + enableReflexion: true, + }, + isActive: true, + }, + }); + + console.log('✅ Created Agent Definition:', agentDefinition.code); + + // 2. 创建5个阶段 + const stages = [ + { + stageCode: 'scientific_question', + stageName: '科学问题梳理', + sortOrder: 1, + isInitial: true, + isFinal: false, + nextStages: ['pico'], + config: { + requiredFields: ['content'], + minContentLength: 10, + }, + }, + { + stageCode: 'pico', + stageName: 'PICO要素', + sortOrder: 2, + isInitial: false, + isFinal: false, + nextStages: ['study_design'], + config: { + requiredFields: ['P', 'I', 'C', 'O'], + }, + }, + { + stageCode: 'study_design', + stageName: '研究设计', + sortOrder: 3, + isInitial: false, + isFinal: false, + nextStages: ['sample_size'], + config: { + requiredFields: ['type'], + }, + }, + { + stageCode: 'sample_size', + stageName: '样本量计算', + sortOrder: 4, + isInitial: false, + isFinal: false, + nextStages: ['endpoints'], + config: { + requiredFields: ['total'], + }, + }, + { + stageCode: 'endpoints', + stageName: '观察指标', + sortOrder: 5, + isInitial: false, + isFinal: true, + nextStages: [], + config: { + requiredFields: ['primary'], + }, + }, + ]; + + for (const stage of stages) { + await prisma.agentStage.upsert({ + where: { + agentId_stageCode: { + agentId: agentDefinition.id, + stageCode: stage.stageCode, + }, + }, + update: stage, + create: { + agentId: agentDefinition.id, + ...stage, + }, + }); + console.log(` ✅ Stage: ${stage.stageName}`); + } + + // 3. 创建Prompt模板 + const prompts = [ + // 系统Prompt + { + promptType: 'system', + promptCode: 'protocol_system', + content: `你是一位经验丰富的临床研究方法学专家,正在帮助研究者制定研究方案。 + +你的职责: +1. 系统引导用户完成研究方案的5个核心要素:科学问题、PICO、研究设计、样本量、观察指标 +2. 提供专业、准确的方法学建议 +3. 确保研究设计的科学性和可行性 +4. 使用通俗易懂的语言,同时保持学术严谨性 + +当前阶段: {{context.currentStage}} +已完成阶段: {{context.completedStages}} + +请根据用户的输入,提供专业指导。`, + variables: ['context'], + }, + // 科学问题阶段Prompt + { + promptType: 'stage', + promptCode: 'stage_scientific_question', + stageCode: 'scientific_question', + content: `【科学问题梳理阶段】 + +你正在帮助用户梳理研究的科学问题。一个好的科学问题应该: +- 明确、具体、可操作 +- 有实际的临床或学术意义 +- 可通过研究方法验证 + +{{#if context.scientificQuestion}} +用户当前的科学问题草稿: +{{context.scientificQuestion.content}} +{{/if}} + +请引导用户: +1. 描述研究背景和动机 +2. 明确想要解决的核心问题 +3. 阐述研究的潜在意义 + +当用户表达清晰后,帮助整理成规范的科学问题陈述,并提供"同步到方案"按钮。`, + variables: ['context'], + }, + // PICO阶段Prompt + { + promptType: 'stage', + promptCode: 'stage_pico', + stageCode: 'pico', + content: `【PICO要素梳理阶段】 + +PICO是临床研究问题结构化的核心框架: +- P (Population): 研究人群 +- I (Intervention): 干预措施 +- C (Comparison): 对照措施 +- O (Outcome): 结局指标 + +{{#if context.pico}} +当前PICO: +- P: {{context.pico.P.value}} +- I: {{context.pico.I.value}} +- C: {{context.pico.C.value}} +- O: {{context.pico.O.value}} +{{/if}} + +请引导用户逐一明确四个要素,确保: +1. P: 纳入标准、排除标准清晰 +2. I: 干预措施具体可操作 +3. C: 对照组设置合理 +4. O: 结局指标可测量、有临床意义 + +当四要素都明确后,提供"同步到方案"按钮。`, + variables: ['context'], + }, + // 研究设计阶段Prompt + { + promptType: 'stage', + promptCode: 'stage_study_design', + stageCode: 'study_design', + content: `【研究设计阶段】 + +根据科学问题和PICO,需要确定合适的研究设计: + +科学问题:{{context.scientificQuestion.content}} +PICO: +- P: {{context.pico.P.value}} +- I: {{context.pico.I.value}} + +常见研究类型: +- 随机对照试验(RCT):最高证据等级,适合验证干预效果 +- 队列研究:适合观察性研究,探索风险因素 +- 病例对照研究:适合罕见疾病研究 +- 横断面研究:描述性研究 + +请引导用户确定: +1. 研究类型 +2. 盲法设计(如适用) +3. 随机化方法(如适用) +4. 研究周期 +5. 是否多中心 + +设计确定后,提供"同步到方案"按钮。`, + variables: ['context'], + }, + // 样本量阶段Prompt + { + promptType: 'stage', + promptCode: 'stage_sample_size', + stageCode: 'sample_size', + content: `【样本量计算阶段】 + +样本量计算需要考虑: +- α (显著性水平): 通常0.05 +- β (统计效力): 通常0.8-0.9 +- 预期效应量 +- 预计脱落率 + +研究设计:{{context.studyDesign.type}} +主要结局:{{context.pico.O.value}} + +请引导用户: +1. 确定检验类型(优效、非劣效、等效) +2. 估计预期效应量(基于文献或预试验) +3. 设定显著性水平和统计效力 +4. 考虑脱落率调整 + +可以使用样本量计算工具辅助计算。 + +样本量确定后,提供"同步到方案"按钮。`, + variables: ['context'], + }, + // 观察指标阶段Prompt + { + promptType: 'stage', + promptCode: 'stage_endpoints', + stageCode: 'endpoints', + content: `【观察指标设计阶段】 + +观察指标是评价研究结果的关键: + +研究类型:{{context.studyDesign.type}} +PICO-O:{{context.pico.O.value}} + +需要明确的指标类型: +1. **主要结局指标(Primary Endpoint)**: + - 与科学问题直接相关 + - 用于样本量计算 + - 每个研究通常只有1-2个 + +2. **次要结局指标(Secondary Endpoints)**: + - 支持主要结局的补充指标 + - 可以有多个 + +3. **安全性指标(Safety Endpoints)**: + - 不良事件、实验室检查等 + +4. **探索性指标(Exploratory)**: + - 为未来研究提供线索 + +请引导用户定义每个指标的: +- 名称 +- 操作定义 +- 测量方法 +- 评价时点 + +所有指标确定后,提供"同步到方案"按钮。 + +🎉 完成观察指标后,您可以点击"一键生成研究方案"生成完整方案文档!`, + variables: ['context'], + }, + // 数据提取Prompt + { + promptType: 'extraction', + promptCode: 'extraction_scientific_question', + stageCode: 'scientific_question', + content: `请从以下对话中提取科学问题信息: + +用户消息:{{userMessage}} + +请以JSON格式输出: +{ + "content": "完整的科学问题陈述", + "background": "研究背景", + "significance": "研究意义", + "readyToSync": true/false +} + +如果信息不完整,readyToSync设为false。`, + variables: ['userMessage'], + }, + { + promptType: 'extraction', + promptCode: 'extraction_pico', + stageCode: 'pico', + content: `请从以下对话中提取PICO要素: + +用户消息:{{userMessage}} +当前PICO:{{currentPico}} + +请以JSON格式输出: +{ + "P": { "value": "研究人群", "details": "详细描述" }, + "I": { "value": "干预措施", "details": "详细描述" }, + "C": { "value": "对照措施", "details": "详细描述" }, + "O": { "value": "结局指标", "details": "详细描述" }, + "readyToSync": true/false +} + +只更新用户提到的字段,保留其他字段不变。 +如果PICO四要素都已完整,readyToSync设为true。`, + variables: ['userMessage', 'currentPico'], + }, + // 研究方案生成Prompt + { + promptType: 'generation', + promptCode: 'generate_protocol', + content: `你是一位资深的临床研究方法学专家,请基于以下核心要素生成一份完整、规范的临床研究方案。 + +## 核心要素 + +### 科学问题 +{{scientificQuestion.content}} +{{#if scientificQuestion.background}}背景:{{scientificQuestion.background}}{{/if}} + +### PICO要素 +- **研究人群(P)**: {{pico.P.value}} + {{pico.P.details}} +- **干预措施(I)**: {{pico.I.value}} + {{pico.I.details}} +- **对照措施(C)**: {{pico.C.value}} + {{pico.C.details}} +- **结局指标(O)**: {{pico.O.value}} + {{pico.O.details}} + +### 研究设计 +- 研究类型: {{studyDesign.type}} +- 盲法设计: {{studyDesign.blinding}} +- 随机化方法: {{studyDesign.randomization}} +- 研究周期: {{studyDesign.duration}} +{{#if studyDesign.multiCenter}}- 多中心: 是,{{studyDesign.centerCount}}个中心{{/if}} + +### 样本量 +- 总样本量: {{sampleSize.total}} +- 每组样本量: {{sampleSize.perGroup}} +- 计算依据: {{sampleSize.justification}} + +### 观察指标 +**主要结局指标:** +{{#each endpoints.primary}} +- {{name}}: {{definition}} ({{method}}, {{timePoint}}) +{{/each}} + +**次要结局指标:** +{{#each endpoints.secondary}} +- {{name}}: {{definition}} +{{/each}} + +**安全性指标:** +{{#each endpoints.safety}} +- {{name}}: {{definition}} +{{/each}} + +--- + +## 生成要求 + +请生成包含以下章节的完整研究方案: + +1. **研究背景与立题依据** + - 疾病/问题背景 + - 国内外研究现状 + - 研究的必要性和意义 + +2. **研究目的** + - 主要目的 + - 次要目的 + +3. **研究方法** + - 研究类型与设计 + - 研究对象 + - 干预措施 + - 对照设置 + - 随机化与盲法 + +4. **受试者选择** + - 入选标准 + - 排除标准 + - 退出/剔除标准 + +5. **观察指标与评价标准** + - 主要疗效指标 + - 次要疗效指标 + - 安全性指标 + - 评价时点 + +6. **统计分析计划** + - 样本量估算 + - 分析数据集定义 + - 统计方法 + +7. **质量控制** + - 数据管理 + - 质量保证措施 + +8. **伦理考虑** + - 伦理审查 + - 知情同意 + - 受试者保护 + +9. **研究进度安排** + - 时间节点 + - 里程碑 + +请使用专业、规范的学术语言,确保内容完整、逻辑清晰、符合临床研究规范。`, + variables: ['scientificQuestion', 'pico', 'studyDesign', 'sampleSize', 'endpoints'], + }, + ]; + + // 获取阶段ID映射 + const stageIdMap = new Map(); + const savedStages = await prisma.agentStage.findMany({ + where: { agentId: agentDefinition.id }, + }); + for (const stage of savedStages) { + stageIdMap.set(stage.stageCode, stage.id); + } + + // 创建Prompts + for (const prompt of prompts) { + const stageId = prompt.stageCode ? stageIdMap.get(prompt.stageCode) : null; + + await prisma.agentPrompt.upsert({ + where: { + agentId_promptCode_version: { + agentId: agentDefinition.id, + promptCode: prompt.promptCode, + version: 1, + }, + }, + update: { + content: prompt.content, + variables: prompt.variables, + }, + create: { + agentId: agentDefinition.id, + stageId: stageId, + promptType: prompt.promptType, + promptCode: prompt.promptCode, + content: prompt.content, + variables: prompt.variables, + version: 1, + isActive: true, + }, + }); + console.log(` ✅ Prompt: ${prompt.promptCode}`); + } + + // 4. 创建Reflexion规则 + const reflexionRules = [ + { + ruleCode: 'scientific_question_completeness', + ruleName: '科学问题完整性检查', + triggerStage: 'scientific_question', + triggerTiming: 'on_sync', + ruleType: 'rule_based', + conditions: { + content: { required: true, minLength: 10 }, + }, + severity: 'warning', + failureAction: 'warn', + sortOrder: 1, + }, + { + ruleCode: 'pico_completeness', + ruleName: 'PICO要素完整性检查', + triggerStage: 'pico', + triggerTiming: 'on_sync', + ruleType: 'rule_based', + conditions: { + P: 'required', + I: 'required', + C: 'required', + O: 'required', + }, + severity: 'error', + failureAction: 'warn', + sortOrder: 2, + }, + { + ruleCode: 'sample_size_validity', + ruleName: '样本量有效性检查', + triggerStage: 'sample_size', + triggerTiming: 'on_sync', + ruleType: 'rule_based', + conditions: { + total: { required: true, min: 1 }, + }, + severity: 'error', + failureAction: 'warn', + sortOrder: 3, + }, + { + ruleCode: 'endpoints_primary_required', + ruleName: '主要终点指标必填', + triggerStage: 'endpoints', + triggerTiming: 'on_sync', + ruleType: 'rule_based', + conditions: { + primary: { notEmpty: true }, + }, + severity: 'error', + failureAction: 'warn', + sortOrder: 4, + }, + ]; + + for (const rule of reflexionRules) { + await prisma.reflexionRule.upsert({ + where: { + agentId_ruleCode: { + agentId: agentDefinition.id, + ruleCode: rule.ruleCode, + }, + }, + update: rule, + create: { + agentId: agentDefinition.id, + ...rule, + isActive: true, + }, + }); + console.log(` ✅ Rule: ${rule.ruleName}`); + } + + console.log('\n🎉 Protocol Agent configuration seeded successfully!'); +} + +main() + .catch((e) => { + console.error('❌ Seed failed:', e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); + + diff --git a/backend/rebuild-and-push.ps1 b/backend/rebuild-and-push.ps1 index 726ddeed..a1638313 100644 --- a/backend/rebuild-and-push.ps1 +++ b/backend/rebuild-and-push.ps1 @@ -135,6 +135,8 @@ Write-Host "" + + diff --git a/backend/recover-code-from-cursor-db.js b/backend/recover-code-from-cursor-db.js index 0ee79904..09dd991b 100644 --- a/backend/recover-code-from-cursor-db.js +++ b/backend/recover-code-from-cursor-db.js @@ -245,6 +245,8 @@ function extractCodeBlocks(obj, blocks = []) { + + diff --git a/backend/restore_job_common.sql b/backend/restore_job_common.sql index 5c06766f..8a9a2470 100644 --- a/backend/restore_job_common.sql +++ b/backend/restore_job_common.sql @@ -43,5 +43,7 @@ CREATE TABLE IF NOT EXISTS platform_schema.job_common ( + + diff --git a/backend/restore_pgboss_functions.sql b/backend/restore_pgboss_functions.sql index 88d52a99..07cdc2f3 100644 --- a/backend/restore_pgboss_functions.sql +++ b/backend/restore_pgboss_functions.sql @@ -117,5 +117,7 @@ CREATE OR REPLACE FUNCTION platform_schema.delete_queue(queue_name text) RETURNS + + diff --git a/backend/scripts/check-dc-tables.mjs b/backend/scripts/check-dc-tables.mjs index 4bf6760d..c01eb871 100644 --- a/backend/scripts/check-dc-tables.mjs +++ b/backend/scripts/check-dc-tables.mjs @@ -264,6 +264,8 @@ checkDCTables(); + + diff --git a/backend/scripts/create-capability-schema.sql b/backend/scripts/create-capability-schema.sql index e94f323d..b59ad44b 100644 --- a/backend/scripts/create-capability-schema.sql +++ b/backend/scripts/create-capability-schema.sql @@ -18,5 +18,7 @@ CREATE SCHEMA IF NOT EXISTS capability_schema; + + diff --git a/backend/scripts/create-tool-c-ai-history-table.mjs b/backend/scripts/create-tool-c-ai-history-table.mjs index c4c0d750..e50c6cfc 100644 --- a/backend/scripts/create-tool-c-ai-history-table.mjs +++ b/backend/scripts/create-tool-c-ai-history-table.mjs @@ -216,6 +216,8 @@ createAiHistoryTable() + + diff --git a/backend/scripts/create-tool-c-table.js b/backend/scripts/create-tool-c-table.js index d023beda..5bcd43b0 100644 --- a/backend/scripts/create-tool-c-table.js +++ b/backend/scripts/create-tool-c-table.js @@ -203,6 +203,8 @@ createToolCTable() + + diff --git a/backend/scripts/create-tool-c-table.mjs b/backend/scripts/create-tool-c-table.mjs index 80092fdb..5b07a6e5 100644 --- a/backend/scripts/create-tool-c-table.mjs +++ b/backend/scripts/create-tool-c-table.mjs @@ -200,6 +200,8 @@ createToolCTable() + + diff --git a/backend/scripts/migrate-aia-prompts.ts b/backend/scripts/migrate-aia-prompts.ts index ecf0300b..01d3cecf 100644 --- a/backend/scripts/migrate-aia-prompts.ts +++ b/backend/scripts/migrate-aia-prompts.ts @@ -323,3 +323,5 @@ main() + + diff --git a/backend/scripts/setup-prompt-system.ts b/backend/scripts/setup-prompt-system.ts index 24ff23c5..62ec8599 100644 --- a/backend/scripts/setup-prompt-system.ts +++ b/backend/scripts/setup-prompt-system.ts @@ -128,5 +128,7 @@ main() + + diff --git a/backend/scripts/test-pkb-apis-simple.ts b/backend/scripts/test-pkb-apis-simple.ts index b6e3ad20..52d34955 100644 --- a/backend/scripts/test-pkb-apis-simple.ts +++ b/backend/scripts/test-pkb-apis-simple.ts @@ -347,6 +347,8 @@ runTests().catch(error => { + + diff --git a/backend/scripts/test-prompt-api.ts b/backend/scripts/test-prompt-api.ts index 57e8df47..02852e44 100644 --- a/backend/scripts/test-prompt-api.ts +++ b/backend/scripts/test-prompt-api.ts @@ -94,5 +94,7 @@ testAPI().catch(console.error); + + diff --git a/backend/scripts/test-unifuncs-deepsearch.ts b/backend/scripts/test-unifuncs-deepsearch.ts index c6df9ffa..a162e10b 100644 --- a/backend/scripts/test-unifuncs-deepsearch.ts +++ b/backend/scripts/test-unifuncs-deepsearch.ts @@ -126,3 +126,5 @@ testDeepSearch().catch(console.error); + + diff --git a/backend/scripts/verify-pkb-rvw-schema.ts b/backend/scripts/verify-pkb-rvw-schema.ts index ecae9959..c52c6124 100644 --- a/backend/scripts/verify-pkb-rvw-schema.ts +++ b/backend/scripts/verify-pkb-rvw-schema.ts @@ -312,6 +312,8 @@ verifySchemas() + + diff --git a/backend/src/common/auth/jwt.service.ts b/backend/src/common/auth/jwt.service.ts index e43ad06e..a7b5a92f 100644 --- a/backend/src/common/auth/jwt.service.ts +++ b/backend/src/common/auth/jwt.service.ts @@ -201,5 +201,7 @@ export const jwtService = new JWTService(); + + diff --git a/backend/src/common/jobs/utils.ts b/backend/src/common/jobs/utils.ts index a68a90c4..1c7259b8 100644 --- a/backend/src/common/jobs/utils.ts +++ b/backend/src/common/jobs/utils.ts @@ -332,6 +332,8 @@ export function getBatchItems( + + diff --git a/backend/src/common/prompt/prompt.types.ts b/backend/src/common/prompt/prompt.types.ts index 5aded3b2..81ae775b 100644 --- a/backend/src/common/prompt/prompt.types.ts +++ b/backend/src/common/prompt/prompt.types.ts @@ -84,5 +84,7 @@ export interface VariableValidation { + + diff --git a/backend/src/common/rag/ChunkService.ts b/backend/src/common/rag/ChunkService.ts index 8176bf9c..e36eeca4 100644 --- a/backend/src/common/rag/ChunkService.ts +++ b/backend/src/common/rag/ChunkService.ts @@ -356,3 +356,5 @@ export default ChunkService; + + diff --git a/backend/src/common/rag/DifyClient.ts b/backend/src/common/rag/DifyClient.ts index 237f403b..f6aac079 100644 --- a/backend/src/common/rag/DifyClient.ts +++ b/backend/src/common/rag/DifyClient.ts @@ -52,3 +52,5 @@ export const DifyClient = DeprecatedDifyClient; + + diff --git a/backend/src/common/streaming/OpenAIStreamAdapter.ts b/backend/src/common/streaming/OpenAIStreamAdapter.ts index e496c59f..ade63c34 100644 --- a/backend/src/common/streaming/OpenAIStreamAdapter.ts +++ b/backend/src/common/streaming/OpenAIStreamAdapter.ts @@ -207,3 +207,5 @@ export function createOpenAIStreamAdapter( + + diff --git a/backend/src/common/streaming/StreamingService.ts b/backend/src/common/streaming/StreamingService.ts index a856f80d..6470bc64 100644 --- a/backend/src/common/streaming/StreamingService.ts +++ b/backend/src/common/streaming/StreamingService.ts @@ -213,3 +213,5 @@ export async function streamChat( + + diff --git a/backend/src/common/streaming/index.ts b/backend/src/common/streaming/index.ts index 654a4759..eb29636d 100644 --- a/backend/src/common/streaming/index.ts +++ b/backend/src/common/streaming/index.ts @@ -31,3 +31,5 @@ export { THINKING_TAGS } from './types'; + + diff --git a/backend/src/common/streaming/types.ts b/backend/src/common/streaming/types.ts index e1bfd612..dbbe9011 100644 --- a/backend/src/common/streaming/types.ts +++ b/backend/src/common/streaming/types.ts @@ -106,3 +106,5 @@ export type SSEEventType = + + diff --git a/backend/src/index.ts b/backend/src/index.ts index ed1de09b..f743c410 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -159,6 +159,15 @@ logger.info('✅ PKB个人知识库路由已注册: /api/v1/pkb'); await fastify.register(aiaRoutes, { prefix: '/api/v1/aia' }); logger.info('✅ AIA智能问答路由已注册: /api/v1/aia'); +// ============================================ +// 【业务模块】Protocol Agent - 研究方案制定Agent +// ============================================ +import { protocolAgentRoutes } from './modules/agent/protocol/index.js'; +await fastify.register((instance, opts, done) => { + protocolAgentRoutes(instance, { prisma, ...opts }).then(() => done()).catch(done); +}, { prefix: '/api/v1/aia/protocol-agent' }); +logger.info('✅ Protocol Agent路由已注册: /api/v1/aia/protocol-agent'); + // ============================================ // 【业务模块】ASL - AI智能文献筛选 // ============================================ diff --git a/backend/src/modules/admin/routes/tenantRoutes.ts b/backend/src/modules/admin/routes/tenantRoutes.ts index cf2a24e7..59795e29 100644 --- a/backend/src/modules/admin/routes/tenantRoutes.ts +++ b/backend/src/modules/admin/routes/tenantRoutes.ts @@ -92,3 +92,5 @@ export async function moduleRoutes(fastify: FastifyInstance) { + + diff --git a/backend/src/modules/admin/types/tenant.types.ts b/backend/src/modules/admin/types/tenant.types.ts index 6b91ab40..ce24e30c 100644 --- a/backend/src/modules/admin/types/tenant.types.ts +++ b/backend/src/modules/admin/types/tenant.types.ts @@ -122,3 +122,5 @@ export interface PaginatedResponse { + + diff --git a/backend/src/modules/admin/types/user.types.ts b/backend/src/modules/admin/types/user.types.ts index f796b32a..6dcb1a57 100644 --- a/backend/src/modules/admin/types/user.types.ts +++ b/backend/src/modules/admin/types/user.types.ts @@ -169,3 +169,5 @@ export const ROLE_DISPLAY_NAMES: Record = { + + diff --git a/backend/src/modules/agent/index.ts b/backend/src/modules/agent/index.ts new file mode 100644 index 00000000..6f4d3afd --- /dev/null +++ b/backend/src/modules/agent/index.ts @@ -0,0 +1,25 @@ +/** + * Agent Framework Module + * + * 通用Agent框架入口 + * + * @module agent + */ + +// Types +export * from './types/index.js'; + +// Services +export { + ConfigLoader, + BaseAgentOrchestrator, + QueryAnalyzer, + StageManager, + TraceLogger, + type OrchestratorDependencies, + type LLMServiceInterface, + type IntentType, + type TransitionCondition, + type TraceLogInput, +} from './services/index.js'; + diff --git a/backend/src/modules/agent/protocol/controllers/ProtocolAgentController.ts b/backend/src/modules/agent/protocol/controllers/ProtocolAgentController.ts new file mode 100644 index 00000000..1c5517e0 --- /dev/null +++ b/backend/src/modules/agent/protocol/controllers/ProtocolAgentController.ts @@ -0,0 +1,287 @@ +/** + * Protocol Agent Controller + * 处理Protocol Agent的HTTP请求 + * + * @module agent/protocol/controllers/ProtocolAgentController + */ + +import { FastifyRequest, FastifyReply } from 'fastify'; +import { PrismaClient } from '@prisma/client'; +import { ProtocolOrchestrator } from '../services/ProtocolOrchestrator.js'; +import { LLMServiceInterface } from '../../services/BaseAgentOrchestrator.js'; +import { ProtocolStageCode } from '../../types/index.js'; + +// 请求类型定义 +interface SendMessageBody { + conversationId: string; + content: string; + messageId?: string; +} + +interface SyncDataBody { + conversationId: string; + stageCode: ProtocolStageCode; + data: Record; +} + +interface GenerateProtocolBody { + conversationId: string; + options?: { + sections?: string[]; + style?: 'academic' | 'concise'; + }; +} + +interface GetContextParams { + conversationId: string; +} + +export class ProtocolAgentController { + private orchestrator: ProtocolOrchestrator; + + constructor(prisma: PrismaClient, llmService: LLMServiceInterface) { + this.orchestrator = new ProtocolOrchestrator({ prisma, llmService }); + } + + /** + * 发送消息 + * POST /api/aia/protocol-agent/message + */ + async sendMessage( + request: FastifyRequest<{ Body: SendMessageBody }>, + reply: FastifyReply + ): Promise { + try { + const { conversationId, content, messageId } = request.body; + const userId = (request as any).user?.userId; + + if (!userId) { + reply.code(401).send({ error: 'Unauthorized' }); + return; + } + + if (!conversationId || !content) { + reply.code(400).send({ error: 'Missing required fields: conversationId, content' }); + return; + } + + const response = await this.orchestrator.handleMessage({ + conversationId, + userId, + content, + messageId, + }); + + reply.send({ + success: true, + data: response, + }); + } catch (error) { + console.error('[ProtocolAgentController] sendMessage error:', error); + reply.code(500).send({ + success: false, + error: error instanceof Error ? error.message : 'Internal server error', + }); + } + } + + /** + * 同步阶段数据 + * POST /api/aia/protocol-agent/sync + */ + async syncData( + request: FastifyRequest<{ Body: SyncDataBody }>, + reply: FastifyReply + ): Promise { + try { + const { conversationId, stageCode, data } = request.body; + const userId = (request as any).user?.userId; + + if (!userId) { + reply.code(401).send({ error: 'Unauthorized' }); + return; + } + + if (!conversationId || !stageCode) { + reply.code(400).send({ error: 'Missing required fields: conversationId, stageCode' }); + return; + } + + const result = await this.orchestrator.handleProtocolSync( + conversationId, + userId, + stageCode, + data + ); + + reply.send({ + success: true, + data: result, + }); + } catch (error) { + console.error('[ProtocolAgentController] syncData error:', error); + reply.code(500).send({ + success: false, + error: error instanceof Error ? error.message : 'Internal server error', + }); + } + } + + /** + * 获取上下文状态 + * GET /api/aia/protocol-agent/context/:conversationId + */ + async getContext( + request: FastifyRequest<{ Params: GetContextParams }>, + reply: FastifyReply + ): Promise { + try { + const { conversationId } = request.params; + + if (!conversationId) { + reply.code(400).send({ error: 'Missing conversationId' }); + return; + } + + const summary = await this.orchestrator.getContextSummary(conversationId); + + if (!summary) { + reply.code(404).send({ error: 'Context not found' }); + return; + } + + reply.send({ + success: true, + data: summary, + }); + } catch (error) { + console.error('[ProtocolAgentController] getContext error:', error); + reply.code(500).send({ + success: false, + error: error instanceof Error ? error.message : 'Internal server error', + }); + } + } + + /** + * 一键生成研究方案 + * POST /api/aia/protocol-agent/generate + */ + async generateProtocol( + request: FastifyRequest<{ Body: GenerateProtocolBody }>, + reply: FastifyReply + ): Promise { + try { + const { conversationId, options } = request.body; + const userId = (request as any).user?.userId; + + if (!userId) { + reply.code(401).send({ error: 'Unauthorized' }); + return; + } + + if (!conversationId) { + reply.code(400).send({ error: 'Missing conversationId' }); + return; + } + + // 获取上下文 + const contextService = this.orchestrator.getContextService(); + const context = await contextService.getContext(conversationId); + + if (!context) { + reply.code(404).send({ error: 'Context not found' }); + return; + } + + // 检查是否所有阶段都已完成 + if (!contextService.isAllStagesCompleted(context)) { + reply.code(400).send({ + error: '请先完成所有5个阶段(科学问题、PICO、研究设计、样本量、观察指标)' + }); + return; + } + + // TODO: 实现方案生成逻辑 + // 这里先返回占位响应,实际应该调用LLM生成完整方案 + reply.send({ + success: true, + data: { + generationId: 'placeholder', + status: 'generating', + message: '研究方案生成中...', + estimatedTime: 30, + }, + }); + } catch (error) { + console.error('[ProtocolAgentController] generateProtocol error:', error); + reply.code(500).send({ + success: false, + error: error instanceof Error ? error.message : 'Internal server error', + }); + } + } + + /** + * 获取生成的方案 + * GET /api/aia/protocol-agent/generation/:generationId + */ + async getGeneration( + request: FastifyRequest<{ Params: { generationId: string } }>, + reply: FastifyReply + ): Promise { + try { + const { generationId } = request.params; + + // TODO: 实现获取生成结果逻辑 + reply.send({ + success: true, + data: { + id: generationId, + status: 'completed', + content: '# 研究方案\n\n(生成中...)', + contentVersion: 1, + }, + }); + } catch (error) { + console.error('[ProtocolAgentController] getGeneration error:', error); + reply.code(500).send({ + success: false, + error: error instanceof Error ? error.message : 'Internal server error', + }); + } + } + + /** + * 导出Word文档 + * POST /api/aia/protocol-agent/generation/:generationId/export + */ + async exportWord( + request: FastifyRequest<{ + Params: { generationId: string }; + Body: { format: 'docx' | 'pdf' }; + }>, + reply: FastifyReply + ): Promise { + try { + const { generationId } = request.params; + const { format } = request.body; + + // TODO: 实现导出逻辑 + reply.send({ + success: true, + data: { + downloadUrl: `/api/aia/protocol-agent/download/${generationId}.${format}`, + expiresAt: new Date(Date.now() + 3600 * 1000).toISOString(), + }, + }); + } catch (error) { + console.error('[ProtocolAgentController] exportWord error:', error); + reply.code(500).send({ + success: false, + error: error instanceof Error ? error.message : 'Internal server error', + }); + } + } +} + diff --git a/backend/src/modules/agent/protocol/index.ts b/backend/src/modules/agent/protocol/index.ts new file mode 100644 index 00000000..62628c7c --- /dev/null +++ b/backend/src/modules/agent/protocol/index.ts @@ -0,0 +1,22 @@ +/** + * Protocol Agent Module + * 研究方案制定Agent + * + * @module agent/protocol + */ + +// Services +export { + ProtocolContextService, + ProtocolOrchestrator, + PromptBuilder, + LLMServiceAdapter, + createLLMServiceAdapter, +} from './services/index.js'; + +// Routes +export { protocolAgentRoutes } from './routes/index.js'; + +// Controllers +export { ProtocolAgentController } from './controllers/ProtocolAgentController.js'; + diff --git a/backend/src/modules/agent/protocol/routes/index.ts b/backend/src/modules/agent/protocol/routes/index.ts new file mode 100644 index 00000000..aabc771a --- /dev/null +++ b/backend/src/modules/agent/protocol/routes/index.ts @@ -0,0 +1,153 @@ +/** + * Protocol Agent Routes + * Protocol Agent的API路由定义 + * + * @module agent/protocol/routes + */ + +import { FastifyInstance } from 'fastify'; +import { PrismaClient } from '@prisma/client'; +import { ProtocolAgentController } from '../controllers/ProtocolAgentController.js'; +import { createLLMServiceAdapter } from '../services/LLMServiceAdapter.js'; +import { authenticate } from '../../../../common/auth/auth.middleware.js'; + +export async function protocolAgentRoutes( + fastify: FastifyInstance, + options: { prisma: PrismaClient } +): Promise { + const { prisma } = options; + + // 创建LLM服务(使用真实的LLM适配器) + const llmService = createLLMServiceAdapter(); + + // 创建控制器 + const controller = new ProtocolAgentController(prisma, llmService); + + // ============================================ + // Protocol Agent API Routes (需要认证) + // ============================================ + + // 发送消息 + fastify.post<{ + Body: { + conversationId: string; + content: string; + messageId?: string; + }; + }>('/message', { + preHandler: [authenticate], + schema: { + body: { + type: 'object', + required: ['conversationId', 'content'], + properties: { + conversationId: { type: 'string' }, + content: { type: 'string' }, + messageId: { type: 'string' }, + }, + }, + }, + }, (request, reply) => controller.sendMessage(request, reply)); + + // 同步阶段数据 + fastify.post('/sync', { + preHandler: [authenticate], + schema: { + body: { + type: 'object', + required: ['conversationId', 'stageCode'], + properties: { + conversationId: { type: 'string' }, + stageCode: { type: 'string' }, + data: { type: 'object' }, + }, + }, + }, + }, (request, reply) => controller.syncData(request as never, reply)); + + // 获取上下文状态 + fastify.get<{ + Params: { conversationId: string }; + }>('/context/:conversationId', { + preHandler: [authenticate], + schema: { + params: { + type: 'object', + required: ['conversationId'], + properties: { + conversationId: { type: 'string' }, + }, + }, + }, + }, (request, reply) => controller.getContext(request, reply)); + + // 一键生成研究方案 + fastify.post<{ + Body: { + conversationId: string; + options?: { + sections?: string[]; + style?: 'academic' | 'concise'; + }; + }; + }>('/generate', { + preHandler: [authenticate], + schema: { + body: { + type: 'object', + required: ['conversationId'], + properties: { + conversationId: { type: 'string' }, + options: { + type: 'object', + properties: { + sections: { type: 'array', items: { type: 'string' } }, + style: { type: 'string', enum: ['academic', 'concise'] }, + }, + }, + }, + }, + }, + }, (request, reply) => controller.generateProtocol(request, reply)); + + // 获取生成的方案 + fastify.get<{ + Params: { generationId: string }; + }>('/generation/:generationId', { + preHandler: [authenticate], + schema: { + params: { + type: 'object', + required: ['generationId'], + properties: { + generationId: { type: 'string' }, + }, + }, + }, + }, (request, reply) => controller.getGeneration(request, reply)); + + // 导出Word文档 + fastify.post<{ + Params: { generationId: string }; + Body: { format: 'docx' | 'pdf' }; + }>('/generation/:generationId/export', { + preHandler: [authenticate], + schema: { + params: { + type: 'object', + required: ['generationId'], + properties: { + generationId: { type: 'string' }, + }, + }, + body: { + type: 'object', + required: ['format'], + properties: { + format: { type: 'string', enum: ['docx', 'pdf'] }, + }, + }, + }, + }, (request, reply) => controller.exportWord(request, reply)); +} + diff --git a/backend/src/modules/agent/protocol/services/LLMServiceAdapter.ts b/backend/src/modules/agent/protocol/services/LLMServiceAdapter.ts new file mode 100644 index 00000000..8bab176a --- /dev/null +++ b/backend/src/modules/agent/protocol/services/LLMServiceAdapter.ts @@ -0,0 +1,184 @@ +/** + * LLM Service Adapter + * 将现有的LLM服务适配为Protocol Agent所需的接口 + * + * @module agent/protocol/services/LLMServiceAdapter + */ + +import { LLMFactory } from '../../../../common/llm/adapters/LLMFactory.js'; +import { ModelType, Message as LLMMessage, LLMOptions } from '../../../../common/llm/adapters/types.js'; +import { LLMServiceInterface } from '../../services/BaseAgentOrchestrator.js'; + +export class LLMServiceAdapter implements LLMServiceInterface { + private defaultModel: ModelType; + private defaultTemperature: number; + private defaultMaxTokens: number; + + constructor(options?: { + defaultModel?: ModelType; + defaultTemperature?: number; + defaultMaxTokens?: number; + }) { + this.defaultModel = options?.defaultModel ?? 'deepseek-v3'; + this.defaultTemperature = options?.defaultTemperature ?? 0.7; + this.defaultMaxTokens = options?.defaultMaxTokens ?? 4096; + } + + /** + * 调用LLM进行对话 + */ + async chat(params: { + messages: Array<{ role: string; content: string }>; + model?: string; + temperature?: number; + maxTokens?: number; + }): Promise<{ + content: string; + thinkingContent?: string; + tokensUsed: number; + model: string; + }> { + // 获取模型类型 + const modelType = this.parseModelType(params.model); + + // 获取LLM适配器 + const adapter = LLMFactory.getAdapter(modelType); + + // 转换消息格式 + const messages: LLMMessage[] = params.messages.map(m => ({ + role: m.role as 'system' | 'user' | 'assistant', + content: m.content, + })); + + // 调用LLM + const options: LLMOptions = { + temperature: params.temperature ?? this.defaultTemperature, + maxTokens: params.maxTokens ?? this.defaultMaxTokens, + }; + + try { + const response = await adapter.chat(messages, options); + + // 提取思考内容(如果有) + const { content, thinkingContent } = this.extractThinkingContent(response.content); + + return { + content, + thinkingContent, + tokensUsed: response.usage?.totalTokens ?? 0, + model: response.model, + }; + } catch (error) { + console.error('[LLMServiceAdapter] chat error:', error); + throw error; + } + } + + /** + * 流式调用LLM(返回AsyncGenerator) + */ + async *chatStream(params: { + messages: Array<{ role: string; content: string }>; + model?: string; + temperature?: number; + maxTokens?: number; + }): AsyncGenerator<{ + content: string; + done: boolean; + tokensUsed?: number; + }> { + const modelType = this.parseModelType(params.model); + const adapter = LLMFactory.getAdapter(modelType); + + const messages: LLMMessage[] = params.messages.map(m => ({ + role: m.role as 'system' | 'user' | 'assistant', + content: m.content, + })); + + const options: LLMOptions = { + temperature: params.temperature ?? this.defaultTemperature, + maxTokens: params.maxTokens ?? this.defaultMaxTokens, + stream: true, + }; + + try { + for await (const chunk of adapter.chatStream(messages, options)) { + yield { + content: chunk.content, + done: chunk.done, + tokensUsed: chunk.usage?.totalTokens, + }; + } + } catch (error) { + console.error('[LLMServiceAdapter] chatStream error:', error); + throw error; + } + } + + /** + * 解析模型类型 + */ + private parseModelType(model?: string): ModelType { + if (!model) return this.defaultModel; + + // 映射模型名称到ModelType + const modelMap: Record = { + 'deepseek-v3': 'deepseek-v3', + 'deepseek-chat': 'deepseek-v3', + 'qwen-max': 'qwen3-72b', + 'qwen3-72b': 'qwen3-72b', + 'qwen-long': 'qwen-long', + 'gpt-5': 'gpt-5', + 'gpt-5-pro': 'gpt-5', + 'claude-4.5': 'claude-4.5', + 'claude-sonnet': 'claude-4.5', + }; + + return modelMap[model.toLowerCase()] ?? this.defaultModel; + } + + /** + * 从响应中提取思考内容(...) + */ + private extractThinkingContent(content: string): { + content: string; + thinkingContent?: string; + } { + // 匹配 ...... + const thinkingPattern = /<(?:think|thinking)>([\s\S]*?)<\/(?:think|thinking)>/gi; + const matches = content.matchAll(thinkingPattern); + + let thinkingContent = ''; + let cleanContent = content; + + for (const match of matches) { + thinkingContent += match[1].trim() + '\n'; + cleanContent = cleanContent.replace(match[0], '').trim(); + } + + return { + content: cleanContent, + thinkingContent: thinkingContent ? thinkingContent.trim() : undefined, + }; + } + + /** + * 获取支持的模型列表 + */ + getSupportedModels(): string[] { + return LLMFactory.getSupportedModels(); + } +} + +/** + * 创建默认的LLM服务适配器 + */ +export function createLLMServiceAdapter(): LLMServiceInterface { + return new LLMServiceAdapter({ + defaultModel: 'deepseek-v3', + defaultTemperature: 0.7, + defaultMaxTokens: 4096, + }); +} + + diff --git a/backend/src/modules/agent/protocol/services/PromptBuilder.ts b/backend/src/modules/agent/protocol/services/PromptBuilder.ts new file mode 100644 index 00000000..7070ab4e --- /dev/null +++ b/backend/src/modules/agent/protocol/services/PromptBuilder.ts @@ -0,0 +1,286 @@ +/** + * Prompt Builder + * 构建和渲染Protocol Agent的Prompt + * + * @module agent/protocol/services/PromptBuilder + */ + +import { ConfigLoader } from '../../services/ConfigLoader.js'; +import { + AgentPrompt, + ProtocolContextData, + PromptRenderContext, +} from '../../types/index.js'; + +export class PromptBuilder { + private configLoader: ConfigLoader; + + constructor(configLoader: ConfigLoader) { + this.configLoader = configLoader; + } + + /** + * 构建完整的消息列表 + */ + async buildMessages( + context: ProtocolContextData, + userMessage: string, + conversationHistory?: Array<{ role: string; content: string }> + ): Promise> { + const messages: Array<{ role: string; content: string }> = []; + + // 1. 系统Prompt + const systemPrompt = await this.buildSystemPrompt(context); + if (systemPrompt) { + messages.push({ role: 'system', content: systemPrompt }); + } + + // 2. 阶段Prompt + const stagePrompt = await this.buildStagePrompt(context); + if (stagePrompt) { + messages.push({ role: 'system', content: stagePrompt }); + } + + // 3. 对话历史(最近5轮) + if (conversationHistory?.length) { + const recentHistory = conversationHistory.slice(-10); // 最近10条消息(5轮) + messages.push(...recentHistory); + } + + // 4. 用户消息 + messages.push({ role: 'user', content: userMessage }); + + return messages; + } + + /** + * 构建系统Prompt + */ + async buildSystemPrompt(context: ProtocolContextData): Promise { + const prompt = await this.configLoader.getSystemPrompt('protocol_agent'); + if (!prompt) return null; + + return this.renderTemplate(prompt.content, { context }); + } + + /** + * 构建当前阶段Prompt + */ + async buildStagePrompt(context: ProtocolContextData): Promise { + const prompt = await this.configLoader.getStagePrompt( + 'protocol_agent', + context.currentStage + ); + if (!prompt) return null; + + return this.renderTemplate(prompt.content, { context }); + } + + /** + * 构建数据提取Prompt + */ + async buildExtractionPrompt( + context: ProtocolContextData, + userMessage: string + ): Promise { + const prompt = await this.configLoader.getExtractionPrompt( + 'protocol_agent', + context.currentStage + ); + if (!prompt) return null; + + return this.renderTemplate(prompt.content, { + userMessage, + context, + currentPico: context.pico, + }); + } + + /** + * 构建方案生成Prompt + */ + async buildGenerationPrompt(context: ProtocolContextData): Promise { + const config = await this.configLoader.loadAgentConfig('protocol_agent'); + const prompt = config.prompts.find(p => p.promptCode === 'generate_protocol'); + + if (!prompt) return null; + + return this.renderTemplate(prompt.content, { + scientificQuestion: context.scientificQuestion, + pico: context.pico, + studyDesign: context.studyDesign, + sampleSize: context.sampleSize, + endpoints: context.endpoints, + }); + } + + /** + * 渲染模板 + * 支持 {{variable}} 和 {{#if variable}}...{{/if}} 语法 + */ + private renderTemplate( + template: string, + variables: Record + ): string { + let result = template; + + // 处理 {{#if variable}}...{{/if}} 条件块 + result = this.processConditionals(result, variables); + + // 处理 {{#each array}}...{{/each}} 循环块 + result = this.processEachBlocks(result, variables); + + // 处理简单变量替换 {{variable}} + result = this.processVariables(result, variables); + + return result; + } + + /** + * 处理条件块 + */ + private processConditionals( + template: string, + variables: Record + ): string { + const ifPattern = /\{\{#if\s+(\S+)\}\}([\s\S]*?)\{\{\/if\}\}/g; + + return template.replace(ifPattern, (match, condition, content) => { + const value = this.getNestedValue(variables, condition); + if (value && value !== false && value !== null && value !== undefined) { + return content; + } + return ''; + }); + } + + /** + * 处理循环块 + */ + private processEachBlocks( + template: string, + variables: Record + ): string { + const eachPattern = /\{\{#each\s+(\S+)\}\}([\s\S]*?)\{\{\/each\}\}/g; + + return template.replace(eachPattern, (match, arrayPath, content) => { + const array = this.getNestedValue(variables, arrayPath); + if (!Array.isArray(array)) return ''; + + return array.map((item, index) => { + let itemContent = content; + + // 替换 {{this}} 为当前项 + itemContent = itemContent.replace(/\{\{this\}\}/g, String(item)); + + // 替换 {{@index}} 为索引 + itemContent = itemContent.replace(/\{\{@index\}\}/g, String(index)); + + // 替换项属性 {{name}}, {{definition}} 等 + if (typeof item === 'object' && item !== null) { + for (const [key, value] of Object.entries(item)) { + const regex = new RegExp(`\\{\\{${key}\\}\\}`, 'g'); + itemContent = itemContent.replace(regex, String(value ?? '')); + } + } + + return itemContent; + }).join('\n'); + }); + } + + /** + * 处理变量替换 + */ + private processVariables( + template: string, + variables: Record + ): string { + const varPattern = /\{\{([^#/][^}]*)\}\}/g; + + return template.replace(varPattern, (match, varPath) => { + const value = this.getNestedValue(variables, varPath.trim()); + + if (value === undefined || value === null) { + return ''; + } + + if (typeof value === 'object') { + return JSON.stringify(value, null, 2); + } + + return String(value); + }); + } + + /** + * 获取嵌套属性值 + */ + private getNestedValue( + obj: Record, + path: string + ): unknown { + const parts = path.split('.'); + let current: unknown = obj; + + for (const part of parts) { + if (current === null || current === undefined) { + return undefined; + } + if (typeof current !== 'object') { + return undefined; + } + current = (current as Record)[part]; + } + + return current; + } + + /** + * 构建欢迎消息 + */ + buildWelcomeMessage(): string { + return `您好!我是研究方案制定助手,将帮助您系统地完成临床研究方案的核心要素设计。 + +我们将一起完成以下5个关键步骤: + +1️⃣ **科学问题梳理** - 明确研究要解决的核心问题 +2️⃣ **PICO要素** - 确定研究人群、干预、对照和结局 +3️⃣ **研究设计** - 选择合适的研究类型和方法 +4️⃣ **样本量计算** - 估算所需的样本量 +5️⃣ **观察指标** - 定义主要和次要结局指标 + +完成这5个要素后,您可以**一键生成完整的研究方案**并下载为Word文档。 + +让我们开始吧!请先告诉我,您想研究什么问题?或者描述一下您的研究背景和想法。`; + } + + /** + * 构建阶段完成消息 + */ + buildStageCompleteMessage( + stageName: string, + nextStageName?: string, + isAllCompleted: boolean = false + ): string { + if (isAllCompleted) { + return `✅ ${stageName}已同步到方案! + +🎉 **恭喜!您已完成所有5个核心要素的梳理!** + +您现在可以: +- 点击「🚀 一键生成研究方案」生成完整方案 +- 或者回顾修改任何阶段的内容 + +需要我帮您生成研究方案吗?`; + } + + return `✅ ${stageName}已同步到方案! + +接下来我们进入**${nextStageName}**阶段。准备好了吗? + +说"继续"我们就开始,或者您也可以先问我任何问题。`; + } +} + + diff --git a/backend/src/modules/agent/protocol/services/ProtocolContextService.ts b/backend/src/modules/agent/protocol/services/ProtocolContextService.ts new file mode 100644 index 00000000..d0d20284 --- /dev/null +++ b/backend/src/modules/agent/protocol/services/ProtocolContextService.ts @@ -0,0 +1,289 @@ +/** + * Protocol Context Service + * 管理研究方案的上下文数据 + * + * @module agent/protocol/services/ProtocolContextService + */ + +import { PrismaClient } from '@prisma/client'; +import { + ProtocolContextData, + ProtocolStageCode, + ScientificQuestionData, + PICOData, + StudyDesignData, + SampleSizeData, + EndpointsData, +} from '../../types/index.js'; + +export class ProtocolContextService { + private prisma: PrismaClient; + + constructor(prisma: PrismaClient) { + this.prisma = prisma; + } + + /** + * 获取或创建上下文 + */ + async getOrCreateContext( + conversationId: string, + userId: string + ): Promise { + let context = await this.getContext(conversationId); + + if (!context) { + context = await this.createContext(conversationId, userId); + } + + return context; + } + + /** + * 获取上下文 + */ + async getContext(conversationId: string): Promise { + const result = await this.prisma.protocolContext.findUnique({ + where: { conversationId }, + }); + + if (!result) return null; + + return this.mapToContextData(result); + } + + /** + * 创建新上下文 + */ + async createContext( + conversationId: string, + userId: string + ): Promise { + const result = await this.prisma.protocolContext.create({ + data: { + conversationId, + userId, + currentStage: 'scientific_question', + status: 'in_progress', + completedStages: [], + }, + }); + + return this.mapToContextData(result); + } + + /** + * 更新阶段数据 + */ + async updateStageData( + conversationId: string, + stageCode: ProtocolStageCode, + data: Record + ): Promise { + const updateData: Record = {}; + + switch (stageCode) { + case 'scientific_question': + updateData.scientificQuestion = data; + break; + case 'pico': + updateData.pico = data; + break; + case 'study_design': + updateData.studyDesign = data; + break; + case 'sample_size': + updateData.sampleSize = data; + break; + case 'endpoints': + updateData.endpoints = data; + break; + } + + const result = await this.prisma.protocolContext.update({ + where: { conversationId }, + data: { + ...updateData, + lastActiveAt: new Date(), + }, + }); + + return this.mapToContextData(result); + } + + /** + * 标记阶段完成并更新当前阶段 + */ + async completeStage( + conversationId: string, + stageCode: ProtocolStageCode, + nextStage?: ProtocolStageCode + ): Promise { + const context = await this.getContext(conversationId); + if (!context) { + throw new Error('Context not found'); + } + + const completedStages = [...context.completedStages]; + if (!completedStages.includes(stageCode)) { + completedStages.push(stageCode); + } + + const result = await this.prisma.protocolContext.update({ + where: { conversationId }, + data: { + completedStages, + currentStage: nextStage ?? context.currentStage, + lastActiveAt: new Date(), + }, + }); + + return this.mapToContextData(result); + } + + /** + * 更新当前阶段 + */ + async updateCurrentStage( + conversationId: string, + stageCode: ProtocolStageCode + ): Promise { + const result = await this.prisma.protocolContext.update({ + where: { conversationId }, + data: { + currentStage: stageCode, + lastActiveAt: new Date(), + }, + }); + + return this.mapToContextData(result); + } + + /** + * 标记方案完成 + */ + async markCompleted(conversationId: string): Promise { + const result = await this.prisma.protocolContext.update({ + where: { conversationId }, + data: { + status: 'completed', + lastActiveAt: new Date(), + }, + }); + + return this.mapToContextData(result); + } + + /** + * 检查是否所有阶段都已完成 + */ + isAllStagesCompleted(context: ProtocolContextData): boolean { + const requiredStages: ProtocolStageCode[] = [ + 'scientific_question', + 'pico', + 'study_design', + 'sample_size', + 'endpoints', + ]; + + return requiredStages.every(stage => context.completedStages.includes(stage)); + } + + /** + * 获取进度百分比 + */ + getProgress(context: ProtocolContextData): number { + const totalStages = 5; + return Math.round((context.completedStages.length / totalStages) * 100); + } + + /** + * 获取阶段状态列表 + */ + getStagesStatus(context: ProtocolContextData): Array<{ + stageCode: ProtocolStageCode; + stageName: string; + status: 'completed' | 'current' | 'pending'; + data: Record | null; + }> { + const stages: Array<{ + code: ProtocolStageCode; + name: string; + dataKey: keyof ProtocolContextData; + }> = [ + { code: 'scientific_question', name: '科学问题梳理', dataKey: 'scientificQuestion' }, + { code: 'pico', name: 'PICO要素', dataKey: 'pico' }, + { code: 'study_design', name: '研究设计', dataKey: 'studyDesign' }, + { code: 'sample_size', name: '样本量计算', dataKey: 'sampleSize' }, + { code: 'endpoints', name: '观察指标', dataKey: 'endpoints' }, + ]; + + return stages.map(stage => ({ + stageCode: stage.code, + stageName: stage.name, + status: context.completedStages.includes(stage.code) + ? 'completed' as const + : context.currentStage === stage.code + ? 'current' as const + : 'pending' as const, + data: context[stage.dataKey] as unknown as Record | null ?? null, + })); + } + + /** + * 获取用于生成方案的完整数据 + */ + getGenerationData(context: ProtocolContextData): { + scientificQuestion: ScientificQuestionData | null; + pico: PICOData | null; + studyDesign: StudyDesignData | null; + sampleSize: SampleSizeData | null; + endpoints: EndpointsData | null; + } { + return { + scientificQuestion: context.scientificQuestion ?? null, + pico: context.pico ?? null, + studyDesign: context.studyDesign ?? null, + sampleSize: context.sampleSize ?? null, + endpoints: context.endpoints ?? null, + }; + } + + /** + * 将数据库结果映射为上下文数据 + */ + private mapToContextData(result: { + id: string; + conversationId: string; + userId: string; + currentStage: string; + status: string; + scientificQuestion: unknown; + pico: unknown; + studyDesign: unknown; + sampleSize: unknown; + endpoints: unknown; + completedStages: string[]; + lastActiveAt: Date; + createdAt: Date; + updatedAt: Date; + }): ProtocolContextData { + return { + id: result.id, + conversationId: result.conversationId, + userId: result.userId, + currentStage: result.currentStage as ProtocolStageCode, + status: result.status as ProtocolContextData['status'], + scientificQuestion: result.scientificQuestion as ScientificQuestionData | undefined, + pico: result.pico as PICOData | undefined, + studyDesign: result.studyDesign as StudyDesignData | undefined, + sampleSize: result.sampleSize as SampleSizeData | undefined, + endpoints: result.endpoints as EndpointsData | undefined, + completedStages: result.completedStages as ProtocolStageCode[], + lastActiveAt: result.lastActiveAt, + createdAt: result.createdAt, + updatedAt: result.updatedAt, + }; + } +} + diff --git a/backend/src/modules/agent/protocol/services/ProtocolOrchestrator.ts b/backend/src/modules/agent/protocol/services/ProtocolOrchestrator.ts new file mode 100644 index 00000000..6b3b0c8c --- /dev/null +++ b/backend/src/modules/agent/protocol/services/ProtocolOrchestrator.ts @@ -0,0 +1,321 @@ +/** + * Protocol Orchestrator + * Protocol Agent的具体实现 + * + * @module agent/protocol/services/ProtocolOrchestrator + */ + +import { PrismaClient } from '@prisma/client'; +import { + BaseAgentOrchestrator, + OrchestratorDependencies, +} from '../../services/BaseAgentOrchestrator.js'; +import { + AgentSession, + AgentResponse, + ProtocolContextData, + ProtocolStageCode, + SyncButtonData, + ActionCard, + UserMessageInput, +} from '../../types/index.js'; +import { ProtocolContextService } from './ProtocolContextService.js'; +import { PromptBuilder } from './PromptBuilder.js'; + +/** 阶段名称映射 */ +const STAGE_NAMES: Record = { + scientific_question: '科学问题梳理', + pico: 'PICO要素', + study_design: '研究设计', + sample_size: '样本量计算', + endpoints: '观察指标', +}; + +/** 阶段顺序 */ +const STAGE_ORDER: ProtocolStageCode[] = [ + 'scientific_question', + 'pico', + 'study_design', + 'sample_size', + 'endpoints', +]; + +export class ProtocolOrchestrator extends BaseAgentOrchestrator { + private contextService: ProtocolContextService; + private promptBuilder: PromptBuilder; + + constructor(deps: OrchestratorDependencies) { + super(deps); + this.contextService = new ProtocolContextService(deps.prisma); + this.promptBuilder = new PromptBuilder(this.configLoader); + } + + /** + * 获取Agent代码 + */ + getAgentCode(): string { + return 'protocol_agent'; + } + + /** + * 覆盖父类handleMessage,确保上下文在处理消息前创建 + */ + async handleMessage(input: UserMessageInput): Promise { + // 确保上下文存在 + await this.contextService.getOrCreateContext(input.conversationId, input.userId); + + // 调用父类方法处理消息 + return super.handleMessage(input); + } + + /** + * 获取上下文数据(如果不存在则返回默认结构) + */ + async getContext(conversationId: string): Promise | null> { + const context = await this.contextService.getContext(conversationId); + if (!context) { + // 返回默认上下文结构,避免 undefined 错误 + return { + currentStage: 'scientific_question', + completedStages: [], + status: 'in_progress', + }; + } + return context as unknown as Record; + } + + /** + * 保存上下文数据 + */ + async saveContext( + conversationId: string, + userId: string, + data: Record + ): Promise { + const context = await this.contextService.getOrCreateContext(conversationId, userId); + const stageCode = context.currentStage; + + await this.contextService.updateStageData(conversationId, stageCode, data); + } + + /** + * 构建阶段响应 + */ + async buildStageResponse( + session: AgentSession, + llmResponse: string, + contextData: Record + ): Promise { + const context = contextData as unknown as ProtocolContextData; + const stageCode = session.currentStage as ProtocolStageCode; + const stageName = STAGE_NAMES[stageCode] || session.currentStage; + + // 检测是否应该显示同步按钮 + const syncButton = this.buildSyncButton(llmResponse, stageCode, context); + + // 构建动作卡片 + const actionCards = this.buildActionCards(stageCode, context); + + return { + content: llmResponse, + stage: stageCode, + stageName, + syncButton, + actionCards, + }; + } + + /** + * 处理Protocol同步请求 + */ + async handleProtocolSync( + conversationId: string, + userId: string, + stageCode: string, + data: Record + ): Promise<{ + success: boolean; + context: ProtocolContextData; + nextStage?: ProtocolStageCode; + message?: string; + }> { + const stage = stageCode as ProtocolStageCode; + + // 保存阶段数据 + await this.contextService.updateStageData(conversationId, stage, { + ...data, + confirmed: true, + confirmedAt: new Date(), + }); + + // 获取下一阶段 + const currentIndex = STAGE_ORDER.indexOf(stage); + const nextStage = currentIndex < STAGE_ORDER.length - 1 + ? STAGE_ORDER[currentIndex + 1] + : undefined; + + // 标记当前阶段完成,更新到下一阶段 + const context = await this.contextService.completeStage( + conversationId, + stage, + nextStage + ); + + // 检查是否所有阶段都已完成 + const allCompleted = this.contextService.isAllStagesCompleted(context); + + return { + success: true, + context, + nextStage, + message: allCompleted + ? '🎉 所有核心要素已完成!您可以点击「一键生成研究方案」生成完整方案。' + : nextStage + ? `已同步${STAGE_NAMES[stage]},进入${STAGE_NAMES[nextStage]}阶段` + : `已同步${STAGE_NAMES[stage]}`, + }; + } + + /** + * 获取Protocol上下文服务 + */ + getContextService(): ProtocolContextService { + return this.contextService; + } + + /** + * 构建同步按钮数据 + */ + private buildSyncButton( + llmResponse: string, + stageCode: ProtocolStageCode, + context: ProtocolContextData + ): SyncButtonData | undefined { + // 检测LLM响应中是否有已整理好的数据 + // 这里用简单的关键词检测,实际可以用更复杂的方式 + const readyPatterns = [ + '整理', + '总结', + '您的科学问题', + '您的PICO', + '您的研究设计', + '样本量', + '观察指标', + '同步到方案', + ]; + + const hasReadyData = readyPatterns.some(p => llmResponse.includes(p)); + + if (!hasReadyData) { + return undefined; + } + + // 确保 completedStages 存在 + const completedStages = context.completedStages || []; + + // 检查当前阶段是否已完成 + if (completedStages.includes(stageCode)) { + return { + stageCode, + extractedData: {}, + label: '已同步', + disabled: true, + }; + } + + return { + stageCode, + extractedData: this.extractDataFromResponse(llmResponse, stageCode), + label: '✅ 同步到方案', + disabled: false, + }; + } + + /** + * 从LLM响应中提取结构化数据 + */ + private extractDataFromResponse( + response: string, + stageCode: ProtocolStageCode + ): Record { + // 尝试从响应中提取JSON格式的数据 + const jsonMatch = response.match(/([\s\S]*?)<\/extracted_data>/); + if (jsonMatch) { + try { + return JSON.parse(jsonMatch[1]); + } catch { + // 解析失败,继续使用默认逻辑 + } + } + + // 简单提取(实际应该用LLM来提取) + switch (stageCode) { + case 'scientific_question': + return { content: response.substring(0, 500), readyToSync: true }; + default: + return { readyToSync: true }; + } + } + + /** + * 构建动作卡片 + */ + private buildActionCards( + stageCode: ProtocolStageCode, + context: ProtocolContextData + ): ActionCard[] { + const cards: ActionCard[] = []; + + // 样本量阶段:添加样本量计算器卡片 + if (stageCode === 'sample_size') { + cards.push({ + id: 'sample_size_calculator', + type: 'tool', + title: '📊 样本量计算器', + description: '使用专业计算器进行样本量估算', + actionUrl: '/tools/sample-size-calculator', + }); + } + + // 所有阶段完成后:添加一键生成按钮(确保context有效) + if (context.completedStages && this.contextService.isAllStagesCompleted(context)) { + cards.push({ + id: 'generate_protocol', + type: 'action', + title: '🚀 一键生成研究方案', + description: '基于5个核心要素生成完整研究方案', + actionUrl: '/api/aia/protocol-agent/generate', + }); + } + + return cards; + } + + /** + * 获取上下文状态摘要(用于前端State Panel) + */ + async getContextSummary(conversationId: string): Promise<{ + currentStage: string; + stageName: string; + progress: number; + stages: Array<{ + stageCode: string; + stageName: string; + status: 'completed' | 'current' | 'pending'; + data: Record | null; + }>; + canGenerate: boolean; + } | null> { + const context = await this.contextService.getContext(conversationId); + if (!context) return null; + + return { + currentStage: context.currentStage, + stageName: STAGE_NAMES[context.currentStage] || context.currentStage, + progress: this.contextService.getProgress(context), + stages: this.contextService.getStagesStatus(context), + canGenerate: this.contextService.isAllStagesCompleted(context), + }; + } +} + diff --git a/backend/src/modules/agent/protocol/services/index.ts b/backend/src/modules/agent/protocol/services/index.ts new file mode 100644 index 00000000..33efc16e --- /dev/null +++ b/backend/src/modules/agent/protocol/services/index.ts @@ -0,0 +1,11 @@ +/** + * Protocol Agent Services Export + * + * @module agent/protocol/services + */ + +export { ProtocolContextService } from './ProtocolContextService.js'; +export { ProtocolOrchestrator } from './ProtocolOrchestrator.js'; +export { PromptBuilder } from './PromptBuilder.js'; +export { LLMServiceAdapter, createLLMServiceAdapter } from './LLMServiceAdapter.js'; + diff --git a/backend/src/modules/agent/services/BaseAgentOrchestrator.ts b/backend/src/modules/agent/services/BaseAgentOrchestrator.ts new file mode 100644 index 00000000..163c48fa --- /dev/null +++ b/backend/src/modules/agent/services/BaseAgentOrchestrator.ts @@ -0,0 +1,561 @@ +/** + * Base Agent Orchestrator + * Agent框架的核心编排器抽象类 + * + * @module agent/services/BaseAgentOrchestrator + */ + +import { PrismaClient } from '@prisma/client'; +import { v4 as uuidv4 } from 'uuid'; +import { + AgentSession, + AgentResponse, + UserMessageInput, + StageTransitionResult, + IntentAnalysis, + AgentFullConfig, + AgentStage, +} from '../types/index.js'; +import { ConfigLoader } from './ConfigLoader.js'; +import { QueryAnalyzer } from './QueryAnalyzer.js'; +import { StageManager } from './StageManager.js'; +import { TraceLogger } from './TraceLogger.js'; + +/** Orchestrator依赖注入 */ +export interface OrchestratorDependencies { + prisma: PrismaClient; + llmService: LLMServiceInterface; +} + +/** LLM服务接口(由具体实现提供) */ +export interface LLMServiceInterface { + chat(params: { + messages: Array<{ role: string; content: string }>; + model?: string; + temperature?: number; + maxTokens?: number; + }): Promise<{ + content: string; + thinkingContent?: string; + tokensUsed: number; + model: string; + }>; +} + +/** + * 抽象基类:Agent编排器 + * 子类需要实现具体Agent的逻辑 + */ +export abstract class BaseAgentOrchestrator { + protected prisma: PrismaClient; + protected llmService: LLMServiceInterface; + protected configLoader: ConfigLoader; + protected queryAnalyzer: QueryAnalyzer; + protected stageManager: StageManager; + protected traceLogger: TraceLogger; + + protected config: AgentFullConfig | null = null; + + constructor(deps: OrchestratorDependencies) { + this.prisma = deps.prisma; + this.llmService = deps.llmService; + this.configLoader = new ConfigLoader(deps.prisma); + this.queryAnalyzer = new QueryAnalyzer(deps.llmService); + this.stageManager = new StageManager(deps.prisma); + this.traceLogger = new TraceLogger(deps.prisma); + } + + /** + * 获取Agent唯一标识(子类必须实现) + */ + abstract getAgentCode(): string; + + /** + * 获取上下文数据(子类必须实现) + */ + abstract getContext(conversationId: string): Promise | null>; + + /** + * 保存上下文数据(子类必须实现) + */ + abstract saveContext( + conversationId: string, + userId: string, + data: Record + ): Promise; + + /** + * 构建阶段响应(子类必须实现) + */ + abstract buildStageResponse( + session: AgentSession, + llmResponse: string, + context: Record + ): Promise; + + /** + * 初始化配置 + */ + protected async ensureConfig(): Promise { + if (!this.config) { + this.config = await this.configLoader.loadAgentConfig(this.getAgentCode()); + } + return this.config; + } + + /** + * 处理用户消息 - 主入口 + */ + async handleMessage(input: UserMessageInput): Promise { + const traceId = uuidv4(); + let stepIndex = 0; + + try { + await this.ensureConfig(); + + // 1. 获取或创建会话 + const session = await this.getOrCreateSession(input.conversationId, input.userId); + + // 2. 记录输入追踪 + await this.traceLogger.log({ + sessionId: session.id, + traceId, + stepIndex: stepIndex++, + stepType: 'query', + input: { content: input.content }, + stageCode: session.currentStage, + }); + + // 3. 意图识别 + const intent = await this.analyzeIntent(input.content, session); + + await this.traceLogger.log({ + sessionId: session.id, + traceId, + stepIndex: stepIndex++, + stepType: 'plan', + input: { userMessage: input.content }, + output: { intent }, + stageCode: session.currentStage, + }); + + // 4. 根据意图处理 + const context = await this.getContext(input.conversationId) || {}; + + // 5. 执行LLM对话 + const llmResponse = await this.executeDialogue(session, input.content, context, intent); + + await this.traceLogger.log({ + sessionId: session.id, + traceId, + stepIndex: stepIndex++, + stepType: 'execute', + input: { intent, context }, + output: { response: llmResponse.content.substring(0, 500) }, + stageCode: session.currentStage, + modelUsed: llmResponse.model, + tokensUsed: llmResponse.tokensUsed, + }); + + // 6. 更新会话统计 + await this.updateSessionStats(session.id, llmResponse.tokensUsed); + + // 7. 构建响应 + const response = await this.buildStageResponse(session, llmResponse.content, context); + response.thinkingContent = llmResponse.thinkingContent; + response.tokensUsed = llmResponse.tokensUsed; + response.modelUsed = llmResponse.model; + + return response; + + } catch (error) { + // 记录错误 + const session = await this.getSession(input.conversationId); + if (session) { + await this.traceLogger.log({ + sessionId: session.id, + traceId, + stepIndex, + stepType: 'execute', + stageCode: session.currentStage, + errorType: 'execution_error', + errorMsg: error instanceof Error ? error.message : String(error), + }); + } + throw error; + } + } + + /** + * 处理同步请求 + */ + async handleSync( + conversationId: string, + userId: string, + stageCode: string, + data: Record + ): Promise { + const traceId = uuidv4(); + + await this.ensureConfig(); + const session = await this.getSession(conversationId); + + if (!session) { + throw new Error('Session not found'); + } + + // 记录同步操作 + await this.traceLogger.log({ + sessionId: session.id, + traceId, + stepIndex: 0, + stepType: 'sync', + input: { stageCode, data }, + stageCode: session.currentStage, + }); + + // 保存上下文数据 + await this.saveContext(conversationId, userId, { + ...data, + confirmed: true, + confirmedAt: new Date(), + }); + + // 执行Reflexion检查(如果启用) + const reflexionResults = await this.runReflexion(session, stageCode, 'on_sync', data); + + // 检查是否有阻塞性错误 + const blockingError = reflexionResults.find(r => !r.passed && r.severity === 'error'); + if (blockingError) { + return { + success: false, + fromStage: session.currentStage, + toStage: session.currentStage, + message: blockingError.message, + reflexionResults, + }; + } + + // 获取下一阶段 + const nextStage = await this.configLoader.getNextStage(this.getAgentCode(), stageCode); + + if (nextStage) { + // 更新会话状态 + await this.updateSessionStage(session.id, nextStage.stageCode); + } + + return { + success: true, + fromStage: session.currentStage, + toStage: nextStage?.stageCode ?? session.currentStage, + message: nextStage ? `进入${nextStage.stageName}阶段` : '所有阶段已完成', + reflexionResults, + }; + } + + /** + * 获取会话 + */ + protected async getSession(conversationId: string): Promise { + const result = await this.prisma.agentSession.findUnique({ + where: { conversationId }, + }); + + if (!result) return null; + + return { + id: result.id, + agentId: result.agentId, + conversationId: result.conversationId, + userId: result.userId, + currentStage: result.currentStage, + status: result.status as AgentSession['status'], + contextRef: result.contextRef ?? undefined, + turnCount: result.turnCount, + totalTokens: result.totalTokens, + createdAt: result.createdAt, + updatedAt: result.updatedAt, + }; + } + + /** + * 获取或创建会话 + */ + protected async getOrCreateSession( + conversationId: string, + userId: string + ): Promise { + let session = await this.getSession(conversationId); + + if (!session) { + const config = await this.ensureConfig(); + const initialStage = config.stages.find((s: AgentStage) => s.isInitial); + + const result = await this.prisma.agentSession.create({ + data: { + agentId: config.definition.id, + conversationId, + userId, + currentStage: initialStage?.stageCode ?? config.stages[0]?.stageCode ?? 'unknown', + status: 'active', + }, + }); + + session = { + id: result.id, + agentId: result.agentId, + conversationId: result.conversationId, + userId: result.userId, + currentStage: result.currentStage, + status: result.status as AgentSession['status'], + contextRef: result.contextRef ?? undefined, + turnCount: result.turnCount, + totalTokens: result.totalTokens, + createdAt: result.createdAt, + updatedAt: result.updatedAt, + }; + } + + return session; + } + + /** + * 分析用户意图 + */ + protected async analyzeIntent( + userMessage: string, + session: AgentSession + ): Promise { + // 简单的意图识别(后续可增强) + const lowerMessage = userMessage.toLowerCase(); + + // 检测阶段切换意图 + if (lowerMessage.includes('继续') || + lowerMessage.includes('下一步') || + lowerMessage.includes('next')) { + return { + intent: 'proceed_next_stage', + confidence: 0.9, + entities: {}, + suggestedAction: 'stage_transition', + }; + } + + // 检测同步意图 + if (lowerMessage.includes('确认') || + lowerMessage.includes('同步') || + lowerMessage.includes('保存')) { + return { + intent: 'confirm_sync', + confidence: 0.85, + entities: {}, + suggestedAction: 'sync_data', + }; + } + + // 默认为对话意图 + return { + intent: 'dialogue', + confidence: 0.7, + entities: {}, + suggestedAction: 'continue_dialogue', + }; + } + + /** + * 执行对话 + */ + protected async executeDialogue( + session: AgentSession, + userMessage: string, + context: Record, + intent: IntentAnalysis + ): Promise<{ + content: string; + thinkingContent?: string; + tokensUsed: number; + model: string; + }> { + const config = await this.ensureConfig(); + + // 获取系统Prompt + const systemPrompt = await this.configLoader.getSystemPrompt(this.getAgentCode()); + + // 获取当前阶段Prompt + const stagePrompt = await this.configLoader.getStagePrompt( + this.getAgentCode(), + session.currentStage + ); + + // 构建消息 + const messages: Array<{ role: string; content: string }> = []; + + if (systemPrompt) { + messages.push({ + role: 'system', + content: this.renderPrompt(systemPrompt.content, { context, intent }), + }); + } + + if (stagePrompt) { + messages.push({ + role: 'system', + content: this.renderPrompt(stagePrompt.content, { context, intent }), + }); + } + + messages.push({ + role: 'user', + content: userMessage, + }); + + // 调用LLM + return await this.llmService.chat({ + messages, + model: config.definition.config?.defaultModel, + temperature: 0.7, + }); + } + + /** + * 渲染Prompt模板 + */ + protected renderPrompt( + template: string, + variables: Record + ): string { + let result = template; + + // 简单的变量替换 {{variable}} + for (const [key, value] of Object.entries(variables)) { + const regex = new RegExp(`\\{\\{${key}\\}\\}`, 'g'); + result = result.replace(regex, JSON.stringify(value, null, 2)); + } + + return result; + } + + /** + * 运行Reflexion检查 + */ + protected async runReflexion( + session: AgentSession, + stageCode: string, + timing: 'on_sync' | 'on_stage_complete' | 'on_generate', + data: Record + ): Promise> { + const rules = await this.configLoader.getReflexionRules( + this.getAgentCode(), + stageCode, + timing + ); + + const results = []; + + for (const rule of rules) { + try { + let passed = true; + let message: string | undefined; + + if (rule.ruleType === 'rule_based' && rule.conditions) { + // 基于规则的检查 + passed = this.evaluateRuleConditions(rule.conditions, data); + if (!passed) { + message = `${rule.ruleName}检查未通过`; + } + } else if (rule.ruleType === 'prompt_based' && rule.promptTemplate) { + // 基于Prompt的检查(简化版,实际应调用LLM) + // TODO: 实现LLM-based检查 + passed = true; + } + + results.push({ + ruleCode: rule.ruleCode, + ruleName: rule.ruleName, + passed, + severity: rule.severity, + message, + }); + } catch (error) { + results.push({ + ruleCode: rule.ruleCode, + ruleName: rule.ruleName, + passed: false, + severity: 'warning' as const, + message: `规则执行出错: ${error instanceof Error ? error.message : String(error)}`, + }); + } + } + + return results; + } + + /** + * 评估规则条件 + */ + protected evaluateRuleConditions( + conditions: Record, + data: Record + ): boolean { + // 简单的规则评估逻辑 + for (const [field, requirement] of Object.entries(conditions)) { + const value = data[field]; + + if (requirement === 'required' && !value) { + return false; + } + + if (typeof requirement === 'object' && requirement !== null) { + const req = requirement as Record; + + if (req.minLength && typeof value === 'string' && value.length < (req.minLength as number)) { + return false; + } + + if (req.notEmpty && (!value || (Array.isArray(value) && value.length === 0))) { + return false; + } + } + } + + return true; + } + + /** + * 更新会话统计 + */ + protected async updateSessionStats(sessionId: string, tokensUsed: number): Promise { + await this.prisma.agentSession.update({ + where: { id: sessionId }, + data: { + turnCount: { increment: 1 }, + totalTokens: { increment: tokensUsed }, + }, + }); + } + + /** + * 更新会话阶段 + */ + protected async updateSessionStage(sessionId: string, stageCode: string): Promise { + await this.prisma.agentSession.update({ + where: { id: sessionId }, + data: { currentStage: stageCode }, + }); + } + + /** + * 获取当前阶段信息 + */ + protected async getCurrentStageInfo(stageCode: string): Promise { + const config = await this.ensureConfig(); + return config.stages.find((s: AgentStage) => s.stageCode === stageCode) ?? null; + } +} + diff --git a/backend/src/modules/agent/services/ConfigLoader.ts b/backend/src/modules/agent/services/ConfigLoader.ts new file mode 100644 index 00000000..24f89e63 --- /dev/null +++ b/backend/src/modules/agent/services/ConfigLoader.ts @@ -0,0 +1,266 @@ +/** + * Agent Configuration Loader + * 从数据库加载Agent配置 + * + * @module agent/services/ConfigLoader + */ + +import { PrismaClient } from '@prisma/client'; +import { + AgentDefinition, + AgentStage, + AgentPrompt, + ReflexionRule, + AgentFullConfig, + AgentConfig, +} from '../types/index.js'; + +export class ConfigLoader { + private prisma: PrismaClient; + + // 配置缓存 + private configCache: Map = new Map(); + private cacheTTL: number = 5 * 60 * 1000; // 5分钟缓存 + + constructor(prisma: PrismaClient) { + this.prisma = prisma; + } + + /** + * 加载Agent完整配置 + * @param agentCode Agent代码,如 'protocol_agent' + * @param useCache 是否使用缓存 + */ + async loadAgentConfig(agentCode: string, useCache: boolean = true): Promise { + // 检查缓存 + if (useCache) { + const cached = this.configCache.get(agentCode); + if (cached && Date.now() - cached.loadedAt.getTime() < this.cacheTTL) { + return cached.config; + } + } + + // 从数据库加载 + const definition = await this.loadDefinition(agentCode); + if (!definition) { + throw new Error(`Agent not found: ${agentCode}`); + } + + const [stages, prompts, reflexionRules] = await Promise.all([ + this.loadStages(definition.id), + this.loadPrompts(definition.id), + this.loadReflexionRules(definition.id), + ]); + + const config: AgentFullConfig = { + definition, + stages, + prompts, + reflexionRules, + }; + + // 更新缓存 + this.configCache.set(agentCode, { config, loadedAt: new Date() }); + + return config; + } + + /** + * 加载Agent定义 + */ + private async loadDefinition(agentCode: string): Promise { + const result = await this.prisma.agentDefinition.findUnique({ + where: { code: agentCode }, + }); + + if (!result) return null; + + return { + id: result.id, + code: result.code, + name: result.name, + description: result.description ?? undefined, + version: result.version, + config: result.config as unknown as AgentConfig | undefined, + isActive: result.isActive, + createdAt: result.createdAt, + updatedAt: result.updatedAt, + }; + } + + /** + * 加载Agent阶段配置 + */ + private async loadStages(agentId: string): Promise { + const results = await this.prisma.agentStage.findMany({ + where: { agentId }, + orderBy: { sortOrder: 'asc' }, + }); + + return results.map((r) => ({ + id: r.id, + agentId: r.agentId, + stageCode: r.stageCode, + stageName: r.stageName, + sortOrder: r.sortOrder, + config: r.config as Record | undefined, + nextStages: r.nextStages, + isInitial: r.isInitial, + isFinal: r.isFinal, + })); + } + + /** + * 加载Agent Prompt模板 + */ + private async loadPrompts(agentId: string): Promise { + const results = await this.prisma.agentPrompt.findMany({ + where: { + agentId, + isActive: true, + }, + }); + + return results.map((r) => ({ + id: r.id, + agentId: r.agentId, + stageId: r.stageId ?? undefined, + promptType: r.promptType as AgentPrompt['promptType'], + promptCode: r.promptCode, + content: r.content, + variables: r.variables, + version: r.version, + isActive: r.isActive, + })); + } + + /** + * 加载Reflexion规则 + */ + private async loadReflexionRules(agentId: string): Promise { + const results = await this.prisma.reflexionRule.findMany({ + where: { + agentId, + isActive: true, + }, + orderBy: { sortOrder: 'asc' }, + }); + + return results.map((r) => ({ + id: r.id, + agentId: r.agentId, + ruleCode: r.ruleCode, + ruleName: r.ruleName, + triggerStage: r.triggerStage ?? undefined, + triggerTiming: r.triggerTiming as ReflexionRule['triggerTiming'], + ruleType: r.ruleType as ReflexionRule['ruleType'], + conditions: r.conditions as Record | undefined, + promptTemplate: r.promptTemplate ?? undefined, + severity: r.severity as ReflexionRule['severity'], + failureAction: r.failureAction as ReflexionRule['failureAction'], + isActive: r.isActive, + sortOrder: r.sortOrder, + })); + } + + /** + * 获取指定阶段的Prompt + */ + async getStagePrompt(agentCode: string, stageCode: string): Promise { + const config = await this.loadAgentConfig(agentCode); + + // 先找阶段特定的Prompt + const stage = config.stages.find((s: AgentStage) => s.stageCode === stageCode); + if (stage) { + const stagePrompt = config.prompts.find( + (p: AgentPrompt) => p.stageId === stage.id && p.promptType === 'stage' + ); + if (stagePrompt) return stagePrompt; + } + + return null; + } + + /** + * 获取系统Prompt + */ + async getSystemPrompt(agentCode: string): Promise { + const config = await this.loadAgentConfig(agentCode); + return config.prompts.find(p => p.promptType === 'system' && !p.stageId) ?? null; + } + + /** + * 获取提取Prompt + */ + async getExtractionPrompt(agentCode: string, stageCode: string): Promise { + const config = await this.loadAgentConfig(agentCode); + const stage = config.stages.find(s => s.stageCode === stageCode); + + if (stage) { + return config.prompts.find( + p => p.stageId === stage.id && p.promptType === 'extraction' + ) ?? null; + } + + return null; + } + + /** + * 获取阶段的Reflexion规则 + */ + async getReflexionRules( + agentCode: string, + stageCode: string, + timing: ReflexionRule['triggerTiming'] + ): Promise { + const config = await this.loadAgentConfig(agentCode); + + return config.reflexionRules.filter( + r => (r.triggerStage === stageCode || r.triggerStage === null) && + r.triggerTiming === timing + ); + } + + /** + * 清除缓存 + */ + clearCache(agentCode?: string): void { + if (agentCode) { + this.configCache.delete(agentCode); + } else { + this.configCache.clear(); + } + } + + /** + * 获取阶段流程顺序 + */ + async getStageFlow(agentCode: string): Promise { + const config = await this.loadAgentConfig(agentCode); + return config.stages.sort((a, b) => a.sortOrder - b.sortOrder); + } + + /** + * 获取下一个阶段 + */ + async getNextStage(agentCode: string, currentStageCode: string): Promise { + const stages = await this.getStageFlow(agentCode); + const currentIndex = stages.findIndex(s => s.stageCode === currentStageCode); + + if (currentIndex >= 0 && currentIndex < stages.length - 1) { + return stages[currentIndex + 1]; + } + + return null; + } + + /** + * 检查是否为最终阶段 + */ + async isFinalStage(agentCode: string, stageCode: string): Promise { + const config = await this.loadAgentConfig(agentCode); + const stage = config.stages.find(s => s.stageCode === stageCode); + return stage?.isFinal ?? false; + } +} + diff --git a/backend/src/modules/agent/services/QueryAnalyzer.ts b/backend/src/modules/agent/services/QueryAnalyzer.ts new file mode 100644 index 00000000..58cecef7 --- /dev/null +++ b/backend/src/modules/agent/services/QueryAnalyzer.ts @@ -0,0 +1,270 @@ +/** + * Query Analyzer + * 用户意图识别和查询分析 + * + * @module agent/services/QueryAnalyzer + */ + +import { IntentAnalysis } from '../types/index.js'; +import { LLMServiceInterface } from './BaseAgentOrchestrator.js'; + +/** 意图类型定义 */ +export type IntentType = + | 'dialogue' // 普通对话 + | 'proceed_next_stage' // 进入下一阶段 + | 'confirm_sync' // 确认同步 + | 'edit_data' // 编辑已有数据 + | 'ask_question' // 提问 + | 'clarification' // 澄清/补充信息 + | 'generate_protocol' // 生成研究方案 + | 'export_document'; // 导出文档 + +/** 意图识别规则 */ +interface IntentRule { + type: IntentType; + keywords: string[]; + patterns?: RegExp[]; + confidence: number; +} + +export class QueryAnalyzer { + private llmService: LLMServiceInterface; + + // 预定义的意图规则 + private intentRules: IntentRule[] = [ + { + type: 'proceed_next_stage', + keywords: ['继续', '下一步', '下一个', 'next', '进入下一', '开始下一'], + confidence: 0.9, + }, + { + type: 'confirm_sync', + keywords: ['确认', '同步', '保存', '确定', 'confirm', 'save'], + confidence: 0.85, + }, + { + type: 'edit_data', + keywords: ['修改', '编辑', '更改', '调整', '更新', 'edit', 'modify'], + confidence: 0.8, + }, + { + type: 'generate_protocol', + keywords: ['生成方案', '生成研究方案', '一键生成', '生成全文'], + confidence: 0.95, + }, + { + type: 'export_document', + keywords: ['导出', '下载', '保存word', '保存文档', 'export', 'download'], + confidence: 0.9, + }, + { + type: 'ask_question', + keywords: ['什么是', '如何', '怎么', '为什么', '是否', '?', '?'], + patterns: [/^(什么|如何|怎么|为什么|是否|能不能|可以|请问)/], + confidence: 0.7, + }, + { + type: 'clarification', + keywords: ['补充', '另外', '还有', '再说', '顺便'], + confidence: 0.6, + }, + ]; + + constructor(llmService: LLMServiceInterface) { + this.llmService = llmService; + } + + /** + * 分析用户输入的意图 + */ + async analyze( + userMessage: string, + context?: { + currentStage?: string; + completedStages?: string[]; + conversationHistory?: Array<{ role: string; content: string }>; + } + ): Promise { + // 1. 基于规则的快速识别 + const ruleBasedResult = this.ruleBasedAnalysis(userMessage); + + if (ruleBasedResult.confidence >= 0.85) { + return ruleBasedResult; + } + + // 2. 上下文增强 + const contextEnhanced = this.contextEnhancedAnalysis( + userMessage, + ruleBasedResult, + context + ); + + // 3. 如果信心度仍然较低,可以考虑使用LLM(此处简化) + // TODO: 实现LLM-based意图识别 + + return contextEnhanced; + } + + /** + * 基于规则的意图分析 + */ + private ruleBasedAnalysis(userMessage: string): IntentAnalysis { + const lowerMessage = userMessage.toLowerCase(); + const entities: Record = {}; + + // 遍历规则匹配 + for (const rule of this.intentRules) { + // 关键词匹配 + const keywordMatch = rule.keywords.some(keyword => + lowerMessage.includes(keyword.toLowerCase()) + ); + + // 正则匹配 + const patternMatch = rule.patterns?.some(pattern => + pattern.test(userMessage) + ); + + if (keywordMatch || patternMatch) { + // 提取实体 + this.extractEntities(userMessage, rule.type, entities); + + return { + intent: rule.type, + confidence: keywordMatch && patternMatch ? + Math.min(rule.confidence + 0.1, 1) : rule.confidence, + entities, + suggestedAction: this.getSuggestedAction(rule.type), + }; + } + } + + // 默认为对话意图 + return { + intent: 'dialogue', + confidence: 0.5, + entities, + suggestedAction: 'continue_dialogue', + }; + } + + /** + * 上下文增强分析 + */ + private contextEnhancedAnalysis( + userMessage: string, + baseResult: IntentAnalysis, + context?: { + currentStage?: string; + completedStages?: string[]; + conversationHistory?: Array<{ role: string; content: string }>; + } + ): IntentAnalysis { + if (!context) return baseResult; + + const result = { ...baseResult }; + + // 根据当前阶段调整意图 + if (context.currentStage) { + // 如果已完成所有阶段,倾向于生成方案 + if (context.completedStages?.length === 5) { + if (userMessage.includes('方案') || userMessage.includes('生成')) { + result.intent = 'generate_protocol'; + result.confidence = Math.max(result.confidence, 0.8); + result.suggestedAction = 'generate_protocol'; + } + } + } + + // 根据对话历史调整 + if (context.conversationHistory?.length) { + const lastAssistantMsg = context.conversationHistory + .filter(m => m.role === 'assistant') + .pop(); + + // 如果上一条AI消息询问是否继续 + if (lastAssistantMsg?.content.includes('继续') || + lastAssistantMsg?.content.includes('下一阶段')) { + if (userMessage.match(/^(好|是|对|可以|行|ok|yes)/i)) { + result.intent = 'proceed_next_stage'; + result.confidence = Math.max(result.confidence, 0.85); + result.suggestedAction = 'stage_transition'; + } + } + } + + return result; + } + + /** + * 提取实体 + */ + private extractEntities( + message: string, + intentType: IntentType, + entities: Record + ): void { + // 阶段名称实体 + const stagePatterns = [ + { pattern: /科学问题/, entity: 'scientific_question' }, + { pattern: /PICO|pico/, entity: 'pico' }, + { pattern: /研究设计/, entity: 'study_design' }, + { pattern: /样本量/, entity: 'sample_size' }, + { pattern: /(终点|结局|观察)指标/, entity: 'endpoints' }, + ]; + + for (const { pattern, entity } of stagePatterns) { + if (pattern.test(message)) { + entities['targetStage'] = entity; + break; + } + } + + // 数字实体 + const numberMatch = message.match(/(\d+)/g); + if (numberMatch) { + entities['numbers'] = numberMatch.map(n => parseInt(n, 10)); + } + } + + /** + * 获取建议的动作 + */ + private getSuggestedAction(intentType: IntentType): string { + const actionMap: Record = { + 'dialogue': 'continue_dialogue', + 'proceed_next_stage': 'stage_transition', + 'confirm_sync': 'sync_data', + 'edit_data': 'edit_context', + 'ask_question': 'answer_question', + 'clarification': 'request_clarification', + 'generate_protocol': 'generate_protocol', + 'export_document': 'export_document', + }; + + return actionMap[intentType] ?? 'continue_dialogue'; + } + + /** + * 检测用户是否表达了同意/肯定 + */ + isAffirmative(message: string): boolean { + const affirmativePatterns = [ + /^(好|是|对|可以|行|ok|yes|yeah|sure|当然|没问题|同意|确认)/i, + /^(嗯|恩|en|um)/i, + ]; + + return affirmativePatterns.some(p => p.test(message.trim())); + } + + /** + * 检测用户是否表达了否定 + */ + isNegative(message: string): boolean { + const negativePatterns = [ + /^(不|否|no|nope|算了|取消|等等|暂时不|还没)/i, + ]; + + return negativePatterns.some(p => p.test(message.trim())); + } +} + diff --git a/backend/src/modules/agent/services/StageManager.ts b/backend/src/modules/agent/services/StageManager.ts new file mode 100644 index 00000000..145e2f2d --- /dev/null +++ b/backend/src/modules/agent/services/StageManager.ts @@ -0,0 +1,283 @@ +/** + * Stage Manager + * Agent阶段状态管理 + * + * @module agent/services/StageManager + */ + +import { PrismaClient } from '@prisma/client'; +import { + AgentSession, + AgentStage, + StageTransitionResult, + ProtocolStageCode, +} from '../types/index.js'; + +/** 阶段转换条件 */ +export interface TransitionCondition { + fromStage: string; + toStage: string; + conditions: Record; +} + +export class StageManager { + private prisma: PrismaClient; + + constructor(prisma: PrismaClient) { + this.prisma = prisma; + } + + /** + * 执行阶段转换 + */ + async transition( + session: AgentSession, + targetStage: string, + stages: AgentStage[] + ): Promise { + const currentStageConfig = stages.find(s => s.stageCode === session.currentStage); + const targetStageConfig = stages.find(s => s.stageCode === targetStage); + + if (!targetStageConfig) { + return { + success: false, + fromStage: session.currentStage, + toStage: targetStage, + message: `目标阶段 ${targetStage} 不存在`, + }; + } + + // 检查是否允许转换 + if (currentStageConfig && !currentStageConfig.nextStages.includes(targetStage)) { + // 检查是否是返回之前的阶段(允许回退) + const targetOrder = targetStageConfig.sortOrder; + const currentOrder = currentStageConfig.sortOrder; + + if (targetOrder > currentOrder) { + return { + success: false, + fromStage: session.currentStage, + toStage: targetStage, + message: `不能从 ${currentStageConfig.stageName} 直接跳转到 ${targetStageConfig.stageName}`, + }; + } + } + + // 更新会话 + await this.prisma.agentSession.update({ + where: { id: session.id }, + data: { currentStage: targetStage }, + }); + + return { + success: true, + fromStage: session.currentStage, + toStage: targetStage, + message: `已进入${targetStageConfig.stageName}阶段`, + }; + } + + /** + * 获取下一个阶段 + */ + getNextStage( + currentStageCode: string, + stages: AgentStage[] + ): AgentStage | null { + const sortedStages = [...stages].sort((a, b) => a.sortOrder - b.sortOrder); + const currentIndex = sortedStages.findIndex(s => s.stageCode === currentStageCode); + + if (currentIndex >= 0 && currentIndex < sortedStages.length - 1) { + return sortedStages[currentIndex + 1]; + } + + return null; + } + + /** + * 获取上一个阶段 + */ + getPreviousStage( + currentStageCode: string, + stages: AgentStage[] + ): AgentStage | null { + const sortedStages = [...stages].sort((a, b) => a.sortOrder - b.sortOrder); + const currentIndex = sortedStages.findIndex(s => s.stageCode === currentStageCode); + + if (currentIndex > 0) { + return sortedStages[currentIndex - 1]; + } + + return null; + } + + /** + * 检查阶段是否完成 + */ + isStageCompleted( + stageCode: ProtocolStageCode, + completedStages: ProtocolStageCode[] + ): boolean { + return completedStages.includes(stageCode); + } + + /** + * 检查是否所有阶段都已完成 + */ + areAllStagesCompleted( + stages: AgentStage[], + completedStages: string[] + ): boolean { + const requiredStages = stages + .filter(s => !s.isFinal) + .map(s => s.stageCode); + + return requiredStages.every(code => completedStages.includes(code)); + } + + /** + * 获取当前进度百分比 + */ + getProgressPercentage( + stages: AgentStage[], + completedStages: string[] + ): number { + const totalStages = stages.filter(s => !s.isFinal).length; + const completed = completedStages.length; + + return totalStages > 0 ? Math.round((completed / totalStages) * 100) : 0; + } + + /** + * 获取阶段状态摘要 + */ + getStagesSummary( + stages: AgentStage[], + currentStageCode: string, + completedStages: string[] + ): Array<{ + stageCode: string; + stageName: string; + status: 'completed' | 'current' | 'pending'; + order: number; + }> { + return stages + .sort((a, b) => a.sortOrder - b.sortOrder) + .map(stage => ({ + stageCode: stage.stageCode, + stageName: stage.stageName, + status: completedStages.includes(stage.stageCode) + ? 'completed' as const + : stage.stageCode === currentStageCode + ? 'current' as const + : 'pending' as const, + order: stage.sortOrder, + })); + } + + /** + * 验证阶段转换是否有效 + */ + validateTransition( + fromStage: string, + toStage: string, + stages: AgentStage[], + completedStages: string[] + ): { valid: boolean; reason?: string } { + const fromStageConfig = stages.find(s => s.stageCode === fromStage); + const toStageConfig = stages.find(s => s.stageCode === toStage); + + if (!fromStageConfig || !toStageConfig) { + return { valid: false, reason: '阶段配置不存在' }; + } + + // 允许回退到已完成的阶段 + if (completedStages.includes(toStage)) { + return { valid: true }; + } + + // 前向转换:检查当前阶段是否已完成 + if (toStageConfig.sortOrder > fromStageConfig.sortOrder) { + if (!completedStages.includes(fromStage)) { + return { + valid: false, + reason: `请先完成${fromStageConfig.stageName}阶段` + }; + } + } + + // 检查是否是允许的转换 + if (!fromStageConfig.nextStages.includes(toStage)) { + // 如果是回退,允许 + if (toStageConfig.sortOrder < fromStageConfig.sortOrder) { + return { valid: true }; + } + return { + valid: false, + reason: `不能从${fromStageConfig.stageName}转换到${toStageConfig.stageName}` + }; + } + + return { valid: true }; + } + + /** + * 获取可用的下一步操作 + */ + getAvailableActions( + currentStageCode: string, + stages: AgentStage[], + completedStages: string[] + ): Array<{ + action: string; + label: string; + targetStage?: string; + enabled: boolean; + }> { + const currentStage = stages.find(s => s.stageCode === currentStageCode); + const actions = []; + + // 同步当前阶段数据 + if (currentStage && !completedStages.includes(currentStageCode)) { + actions.push({ + action: 'sync', + label: '同步到方案', + enabled: true, + }); + } + + // 进入下一阶段 + const nextStage = this.getNextStage(currentStageCode, stages); + if (nextStage) { + actions.push({ + action: 'next_stage', + label: `进入${nextStage.stageName}`, + targetStage: nextStage.stageCode, + enabled: completedStages.includes(currentStageCode), + }); + } + + // 生成研究方案(仅当所有阶段完成时) + if (this.areAllStagesCompleted(stages, completedStages)) { + actions.push({ + action: 'generate_protocol', + label: '一键生成研究方案', + enabled: true, + }); + } + + // 返回上一阶段 + const prevStage = this.getPreviousStage(currentStageCode, stages); + if (prevStage) { + actions.push({ + action: 'prev_stage', + label: `返回${prevStage.stageName}`, + targetStage: prevStage.stageCode, + enabled: true, + }); + } + + return actions; + } +} + diff --git a/backend/src/modules/agent/services/TraceLogger.ts b/backend/src/modules/agent/services/TraceLogger.ts new file mode 100644 index 00000000..4f2441a2 --- /dev/null +++ b/backend/src/modules/agent/services/TraceLogger.ts @@ -0,0 +1,349 @@ +/** + * Trace Logger + * Agent执行追踪日志记录 + * + * @module agent/services/TraceLogger + */ + +import { PrismaClient } from '@prisma/client'; +import { AgentTraceRecord, StepType } from '../types/index.js'; + +/** 追踪日志输入 */ +export interface TraceLogInput { + sessionId: string; + traceId: string; + stepIndex: number; + stepType: StepType; + input?: Record; + output?: Record; + stageCode?: string; + modelUsed?: string; + tokensUsed?: number; + durationMs?: number; + errorType?: string; + errorMsg?: string; +} + +export class TraceLogger { + private prisma: PrismaClient; + private enabled: boolean = true; + + constructor(prisma: PrismaClient) { + this.prisma = prisma; + } + + /** + * 启用/禁用追踪 + */ + setEnabled(enabled: boolean): void { + this.enabled = enabled; + } + + /** + * 记录追踪日志 + */ + async log(input: TraceLogInput): Promise { + if (!this.enabled) { + return null; + } + + try { + const result = await this.prisma.agentTrace.create({ + data: { + sessionId: input.sessionId, + traceId: input.traceId, + stepIndex: input.stepIndex, + stepType: input.stepType, + input: input.input ? JSON.parse(JSON.stringify(input.input)) : undefined, + output: input.output ? JSON.parse(JSON.stringify(input.output)) : undefined, + stageCode: input.stageCode ?? undefined, + modelUsed: input.modelUsed ?? undefined, + tokensUsed: input.tokensUsed ?? undefined, + durationMs: input.durationMs ?? undefined, + errorType: input.errorType ?? undefined, + errorMsg: input.errorMsg ?? undefined, + }, + }); + + return { + id: result.id, + sessionId: result.sessionId, + traceId: result.traceId, + stepIndex: result.stepIndex, + stepType: result.stepType as StepType, + input: result.input as Record | undefined, + output: result.output as Record | undefined, + stageCode: result.stageCode ?? undefined, + modelUsed: result.modelUsed ?? undefined, + tokensUsed: result.tokensUsed ?? undefined, + durationMs: result.durationMs ?? undefined, + errorType: result.errorType ?? undefined, + errorMsg: result.errorMsg ?? undefined, + createdAt: result.createdAt, + }; + } catch (error) { + console.error('[TraceLogger] Failed to log trace:', error); + return null; + } + } + + /** + * 批量记录追踪日志 + */ + async logBatch(inputs: TraceLogInput[]): Promise { + if (!this.enabled || inputs.length === 0) { + return 0; + } + + try { + const result = await this.prisma.agentTrace.createMany({ + data: inputs.map(input => ({ + sessionId: input.sessionId, + traceId: input.traceId, + stepIndex: input.stepIndex, + stepType: input.stepType, + input: input.input ? JSON.parse(JSON.stringify(input.input)) : undefined, + output: input.output ? JSON.parse(JSON.stringify(input.output)) : undefined, + stageCode: input.stageCode ?? undefined, + modelUsed: input.modelUsed ?? undefined, + tokensUsed: input.tokensUsed ?? undefined, + durationMs: input.durationMs ?? undefined, + errorType: input.errorType ?? undefined, + errorMsg: input.errorMsg ?? undefined, + })), + }); + + return result.count; + } catch (error) { + console.error('[TraceLogger] Failed to batch log traces:', error); + return 0; + } + } + + /** + * 获取会话的所有追踪记录 + */ + async getSessionTraces(sessionId: string): Promise { + const results = await this.prisma.agentTrace.findMany({ + where: { sessionId }, + orderBy: [ + { traceId: 'asc' }, + { stepIndex: 'asc' }, + ], + }); + + return results.map(r => ({ + id: r.id, + sessionId: r.sessionId, + traceId: r.traceId, + stepIndex: r.stepIndex, + stepType: r.stepType as StepType, + input: r.input as Record | undefined, + output: r.output as Record | undefined, + stageCode: r.stageCode ?? undefined, + modelUsed: r.modelUsed ?? undefined, + tokensUsed: r.tokensUsed ?? undefined, + durationMs: r.durationMs ?? undefined, + errorType: r.errorType ?? undefined, + errorMsg: r.errorMsg ?? undefined, + createdAt: r.createdAt, + })); + } + + /** + * 获取单个请求的追踪记录 + */ + async getTraceById(traceId: string): Promise { + const results = await this.prisma.agentTrace.findMany({ + where: { traceId }, + orderBy: { stepIndex: 'asc' }, + }); + + return results.map(r => ({ + id: r.id, + sessionId: r.sessionId, + traceId: r.traceId, + stepIndex: r.stepIndex, + stepType: r.stepType as StepType, + input: r.input as Record | undefined, + output: r.output as Record | undefined, + stageCode: r.stageCode ?? undefined, + modelUsed: r.modelUsed ?? undefined, + tokensUsed: r.tokensUsed ?? undefined, + durationMs: r.durationMs ?? undefined, + errorType: r.errorType ?? undefined, + errorMsg: r.errorMsg ?? undefined, + createdAt: r.createdAt, + })); + } + + /** + * 获取会话的错误追踪 + */ + async getSessionErrors(sessionId: string): Promise { + const results = await this.prisma.agentTrace.findMany({ + where: { + sessionId, + errorType: { not: null }, + }, + orderBy: { createdAt: 'desc' }, + }); + + return results.map(r => ({ + id: r.id, + sessionId: r.sessionId, + traceId: r.traceId, + stepIndex: r.stepIndex, + stepType: r.stepType as StepType, + input: r.input as Record | undefined, + output: r.output as Record | undefined, + stageCode: r.stageCode ?? undefined, + modelUsed: r.modelUsed ?? undefined, + tokensUsed: r.tokensUsed ?? undefined, + durationMs: r.durationMs ?? undefined, + errorType: r.errorType ?? undefined, + errorMsg: r.errorMsg ?? undefined, + createdAt: r.createdAt, + })); + } + + /** + * 计算会话的Token统计 + */ + async getSessionTokenStats(sessionId: string): Promise<{ + totalTokens: number; + byModel: Record; + byStepType: Record; + }> { + const traces = await this.prisma.agentTrace.findMany({ + where: { sessionId }, + select: { + tokensUsed: true, + modelUsed: true, + stepType: true, + }, + }); + + const stats = { + totalTokens: 0, + byModel: {} as Record, + byStepType: {} as Record, + }; + + for (const trace of traces) { + const tokens = trace.tokensUsed ?? 0; + stats.totalTokens += tokens; + + if (trace.modelUsed) { + stats.byModel[trace.modelUsed] = (stats.byModel[trace.modelUsed] ?? 0) + tokens; + } + + stats.byStepType[trace.stepType] = (stats.byStepType[trace.stepType] ?? 0) + tokens; + } + + return stats; + } + + /** + * 计算平均响应时间 + */ + async getAverageResponseTime(sessionId: string): Promise { + const result = await this.prisma.agentTrace.aggregate({ + where: { + sessionId, + durationMs: { not: null }, + }, + _avg: { + durationMs: true, + }, + }); + + return result._avg.durationMs ?? 0; + } + + /** + * 删除旧的追踪记录(清理) + */ + async cleanupOldTraces(daysToKeep: number = 30): Promise { + const cutoffDate = new Date(); + cutoffDate.setDate(cutoffDate.getDate() - daysToKeep); + + const result = await this.prisma.agentTrace.deleteMany({ + where: { + createdAt: { lt: cutoffDate }, + }, + }); + + return result.count; + } + + /** + * 生成追踪报告 + */ + async generateTraceReport(sessionId: string): Promise<{ + sessionId: string; + totalSteps: number; + totalTokens: number; + totalDuration: number; + stageBreakdown: Array<{ + stage: string; + steps: number; + tokens: number; + duration: number; + }>; + errors: Array<{ + traceId: string; + stepType: string; + errorType: string; + errorMsg: string; + }>; + }> { + const traces = await this.getSessionTraces(sessionId); + + const stageMap = new Map(); + let totalTokens = 0; + let totalDuration = 0; + const errors: Array<{ + traceId: string; + stepType: string; + errorType: string; + errorMsg: string; + }> = []; + + for (const trace of traces) { + totalTokens += trace.tokensUsed ?? 0; + totalDuration += trace.durationMs ?? 0; + + if (trace.stageCode) { + const existing = stageMap.get(trace.stageCode) ?? { steps: 0, tokens: 0, duration: 0 }; + stageMap.set(trace.stageCode, { + steps: existing.steps + 1, + tokens: existing.tokens + (trace.tokensUsed ?? 0), + duration: existing.duration + (trace.durationMs ?? 0), + }); + } + + if (trace.errorType) { + errors.push({ + traceId: trace.traceId, + stepType: trace.stepType, + errorType: trace.errorType, + errorMsg: trace.errorMsg ?? '', + }); + } + } + + return { + sessionId, + totalSteps: traces.length, + totalTokens, + totalDuration, + stageBreakdown: Array.from(stageMap.entries()).map(([stage, stats]) => ({ + stage, + ...stats, + })), + errors, + }; + } +} + diff --git a/backend/src/modules/agent/services/index.ts b/backend/src/modules/agent/services/index.ts new file mode 100644 index 00000000..092ca94d --- /dev/null +++ b/backend/src/modules/agent/services/index.ts @@ -0,0 +1,12 @@ +/** + * Agent Services Export + * + * @module agent/services + */ + +export { ConfigLoader } from './ConfigLoader.js'; +export { BaseAgentOrchestrator, type OrchestratorDependencies, type LLMServiceInterface } from './BaseAgentOrchestrator.js'; +export { QueryAnalyzer, type IntentType } from './QueryAnalyzer.js'; +export { StageManager, type TransitionCondition } from './StageManager.js'; +export { TraceLogger, type TraceLogInput } from './TraceLogger.js'; + diff --git a/backend/src/modules/agent/types/index.ts b/backend/src/modules/agent/types/index.ts new file mode 100644 index 00000000..0f26b2a7 --- /dev/null +++ b/backend/src/modules/agent/types/index.ts @@ -0,0 +1,382 @@ +/** + * Protocol Agent Framework - Core Types + * 通用Agent框架类型定义 + * + * @module agent/types + */ + +// ============================================================ +// 基础类型 +// ============================================================ + +/** Agent状态枚举 */ +export type AgentSessionStatus = 'active' | 'completed' | 'paused' | 'error'; + +/** 阶段状态枚举 */ +export type StageStatus = 'pending' | 'in_progress' | 'completed' | 'skipped'; + +/** 执行步骤类型 */ +export type StepType = 'query' | 'plan' | 'execute' | 'reflect' | 'tool_call' | 'sync'; + +/** Prompt类型 */ +export type PromptType = 'system' | 'stage' | 'extraction' | 'reflexion' | 'generation'; + +/** Reflexion规则类型 */ +export type ReflexionRuleType = 'rule_based' | 'prompt_based'; + +/** 规则严重性 */ +export type ReflexionSeverity = 'error' | 'warning' | 'info'; + +/** 失败处理动作 */ +export type FailureAction = 'block' | 'warn' | 'log'; + +// ============================================================ +// Agent定义相关 +// ============================================================ + +/** Agent全局配置 */ +export interface AgentConfig { + defaultModel: string; + maxTurns: number; + timeout: number; + enableTrace: boolean; + enableReflexion: boolean; +} + +/** Agent定义 */ +export interface AgentDefinition { + id: string; + code: string; + name: string; + description?: string; + version: string; + config?: AgentConfig; + isActive: boolean; + createdAt: Date; + updatedAt: Date; +} + +/** Agent阶段定义 */ +export interface AgentStage { + id: string; + agentId: string; + stageCode: string; + stageName: string; + sortOrder: number; + config?: Record; + nextStages: string[]; + isInitial: boolean; + isFinal: boolean; +} + +/** Agent Prompt定义 */ +export interface AgentPrompt { + id: string; + agentId: string; + stageId?: string; + promptType: PromptType; + promptCode: string; + content: string; + variables: string[]; + version: number; + isActive: boolean; +} + +// ============================================================ +// 会话与追踪 +// ============================================================ + +/** Agent会话 */ +export interface AgentSession { + id: string; + agentId: string; + conversationId: string; + userId: string; + currentStage: string; + status: AgentSessionStatus; + contextRef?: string; + turnCount: number; + totalTokens: number; + createdAt: Date; + updatedAt: Date; +} + +/** 执行追踪记录 */ +export interface AgentTraceRecord { + id: string; + sessionId: string; + traceId: string; + stepIndex: number; + stepType: StepType; + input?: Record; + output?: Record; + stageCode?: string; + modelUsed?: string; + tokensUsed?: number; + durationMs?: number; + errorType?: string; + errorMsg?: string; + createdAt: Date; +} + +// ============================================================ +// Reflexion规则 +// ============================================================ + +/** Reflexion规则定义 */ +export interface ReflexionRule { + id: string; + agentId: string; + ruleCode: string; + ruleName: string; + triggerStage?: string; + triggerTiming: 'on_sync' | 'on_stage_complete' | 'on_generate'; + ruleType: ReflexionRuleType; + conditions?: Record; + promptTemplate?: string; + severity: ReflexionSeverity; + failureAction: FailureAction; + isActive: boolean; + sortOrder: number; +} + +/** Reflexion检查结果 */ +export interface ReflexionResult { + ruleCode: string; + ruleName: string; + passed: boolean; + severity: ReflexionSeverity; + message?: string; + details?: Record; +} + +// ============================================================ +// Protocol Agent专用类型 +// ============================================================ + +/** Protocol Context状态 */ +export type ProtocolContextStatus = 'in_progress' | 'completed' | 'abandoned'; + +/** Protocol阶段代码 */ +export type ProtocolStageCode = + | 'scientific_question' + | 'pico' + | 'study_design' + | 'sample_size' + | 'endpoints'; + +/** 科学问题数据 */ +export interface ScientificQuestionData { + content: string; + background?: string; + significance?: string; + confirmed: boolean; + confirmedAt?: Date; +} + +/** PICO要素 */ +export interface PICOElement { + value: string; + details?: string; +} + +/** PICO数据 */ +export interface PICOData { + P: PICOElement; // Population + I: PICOElement; // Intervention + C: PICOElement; // Comparison + O: PICOElement; // Outcome + confirmed: boolean; + confirmedAt?: Date; +} + +/** 研究设计数据 */ +export interface StudyDesignData { + type: string; // RCT, Cohort, Case-Control, etc. + blinding?: string; // Open, Single-blind, Double-blind + randomization?: string; // Simple, Block, Stratified + duration?: string; // 研究周期 + multiCenter?: boolean; + centerCount?: number; + confirmed: boolean; + confirmedAt?: Date; +} + +/** 样本量数据 */ +export interface SampleSizeData { + total: number; + perGroup?: number; + alpha?: number; // 显著性水平 + power?: number; // 统计效力 + effectSize?: number; // 效应量 + dropoutRate?: number; // 脱落率 + justification?: string; // 计算依据 + confirmed: boolean; + confirmedAt?: Date; +} + +/** 终点指标项 */ +export interface EndpointItem { + name: string; + definition?: string; + method?: string; + timePoint?: string; +} + +/** 终点指标数据 */ +export interface EndpointsData { + primary: EndpointItem[]; + secondary: EndpointItem[]; + safety: EndpointItem[]; + exploratory?: EndpointItem[]; + confirmed: boolean; + confirmedAt?: Date; +} + +/** Protocol上下文完整数据 */ +export interface ProtocolContextData { + id: string; + conversationId: string; + userId: string; + currentStage: ProtocolStageCode; + status: ProtocolContextStatus; + + // 5个核心阶段数据 + scientificQuestion?: ScientificQuestionData; + pico?: PICOData; + studyDesign?: StudyDesignData; + sampleSize?: SampleSizeData; + endpoints?: EndpointsData; + + // 元数据 + completedStages: ProtocolStageCode[]; + lastActiveAt: Date; + createdAt: Date; + updatedAt: Date; +} + +/** Protocol生成记录 */ +export interface ProtocolGenerationRecord { + id: string; + contextId: string; + userId: string; + generatedContent: string; + contentVersion: number; + promptUsed: string; + modelUsed: string; + tokensUsed?: number; + durationMs?: number; + wordFileKey?: string; + lastExportedAt?: Date; + status: 'generating' | 'completed' | 'failed'; + errorMessage?: string; + createdAt: Date; + updatedAt: Date; +} + +// ============================================================ +// 服务接口类型 +// ============================================================ + +/** 用户消息输入 */ +export interface UserMessageInput { + conversationId: string; + userId: string; + content: string; + messageId?: string; + attachments?: unknown[]; +} + +/** Agent响应 */ +export interface AgentResponse { + content: string; + thinkingContent?: string; + + // 元数据 + stage: string; + stageName: string; + + // 动作卡片 + actionCards?: ActionCard[]; + + // 同步按钮 + syncButton?: SyncButtonData; + + // Token统计 + tokensUsed?: number; + modelUsed?: string; +} + +/** 动作卡片 */ +export interface ActionCard { + id: string; + type: string; + title: string; + description?: string; + actionUrl?: string; + actionParams?: Record; +} + +/** 同步按钮数据 */ +export interface SyncButtonData { + stageCode: string; + extractedData: Record; + label: string; + disabled?: boolean; +} + +/** 同步请求 */ +export interface SyncRequest { + conversationId: string; + userId: string; + stageCode: ProtocolStageCode; + data: Record; +} + +/** 同步响应 */ +export interface SyncResponse { + success: boolean; + context: ProtocolContextData; + nextStage?: ProtocolStageCode; + message?: string; +} + +/** 意图识别结果 */ +export interface IntentAnalysis { + intent: string; + confidence: number; + entities: Record; + suggestedAction?: string; +} + +/** 阶段转换结果 */ +export interface StageTransitionResult { + success: boolean; + fromStage: string; + toStage: string; + message?: string; + reflexionResults?: ReflexionResult[]; +} + +// ============================================================ +// 配置加载相关 +// ============================================================ + +/** Agent完整配置(含所有关联数据) */ +export interface AgentFullConfig { + definition: AgentDefinition; + stages: AgentStage[]; + prompts: AgentPrompt[]; + reflexionRules: ReflexionRule[]; +} + +/** Prompt渲染上下文 */ +export interface PromptRenderContext { + stage: string; + context: ProtocolContextData; + conversationHistory?: Array<{ role: string; content: string }>; + userMessage: string; + additionalVariables?: Record; +} + + diff --git a/backend/src/modules/aia/controllers/agentController.ts b/backend/src/modules/aia/controllers/agentController.ts index 5dcfc0a5..1a16c8a8 100644 --- a/backend/src/modules/aia/controllers/agentController.ts +++ b/backend/src/modules/aia/controllers/agentController.ts @@ -244,3 +244,5 @@ async function matchIntent(query: string): Promise<{ + + diff --git a/backend/src/modules/aia/controllers/attachmentController.ts b/backend/src/modules/aia/controllers/attachmentController.ts index e4cc6172..ae315f3d 100644 --- a/backend/src/modules/aia/controllers/attachmentController.ts +++ b/backend/src/modules/aia/controllers/attachmentController.ts @@ -98,3 +98,5 @@ export async function uploadAttachment( + + diff --git a/backend/src/modules/aia/index.ts b/backend/src/modules/aia/index.ts index 565c0c9d..93cf32f6 100644 --- a/backend/src/modules/aia/index.ts +++ b/backend/src/modules/aia/index.ts @@ -27,3 +27,5 @@ export { aiaRoutes }; + + diff --git a/backend/src/modules/asl/fulltext-screening/__tests__/api-integration-test.ts b/backend/src/modules/asl/fulltext-screening/__tests__/api-integration-test.ts index af6a01ce..52ff3813 100644 --- a/backend/src/modules/asl/fulltext-screening/__tests__/api-integration-test.ts +++ b/backend/src/modules/asl/fulltext-screening/__tests__/api-integration-test.ts @@ -368,6 +368,8 @@ runTests().catch((error) => { + + diff --git a/backend/src/modules/asl/fulltext-screening/__tests__/e2e-real-test-v2.ts b/backend/src/modules/asl/fulltext-screening/__tests__/e2e-real-test-v2.ts index d58dd227..9afe67f2 100644 --- a/backend/src/modules/asl/fulltext-screening/__tests__/e2e-real-test-v2.ts +++ b/backend/src/modules/asl/fulltext-screening/__tests__/e2e-real-test-v2.ts @@ -309,6 +309,8 @@ runTest() + + diff --git a/backend/src/modules/asl/fulltext-screening/__tests__/fulltext-screening-api.http b/backend/src/modules/asl/fulltext-screening/__tests__/fulltext-screening-api.http index 220a1c35..35f5892b 100644 --- a/backend/src/modules/asl/fulltext-screening/__tests__/fulltext-screening-api.http +++ b/backend/src/modules/asl/fulltext-screening/__tests__/fulltext-screening-api.http @@ -347,6 +347,8 @@ Content-Type: application/json + + diff --git a/backend/src/modules/dc/tool-b/services/ConflictDetectionService.ts b/backend/src/modules/dc/tool-b/services/ConflictDetectionService.ts index 8ae9e674..a037cb86 100644 --- a/backend/src/modules/dc/tool-b/services/ConflictDetectionService.ts +++ b/backend/src/modules/dc/tool-b/services/ConflictDetectionService.ts @@ -283,6 +283,8 @@ export const conflictDetectionService = new ConflictDetectionService(); + + diff --git a/backend/src/modules/dc/tool-c/README.md b/backend/src/modules/dc/tool-c/README.md index 3088408f..f60582f6 100644 --- a/backend/src/modules/dc/tool-c/README.md +++ b/backend/src/modules/dc/tool-c/README.md @@ -233,6 +233,8 @@ curl -X POST http://localhost:3000/api/v1/dc/tool-c/test/execute \ + + diff --git a/backend/src/modules/dc/tool-c/controllers/StreamAIController.ts b/backend/src/modules/dc/tool-c/controllers/StreamAIController.ts index e34dd0f9..d6313279 100644 --- a/backend/src/modules/dc/tool-c/controllers/StreamAIController.ts +++ b/backend/src/modules/dc/tool-c/controllers/StreamAIController.ts @@ -287,6 +287,8 @@ export const streamAIController = new StreamAIController(); + + diff --git a/backend/src/modules/iit-manager/agents/SessionMemory.ts b/backend/src/modules/iit-manager/agents/SessionMemory.ts index e1ea5852..e97d5d1a 100644 --- a/backend/src/modules/iit-manager/agents/SessionMemory.ts +++ b/backend/src/modules/iit-manager/agents/SessionMemory.ts @@ -196,6 +196,8 @@ logger.info('[SessionMemory] 会话记忆管理器已启动', { + + diff --git a/backend/src/modules/iit-manager/check-iit-table-structure.ts b/backend/src/modules/iit-manager/check-iit-table-structure.ts index a40a8d4c..9bfd11d1 100644 --- a/backend/src/modules/iit-manager/check-iit-table-structure.ts +++ b/backend/src/modules/iit-manager/check-iit-table-structure.ts @@ -130,6 +130,8 @@ checkTableStructure(); + + diff --git a/backend/src/modules/iit-manager/check-project-config.ts b/backend/src/modules/iit-manager/check-project-config.ts index d703026f..566de5e8 100644 --- a/backend/src/modules/iit-manager/check-project-config.ts +++ b/backend/src/modules/iit-manager/check-project-config.ts @@ -117,6 +117,8 @@ checkProjectConfig().catch(console.error); + + diff --git a/backend/src/modules/iit-manager/check-test-project-in-db.ts b/backend/src/modules/iit-manager/check-test-project-in-db.ts index 2ac39a0d..553bf3b2 100644 --- a/backend/src/modules/iit-manager/check-test-project-in-db.ts +++ b/backend/src/modules/iit-manager/check-test-project-in-db.ts @@ -99,6 +99,8 @@ main(); + + diff --git a/backend/src/modules/iit-manager/docs/微信服务号接入指南.md b/backend/src/modules/iit-manager/docs/微信服务号接入指南.md index 7e690c5f..48615f43 100644 --- a/backend/src/modules/iit-manager/docs/微信服务号接入指南.md +++ b/backend/src/modules/iit-manager/docs/微信服务号接入指南.md @@ -556,6 +556,8 @@ URL: https://iit.xunzhengyixue.com/api/v1/iit/patient-wechat/callback + + diff --git a/backend/src/modules/iit-manager/generate-wechat-tokens.ts b/backend/src/modules/iit-manager/generate-wechat-tokens.ts index 5686d4ac..319e5640 100644 --- a/backend/src/modules/iit-manager/generate-wechat-tokens.ts +++ b/backend/src/modules/iit-manager/generate-wechat-tokens.ts @@ -191,6 +191,8 @@ console.log(''); + + diff --git a/backend/src/modules/iit-manager/services/PatientWechatService.ts b/backend/src/modules/iit-manager/services/PatientWechatService.ts index 4a8bd509..a4d259ed 100644 --- a/backend/src/modules/iit-manager/services/PatientWechatService.ts +++ b/backend/src/modules/iit-manager/services/PatientWechatService.ts @@ -508,6 +508,8 @@ export const patientWechatService = new PatientWechatService(); + + diff --git a/backend/src/modules/iit-manager/test-chatservice-dify.ts b/backend/src/modules/iit-manager/test-chatservice-dify.ts index a9c12259..f27b7a91 100644 --- a/backend/src/modules/iit-manager/test-chatservice-dify.ts +++ b/backend/src/modules/iit-manager/test-chatservice-dify.ts @@ -153,6 +153,8 @@ testDifyIntegration().catch(error => { + + diff --git a/backend/src/modules/iit-manager/test-iit-database.ts b/backend/src/modules/iit-manager/test-iit-database.ts index 43291a75..145cb277 100644 --- a/backend/src/modules/iit-manager/test-iit-database.ts +++ b/backend/src/modules/iit-manager/test-iit-database.ts @@ -182,6 +182,8 @@ testIitDatabase() + + diff --git a/backend/src/modules/iit-manager/test-patient-wechat-config.ts b/backend/src/modules/iit-manager/test-patient-wechat-config.ts index de548abe..54925543 100644 --- a/backend/src/modules/iit-manager/test-patient-wechat-config.ts +++ b/backend/src/modules/iit-manager/test-patient-wechat-config.ts @@ -168,6 +168,8 @@ if (hasError) { + + diff --git a/backend/src/modules/iit-manager/test-patient-wechat-url-verify.ts b/backend/src/modules/iit-manager/test-patient-wechat-url-verify.ts index 81f6d17c..71b7a7f8 100644 --- a/backend/src/modules/iit-manager/test-patient-wechat-url-verify.ts +++ b/backend/src/modules/iit-manager/test-patient-wechat-url-verify.ts @@ -194,6 +194,8 @@ async function testUrlVerification() { + + diff --git a/backend/src/modules/iit-manager/test-redcap-query-from-db.ts b/backend/src/modules/iit-manager/test-redcap-query-from-db.ts index b696405e..02170602 100644 --- a/backend/src/modules/iit-manager/test-redcap-query-from-db.ts +++ b/backend/src/modules/iit-manager/test-redcap-query-from-db.ts @@ -275,6 +275,8 @@ main().catch((error) => { + + diff --git a/backend/src/modules/iit-manager/test-wechat-mp-local.ps1 b/backend/src/modules/iit-manager/test-wechat-mp-local.ps1 index 3f2d960f..8341b983 100644 --- a/backend/src/modules/iit-manager/test-wechat-mp-local.ps1 +++ b/backend/src/modules/iit-manager/test-wechat-mp-local.ps1 @@ -159,6 +159,8 @@ Write-Host "" + + diff --git a/backend/src/modules/iit-manager/types/index.ts b/backend/src/modules/iit-manager/types/index.ts index b2c1c9b3..8946d660 100644 --- a/backend/src/modules/iit-manager/types/index.ts +++ b/backend/src/modules/iit-manager/types/index.ts @@ -252,6 +252,8 @@ export interface CachedProtocolRules { + + diff --git a/backend/src/modules/pkb/routes/health.ts b/backend/src/modules/pkb/routes/health.ts index f32a4eb9..4ddc33a1 100644 --- a/backend/src/modules/pkb/routes/health.ts +++ b/backend/src/modules/pkb/routes/health.ts @@ -65,6 +65,8 @@ export default async function healthRoutes(fastify: FastifyInstance) { + + diff --git a/backend/src/modules/rvw/__tests__/api.http b/backend/src/modules/rvw/__tests__/api.http index 137f03ec..6d26e9a2 100644 --- a/backend/src/modules/rvw/__tests__/api.http +++ b/backend/src/modules/rvw/__tests__/api.http @@ -143,6 +143,8 @@ Content-Type: application/json + + diff --git a/backend/src/modules/rvw/__tests__/test-api.ps1 b/backend/src/modules/rvw/__tests__/test-api.ps1 index 2ae44ae3..6839a556 100644 --- a/backend/src/modules/rvw/__tests__/test-api.ps1 +++ b/backend/src/modules/rvw/__tests__/test-api.ps1 @@ -128,6 +128,8 @@ Write-Host " - 删除任务: DELETE $BaseUrl/api/v1/rvw/tasks/{taskId}" -Foregr + + diff --git a/backend/src/modules/rvw/index.ts b/backend/src/modules/rvw/index.ts index ac821f15..03b894b6 100644 --- a/backend/src/modules/rvw/index.ts +++ b/backend/src/modules/rvw/index.ts @@ -42,6 +42,8 @@ export * from './services/utils.js'; + + diff --git a/backend/src/modules/rvw/services/utils.ts b/backend/src/modules/rvw/services/utils.ts index b2bd0616..6b76d956 100644 --- a/backend/src/modules/rvw/services/utils.ts +++ b/backend/src/modules/rvw/services/utils.ts @@ -133,6 +133,8 @@ export function validateAgentSelection(agents: string[]): void { + + diff --git a/backend/src/tests/README.md b/backend/src/tests/README.md index aceaa45f..65d79af9 100644 --- a/backend/src/tests/README.md +++ b/backend/src/tests/README.md @@ -433,6 +433,8 @@ SET session_replication_role = 'origin'; + + diff --git a/backend/src/tests/test-cross-language-search.ts b/backend/src/tests/test-cross-language-search.ts index 1867a637..f3b081c7 100644 --- a/backend/src/tests/test-cross-language-search.ts +++ b/backend/src/tests/test-cross-language-search.ts @@ -114,3 +114,5 @@ testCrossLanguageSearch(); + + diff --git a/backend/src/tests/test-query-rewrite.ts b/backend/src/tests/test-query-rewrite.ts index aaf54a7a..7e5a57ff 100644 --- a/backend/src/tests/test-query-rewrite.ts +++ b/backend/src/tests/test-query-rewrite.ts @@ -176,3 +176,5 @@ testQueryRewrite(); + + diff --git a/backend/src/tests/test-rerank.ts b/backend/src/tests/test-rerank.ts index 41e87f48..ba880026 100644 --- a/backend/src/tests/test-rerank.ts +++ b/backend/src/tests/test-rerank.ts @@ -122,3 +122,5 @@ testRerank(); + + diff --git a/backend/src/tests/verify-test1-database.sql b/backend/src/tests/verify-test1-database.sql index 3fd47718..f5229e6e 100644 --- a/backend/src/tests/verify-test1-database.sql +++ b/backend/src/tests/verify-test1-database.sql @@ -135,6 +135,8 @@ WHERE key = 'verify_test'; + + diff --git a/backend/src/tests/verify-test1-database.ts b/backend/src/tests/verify-test1-database.ts index 5bb64f5a..b9206e9a 100644 --- a/backend/src/tests/verify-test1-database.ts +++ b/backend/src/tests/verify-test1-database.ts @@ -278,6 +278,8 @@ verifyDatabase() + + diff --git a/backend/src/types/global.d.ts b/backend/src/types/global.d.ts index 0cfe6924..69508978 100644 --- a/backend/src/types/global.d.ts +++ b/backend/src/types/global.d.ts @@ -68,6 +68,8 @@ export {} + + diff --git a/backend/sync-dc-database.ps1 b/backend/sync-dc-database.ps1 index ff830387..e3c1b96a 100644 --- a/backend/sync-dc-database.ps1 +++ b/backend/sync-dc-database.ps1 @@ -91,6 +91,8 @@ Write-Host "✅ 完成!" -ForegroundColor Green + + diff --git a/backend/temp_check.sql b/backend/temp_check.sql index 8ce4f7d2..19602e65 100644 --- a/backend/temp_check.sql +++ b/backend/temp_check.sql @@ -17,5 +17,7 @@ SELECT schema_name FROM information_schema.schemata WHERE schema_name NOT IN ('p + + diff --git a/backend/test-pkb-migration.http b/backend/test-pkb-migration.http index e09f5525..8c43c490 100644 --- a/backend/test-pkb-migration.http +++ b/backend/test-pkb-migration.http @@ -179,6 +179,8 @@ DELETE {{baseUrl}}/api/v1/pkb/knowledge/knowledge-bases/{{testKbId}} + + diff --git a/backend/test-tool-c-advanced-scenarios.mjs b/backend/test-tool-c-advanced-scenarios.mjs index ac4f5f2a..72eb2f99 100644 --- a/backend/test-tool-c-advanced-scenarios.mjs +++ b/backend/test-tool-c-advanced-scenarios.mjs @@ -378,6 +378,8 @@ runAdvancedTests().catch(error => { + + diff --git a/backend/test-tool-c-day2.mjs b/backend/test-tool-c-day2.mjs index dcce41ed..fb6c0fd0 100644 --- a/backend/test-tool-c-day2.mjs +++ b/backend/test-tool-c-day2.mjs @@ -444,6 +444,8 @@ runAllTests() + + diff --git a/backend/test-tool-c-day3.mjs b/backend/test-tool-c-day3.mjs index 328f37a9..5582c1fd 100644 --- a/backend/test-tool-c-day3.mjs +++ b/backend/test-tool-c-day3.mjs @@ -402,6 +402,8 @@ runAllTests() + + diff --git a/backend/verify_all_users.ts b/backend/verify_all_users.ts index f5d44760..36fb56e6 100644 --- a/backend/verify_all_users.ts +++ b/backend/verify_all_users.ts @@ -37,5 +37,7 @@ main() + + diff --git a/backend/verify_functions.ts b/backend/verify_functions.ts index fee43851..d0cacaee 100644 --- a/backend/verify_functions.ts +++ b/backend/verify_functions.ts @@ -35,5 +35,7 @@ main() + + diff --git a/backend/verify_job_common.ts b/backend/verify_job_common.ts index 79b89a65..88d4f93f 100644 --- a/backend/verify_job_common.ts +++ b/backend/verify_job_common.ts @@ -47,5 +47,7 @@ main() + + diff --git a/backend/verify_mock_user.ts b/backend/verify_mock_user.ts index d7db66df..a4be222c 100644 --- a/backend/verify_mock_user.ts +++ b/backend/verify_mock_user.ts @@ -36,5 +36,7 @@ main() + + diff --git a/backend/verify_system.ts b/backend/verify_system.ts index d3486f86..c0815dca 100644 --- a/backend/verify_system.ts +++ b/backend/verify_system.ts @@ -176,5 +176,7 @@ main() + + diff --git a/deploy-to-sae.ps1 b/deploy-to-sae.ps1 index 53607c98..cdbd7412 100644 --- a/deploy-to-sae.ps1 +++ b/deploy-to-sae.ps1 @@ -186,6 +186,8 @@ Set-Location .. + + diff --git a/docs/00-系统总体设计/00-系统当前状态与开发指南.md b/docs/00-系统总体设计/00-系统当前状态与开发指南.md index b463b336..39d01dc1 100644 --- a/docs/00-系统总体设计/00-系统当前状态与开发指南.md +++ b/docs/00-系统总体设计/00-系统当前状态与开发指南.md @@ -1,18 +1,20 @@ # AIclinicalresearch 系统当前状态与开发指南 -> **文档版本:** v4.1 +> **文档版本:** v4.2 > **创建日期:** 2025-11-28 > **维护者:** 开发团队 -> **最后更新:** 2026-01-22 +> **最后更新:** 2026-01-24 > **🎉 重大里程碑:** +> - **2026-01-24:Protocol Agent MVP完成!** 可复用Agent框架+5阶段对话流程 > - **2026-01-22:OSS 存储集成完成!** 阿里云 OSS 正式接入平台基础层 > - **2026-01-21:成功替换 Dify!** PKB 模块完全使用自研 pgvector RAG 引擎 > -> **最新进展(OSS 集成):** -> - ✅ **4个 Bucket 已创建**:生产/开发 × 数据/静态资源 -> - ✅ **存储适配器架构**:OSSAdapter + LocalAdapter,支持云端和私有化部署 -> - ✅ **PKB 模块集成**:文档上传到 OSS,目录结构规范化 -> - ✅ **开发规范建立**:`docs/04-开发规范/11-OSS存储开发规范.md` +> **最新进展(Protocol Agent):** +> - ✅ **通用Agent框架**:Query→Planner→Executor→Tools→Reflection 五层架构 +> - ✅ **8张数据库表**:agent_schema(6) + protocol_schema(2) +> - ✅ **5阶段对话流程**:科学问题→PICO→研究设计→样本量→观察指标 +> - ✅ **前端完整实现**:Gemini折叠侧边栏+状态面板+100%原型图还原 +> - ⏳ **待联调测试**:前后端集成调试 > > **部署状态:** ✅ 生产环境运行中 | 公网地址:http://8.140.53.236/ > **文档目的:** 快速了解系统当前状态,为新AI助手提供上下文 @@ -47,7 +49,7 @@ | 模块代号 | 模块名称 | 核心功能 | 商业价值 | 当前状态 | 优先级 | |---------|---------|---------|---------|---------|--------| -| **AIA** | AI智能问答 | 12个智能体(选题→方案→评审→写作) | ⭐⭐⭐⭐⭐ | 🎉 **V2.1完成(90%)** - Prompt管理集成 | **P0** | +| **AIA** | AI智能问答 | 12个智能体 + Protocol Agent(全流程方案) | ⭐⭐⭐⭐⭐ | 🎉 **V3.0 MVP完成(75%)** - Agent框架+5阶段流程 | **P0** | | **PKB** | 个人知识库 | RAG问答、私人文献库 | ⭐⭐⭐ | 🎉 **Dify已替换!自研RAG上线(95%)** | P1 | | **ASL** | AI智能文献 | 文献筛选、Meta分析、证据图谱 | ⭐⭐⭐⭐⭐ | 🎉 **智能检索MVP完成(60%)** - DeepSearch集成 | **P0** | | **DC** | 数据清洗整理 | ETL + 医学NER(百万行级数据) | ⭐⭐⭐⭐⭐ | ✅ **Tool B完成 + Tool C 99%(异步架构+性能优化-99%+多指标转换+7大功能)** | **P0** | diff --git a/docs/01-平台基础层/02-存储服务/OSS账号与配置信息.md b/docs/01-平台基础层/02-存储服务/OSS账号与配置信息.md index 3bc4a2bb..ce485c59 100644 --- a/docs/01-平台基础层/02-存储服务/OSS账号与配置信息.md +++ b/docs/01-平台基础层/02-存储服务/OSS账号与配置信息.md @@ -164,3 +164,5 @@ npm run dev + + diff --git a/docs/01-平台基础层/02-存储服务/OSS集成开发记录-2026-01-22.md b/docs/01-平台基础层/02-存储服务/OSS集成开发记录-2026-01-22.md index 6e3a9d27..0c0b593e 100644 --- a/docs/01-平台基础层/02-存储服务/OSS集成开发记录-2026-01-22.md +++ b/docs/01-平台基础层/02-存储服务/OSS集成开发记录-2026-01-22.md @@ -191,3 +191,5 @@ OSS_INTERNAL=false # 本地开发用公网,生产用内网 + + diff --git a/docs/02-通用能力层/03-RAG引擎/05-RAG引擎使用指南.md b/docs/02-通用能力层/03-RAG引擎/05-RAG引擎使用指南.md index be268e30..452ef0a5 100644 --- a/docs/02-通用能力层/03-RAG引擎/05-RAG引擎使用指南.md +++ b/docs/02-通用能力层/03-RAG引擎/05-RAG引擎使用指南.md @@ -561,3 +561,5 @@ npx tsx src/tests/test-pdf-ingest.ts + + diff --git a/docs/02-通用能力层/03-RAG引擎/06-pg_bigm安装指南.md b/docs/02-通用能力层/03-RAG引擎/06-pg_bigm安装指南.md index 475442ee..2aebdb8b 100644 --- a/docs/02-通用能力层/03-RAG引擎/06-pg_bigm安装指南.md +++ b/docs/02-通用能力层/03-RAG引擎/06-pg_bigm安装指南.md @@ -210,3 +210,5 @@ SELECT * WHERE content LIKE '%肺癌%'; 4. ⏳ 生产环境部署(阿里云 RDS) 5. ⏳ 创建索引并验证性能 + + diff --git a/docs/02-通用能力层/快速引用卡片.md b/docs/02-通用能力层/快速引用卡片.md index 569b7a8f..3ad1a47d 100644 --- a/docs/02-通用能力层/快速引用卡片.md +++ b/docs/02-通用能力层/快速引用卡片.md @@ -239,3 +239,5 @@ const userId = 'test'; // ❌ 应该用 getUserId(request) + + diff --git a/docs/02-通用能力层/通用能力层技术债务清单.md b/docs/02-通用能力层/通用能力层技术债务清单.md index 04968f97..910c1b7b 100644 --- a/docs/02-通用能力层/通用能力层技术债务清单.md +++ b/docs/02-通用能力层/通用能力层技术债务清单.md @@ -812,6 +812,8 @@ export const AsyncProgressBar: React.FC = ({ + + diff --git a/docs/03-业务模块/ADMIN-运营管理端/00-Phase3.5完成总结.md b/docs/03-业务模块/ADMIN-运营管理端/00-Phase3.5完成总结.md index 04b7e8ae..bd0be381 100644 --- a/docs/03-业务模块/ADMIN-运营管理端/00-Phase3.5完成总结.md +++ b/docs/03-业务模块/ADMIN-运营管理端/00-Phase3.5完成总结.md @@ -308,3 +308,5 @@ Level 3: 兜底Prompt(缓存也失效) + + diff --git a/docs/03-业务模块/ADMIN-运营管理端/06-开发记录/2026-01-16_用户管理功能与模块权限系统完成.md b/docs/03-业务模块/ADMIN-运营管理端/06-开发记录/2026-01-16_用户管理功能与模块权限系统完成.md index 8862b20d..7e2fb990 100644 --- a/docs/03-业务模块/ADMIN-运营管理端/06-开发记录/2026-01-16_用户管理功能与模块权限系统完成.md +++ b/docs/03-业务模块/ADMIN-运营管理端/06-开发记录/2026-01-16_用户管理功能与模块权限系统完成.md @@ -494,3 +494,5 @@ const pageSize = Number(query.pageSize) || 20; + + diff --git a/docs/03-业务模块/ADMIN-运营管理端/README.md b/docs/03-业务模块/ADMIN-运营管理端/README.md index 1e0837f5..6ec767e8 100644 --- a/docs/03-业务模块/ADMIN-运营管理端/README.md +++ b/docs/03-业务模块/ADMIN-运营管理端/README.md @@ -228,3 +228,5 @@ ADMIN-运营管理端/ + + diff --git a/docs/03-业务模块/ADMIN运营与INST机构管理端-文档体系建立完成.md b/docs/03-业务模块/ADMIN运营与INST机构管理端-文档体系建立完成.md index d239c22b..c84df197 100644 --- a/docs/03-业务模块/ADMIN运营与INST机构管理端-文档体系建立完成.md +++ b/docs/03-业务模块/ADMIN运营与INST机构管理端-文档体系建立完成.md @@ -327,3 +327,5 @@ INST-机构管理端/ + + diff --git a/docs/03-业务模块/AIA-AI智能问答/00-模块当前状态与开发指南.md b/docs/03-业务模块/AIA-AI智能问答/00-模块当前状态与开发指南.md index 9309106d..73f11582 100644 --- a/docs/03-业务模块/AIA-AI智能问答/00-模块当前状态与开发指南.md +++ b/docs/03-业务模块/AIA-AI智能问答/00-模块当前状态与开发指南.md @@ -1,15 +1,16 @@ # AIA AI智能问答模块 - 当前状态与开发指南 -> **文档版本:** v2.1 +> **文档版本:** v3.0 > **创建日期:** 2026-01-14 > **维护者:** AIA模块开发团队 -> **最后更新:** 2026-01-18 🎉 **V2.1版本发布 - Prompt管理系统集成完成** +> **最后更新:** 2026-01-24 🎉 **V3.0版本发布 - Protocol Agent MVP完成** > **重大里程碑:** > - 🏆 通用流式响应服务(OpenAI Compatible) > - 🎨 现代感UI(100%还原原型图V11) > - 🚀 Ant Design X 深度集成 > - ✨ 12个智能体配置完成 > - 🆕 Prompt管理系统集成(灰度预览、版本管理) +> - 🎉 **Protocol Agent MVP完成(可复用Agent框架+5阶段对话流程)** --- @@ -41,9 +42,9 @@ AIA(AI Intelligent Assistant)模块提供覆盖临床研究全生命周期 ### 当前状态 -- **开发阶段:** ✅ **V2.1 Prompt 管理集成完成** -- **架构版本:** V2.1(集成 PromptService) -- **完成度:** 90%(Prompt 管理已对接,支持灰度预览) +- **开发阶段:** ✅ **V3.0 Protocol Agent MVP完成** +- **架构版本:** V3.0(通用Agent框架 + Protocol Agent) +- **完成度:** 75%(MVP核心流程完成,待前后端联调) ### ✅ V2.1 新增功能(2026-01-18) @@ -71,9 +72,26 @@ AIA(AI Intelligent Assistant)模块提供覆盖临床研究全生命周期 --- +### ✅ V3.0 Protocol Agent MVP(2026-01-24) + +**通用Agent框架:** +- [x] 5层架构完整实现:Query→Planner→Executor→Tools→Reflection +- [x] ConfigLoader, BaseAgentOrchestrator, QueryAnalyzer, StageManager, TraceLogger +- [x] agent_schema: 6张表,protocol_schema: 2张表 +- [x] 完整类型定义(382行) + +**Protocol Agent:** +- [x] 后端:6个核心服务,6个API端点,10/10测试通过 +- [x] 前端:三栏布局,5阶段状态面板,100%还原原型图 +- [x] 5阶段流程:科学问题→PICO→研究设计→样本量→观察指标 + +**待完成:** 前后端联调、一键生成、Word导出 + +--- + ## 🏗️ 架构设计 -### V2 架构特点 +### V3 架构特点 ``` ┌─────────────────────────────────────────────────────────┐ diff --git a/docs/03-业务模块/AIA-AI智能问答/00-系统设计/Protocol_Agent_Development_Simplification_Guide.md b/docs/03-业务模块/AIA-AI智能问答/00-系统设计/Protocol_Agent_Development_Simplification_Guide.md new file mode 100644 index 00000000..97c69ca2 --- /dev/null +++ b/docs/03-业务模块/AIA-AI智能问答/00-系统设计/Protocol_Agent_Development_Simplification_Guide.md @@ -0,0 +1,90 @@ +# **Protocol Agent 降本增效开发指南:从 MVP 到最终形态** + +核心原则: "Think Big, Start Small" (大处着眼,小处着手)。 +当前状态: 感到 PRD 工作量过大,需要削减范围,聚焦核心价值。 +目标: 将 V3 架构拆解为可落地的 MVP,优先保证核心流程跑通,暂缓非关键特性的开发。 + +## **1\. 心理减负:我们到底在做什么?** + +其实 Protocol Agent V3 的核心代码并没有 PRD 看起来那么吓人。剥去那些高大上的名词,它的本质其实很简单: + +1. **Orchestrator** ≈ 一个 switch/case 语句,根据当前步骤加载不同的 Prompt。 +2. **Context** ≈ 往数据库存一个 JSON 对象。 +3. **Action Card** ≈ 给前端发一个特定格式的 JSON,让它渲染个按钮。 +4. **Backend CoT** ≈ 把一大段 Prompt 拆成 3 段小 Prompt 拼起来。 + +**结论:** 技术本身不难(都是 CRUD 和字符串拼接),难的是逻辑梳理。只要逻辑理顺了,代码量其实可控。 + +## **2\. 战术削减:MVP 阶段可以“砍”掉什么?(Phase 1 Scope)** + +为了在第一阶段快速上线并验证价值,建议**暂时砍掉**以下 50% 的非核心功能: + +| 功能模块 | PRD 定义 (V3) | MVP 策略 (Phase 1\) | 理由 (为什么砍掉?) | +| :---- | :---- | :---- | :---- | +| **🧠 大脑 (Brain)** | 复杂的意图识别 Router | **线性流转** | 用户只要完成 PICO,就自动跳到样本量。不需要 AI 猜意图,强制按顺序来。 | +| **💾 记忆 (Memory)** | 复杂的增量提取算法 | **全量覆盖** | 不要做复杂的 diff 提取。每次对话结束后,直接让 AI 提取一次 PICO 覆盖存入 DB 即可。 | +| **📚 知识 (Knowledge)** | EKB V2 混合检索 | **Mock 数据 / 暂缓** | 第一版先用通用大模型的能力,暂不接自建知识库。或者仅接入最简单的关键词检索。 | +| **🛠️ 手脚 (Hands)** | 通用的 Action Card 协议 | **硬编码** | 不要写通用的 Deep Link 协议。直接硬编码:if (stage \== 'SAMPLE') return { type: 'sample\_card' }。 | +| **⚙️ 配置 (Ops)** | 可视化 SOP 编排后台 | **JSON 文件** | 不要开发 Admin UI。直接在代码里写一个 const SOP\_CONFIG \= \[...\] JSON 对象。 | +| **🛡️ 风控 (Reflexion)** | 复杂的规则引擎 | **Prompt 校验** | 不写代码规则。直接在 System Prompt 里加一句:“请检查样本量是否过小”。 | + +**MVP 核心交付物:** + +1. 一个能聊天的界面。 +2. 右侧能显示 PICO 表格(数据存 DB)。 +3. 能弹出一个样本量计算器,算完能把数填回右侧表格。 + (这就足够震撼了!) + +## **3\. 分阶段实施路线图 (Step-by-Step)** + +我们把原本宏大的 V3 计划,重新拆解为三个“舒适区”内的小目标。 + +### **✅ Phase 1: 核心链路打通 (2周)** + +**目标:** "跑通 Happy Path"。用户能进来,能聊,能算,能存。 + +1. **数据库**:建 ProtocolContext 表 (JSONB)。 +2. **后端**:写一个简单的 Orchestrator,只支持 SCIENTIFIC\_QUESTION \-\> PICO \-\> SAMPLE\_SIZE 三个阶段的线性切换。 +3. **前端**: + * 左侧聊天。 + * 右侧面板(只读,显示 Context JSON)。 + * 一个写死的“样本量计算”卡片。 +4. **验证**:用户说完 PICO,右侧面板能变;点击卡片能算数;算完数右侧面板能更新。**(这就够了!)** + +### **⏩ Phase 2: 配置化与逻辑增强 (2周)** + +**目标:** "把硬编码变成配置"。 + +1. **配置化**:把 Phase 1 里写死在代码里的 Prompt 和 SOP,提取到 prompt\_templates 表中。 +2. **管理后台**:做一个极其简陋的 Admin 页面(哪怕是纯文本框编辑 JSON),用来改 Prompt。 +3. **知识库接入**:此时再把 EKB V2 接入进来,增强“选题评价”环节的专业度。 + +### **🚀 Phase 3: 平台化与反思 (2周)** + +**目标:** "更聪明,更通用"。 + +1. **Reflexion**:加上 reflexionGuard,开始对用户算出的结果指指点点(“你这个样本量太小了”)。 +2. **通用化**:重构代码,提取 BaseOrchestrator,准备开发统计 Agent。 + +## **4\. 给开发团队的具体建议 (Actionable Advice)** + +1. **不要过度设计 (YAGNI)**: + * 不要一开始就写 interface IAgent,不要写复杂的 AbstractFactory。 + * 就写一个 ProtocolService.ts,里面的逻辑哪怕有点面条代码也没关系,先跑通。 +2. **利用现有资源**: + * **前端**:你们已经有了 Ant Design X,聊天界面和卡片组件几乎是现成的。 + * **后端**:你们已经有了 pg-boss,异步提取任务直接套用以前的代码。 + * **工具**:样本量计算器已经是现成的 Web 页面,只要加一行代码 window.parent.postMessage 就能和 Chat 通信(如果是 iframe 嵌入)或通过 URL 回调。 +3. **关于“力不从心”**: + * 这种感觉通常来自于\*\*“不确定性”\*\*。 + * **解决办法**:把 PRD 里的所有“待定”、“可能”、“未来支持”全部划掉。只做确定的、具体的、最简单的功能。 + +## **5\. 总结** + +**您不需要现在就造出完美的 AIA-OS。** + +现在的任务只是: +用 Postgres 存一个 JSON,让 LLM 根据这个 JSON 说话,再让一个网页计算器改一下这个 JSON。 +仅此而已。把这个做出来,就是巨大的成功。其他的(可视化编排、混合检索、多Agent协同)都是在这个核心循环跑通之后,自然生长出来的枝叶。 + +**建议:** 拿着这份“减负版”计划,跟团队说:“我们先不看那份几十页的 PRD 了,我们先花两周时间,把这三个核心功能(聊天、状态同步、工具调用)做出来。” \ No newline at end of file diff --git a/docs/03-业务模块/AIA-AI智能问答/00-系统设计/Protocol_Agent_PRD_v1.0.md b/docs/03-业务模块/AIA-AI智能问答/00-系统设计/Protocol_Agent_PRD_v1.0.md new file mode 100644 index 00000000..bffad01b --- /dev/null +++ b/docs/03-业务模块/AIA-AI智能问答/00-系统设计/Protocol_Agent_PRD_v1.0.md @@ -0,0 +1,158 @@ +# **产品需求文档 (PRD): 研究方案制定智能体 (Protocol Agent) & AIA 基础平台** + +文档版本: v1.0 +状态: Final Draft +日期: 2026-01-20 +项目代号: AIA-Protocol-V3 +核心理念: Plan-Execute-Reflexion (规划-执行-反思) | Backend-Driven SOP (后端驱动) | Deep Link Action (深度链接) + +## **1\. 项目背景与目标 (Background & Goals)** + +### **1.1 背景** + +当前 AIA 模块包含 7 个独立运行的 AI 问答工具(科学问题、PICO、选题评价等)。这种“散点式”的交互导致用户体验割裂,上下文(Context)无法流转,且缺乏对复杂逻辑(如样本量计算)的深度支持。 + +### **1.2 核心目标** + +1. **统一入口 (Unified Entry)**:将 7 个模块整合为一个 **Protocol Agent**,通过自然语言对话引导用户完成全流程。 +2. **长程记忆 (Long-term Memory)**:利用结构化数据 (JSONB) 维护一份“活的”研究方案,突破 LLM Context 限制。 +3. **专业深度 (Professional Depth)**:引入 **自建知识库 (EKB)** 和 **后端配置化思维链 (CoT)**,确保输出符合医学逻辑。 +4. **平台化基石 (Platform Foundation)**:沉淀一套通用的 Agent 开发框架 (AIA-OS),为未来的统计、清洗 Agent 打下地基。 + +## **2\. 产品核心理念:仿生架构 (The Bionic Architecture)** + +产品设计严格遵循 **"Brain-Memory-Knowledge-Hands"** 四位一体架构: + +| 组件 | 对应系统模块 | 核心职责 | +| :---- | :---- | :---- | +| **🧠 大脑 (Brain)** | **Orchestrator Engine** | 负责意图识别 (Router)、流程规划 (Plan)、思维链执行 (SOP) 和结果反思 (Reflexion)。 | +| **💾 记忆 (Memory)** | **ProtocolContext (DB)** | 存储结构化的方案数据(PICO、N值、指标),实现跨会话、跨工具的状态同步。 | +| **📚 知识 (Knowledge)** | **EKB (Self-built RAG)** | 基于 Postgres 的混合检索系统,提供 RCT 案例、指南、评分标准等外部知识。 | +| **🛠️ 手脚 (Hands)** | **Deep Link Action** | 通过 **Action Card** 协议唤起现有的 Web 工具(样本量计算器、清洗工具),并回写结果。 | + +## **3\. 用户流程 (User Journey)** + +### **3.1 典型交互路径** + +1. **意图识别**:用户输入“我想做一个阿司匹林的研究”。 + * *Brain*: 识别为 SCIENTIFIC\_QUESTION 阶段,加载对应 SOP。 +2. **多轮对话 (Plan)**:AI 引导用户完善 PICO 信息。 + * *Memory*: 实时提取 P, I, C, O 并存入数据库。 + * *UI*: 右侧 **状态面板 (State Panel)** 实时点亮已确认的字段。 +3. **工具执行 (Execute)**:用户确认 PICO 后,AI 判断需要计算样本量。 + * *Brain*: 生成 Action Card。 + * *UI*: 聊天窗显示“🚀 前往计算样本量”卡片。 + * *Hands*: 用户点击跳转 \-\> 在工具页完成计算 \-\> 点击“同步”。 +4. **反思校验 (Reflexion)**:数据回写后。 + * *Brain*: 触发后端校验规则(如 N \< 20)。 + * *UI*: AI 发送消息:“⚠️ 样本量过小,建议重新调整 Alpha 值。” +5. **阶段流转**:校验通过后,自动进入下一阶段(观察指标设计)。 + +## **4\. 功能需求详情 (Functional Requirements)** + +### **4.1 核心交互界面 (Unified Interface)** + +#### **F1. 流式对话窗口 (Chat Interface)** + +* **支持格式**:Markdown, Latex, **XML Thinking Block** (深度思考折叠区)。 +* **Action Card 渲染**:识别后端返回的 type: action\_card JSON,渲染为操作卡片。 +* **Reflexion 反馈**:识别系统级通知(System Message),以特殊样式(如紫色边框)展示 AI 的反思结果。 + +#### **F2. 实时状态面板 (Protocol State Panel)** + +* **位置**:屏幕右侧(桌面端)/ 抽屉式(移动端)。 +* **数据源**:实时订阅 ProtocolContext.data。 +* **展示内容**: + * 科学问题 (Text) + * PICO (Table) + * 样本量 (Highlight Number) + * 核心指标 (Tags) +* **交互**:支持用户**手动编辑**面板内容,编辑结果优先级高于 AI 提取。 + +### **4.2 编排引擎 (Orchestrator Backend)** + +#### **F3. 动态 SOP 编排 (Backend CoT)** + +* **机制**:从数据库 prompt\_templates.chain\_config 加载 SOP 步骤。 +* **执行**:将 SOP 编译为 System Prompt,强制 AI 输出 XML 思考标签。 +* **配置项**:支持在 Admin 后台配置 Step 顺序、指令内容、输出 Key。 + +#### **F4. 反思卫士 (Reflexion Guard)** + +* **触发时机**:工具回写数据后、阶段切换前。 +* **校验逻辑**: + * **硬规则**:N \< 10 (Fail), Power \< 0.8 (Warning)。 + * **软规则**:Prompt-based check ("检查对照组设置是否符合伦理?")。 + +### **4.3 知识库集成 (EKB Integration)** + +#### **F5. 混合检索 (Hybrid Search)** + +* **场景**:选题评价、指标设计。 +* **逻辑**: + * SQL 过滤:WHERE study\_design-\>\>'sample\_size' \> 500。 + * Vector 检索:ORDER BY embedding \<=\> query。 + * Rerank:调用重排序模型取 Top 3。 + +### **4.4 工具集成 (Deep Link)** + +#### **F6. 通用回写协议 (Universal Sync API)** + +* **API**: POST /api/v1/aia/context/:id/sync +* **Payload**: { "agent\_type": "PROTOCOL", "key": "sample\_size", "value": {...} } +* **工具端改造**:现有样本量计算器需支持解析 URL 参数 (?ctx\_id=...) 并在计算完成后调用回写 API。 + +## **5\. 后端配置与运营 (LLM Ops)** + +为了支持“后端配置化”,需要构建一个简易的运营后台。 + +### **5.1 配置管理** + +* **全局配置**:Base Persona, Memory Schema。 +* **阶段配置**:7 个阶段的 Prompt, SOP (JSON Array), Reflexion Rules。 + +### **5.2 可观测性 (Trace)** + +* **会话回放**:查看每一次 Router 决策、CoT 思考过程、Tool 调用结果。 +* **Bad Case 修正**:基于 Trace ID 快速定位问题,调整 Prompt 后一键重测。 + +## **6\. 平台化扩展策略 (Platform Strategy)** + +本项目的交付物不仅仅是一个 Agent,而是一套 **AIA-OS 规范**。 + +### **6.1 通用能力抽象** + +* **Context Interface**:定义 IContext 接口,未来支持 StatsContext (统计)、CleaningContext (清洗)。 +* **Action Card Protocol**:action\_card JSON 结构在所有 Agent 中通用。 +* **Orchestrator Core**:状态机引擎与业务逻辑解耦,未来只需编写不同的 Config 即可生成新 Agent。 + +## **7\. 技术架构 (Technical Architecture)** + +* **前端**:React 19 \+ Ant Design X \+ Tailwind CSS +* **后端**:Node.js (Fastify) +* **语言**:TypeScript (全栈) +* **数据库**:PostgreSQL 15+ (pgvector 插件) +* **ORM**:Prisma (Schema-first) +* **异步队列**:pg-boss (处理耗时任务如 PDF 解析、结构化提取) +* **模型层**:OpenAI Compatible API (DeepSeek/Qwen) + +## **8\. 实施路线图 (Roadmap)** + +### **Phase 1: 核心骨架 (Week 1\)** + +* \[后端\] 建立 protocol\_contexts 和 prompt\_templates (含 CoT 字段) 表。 +* \[后端\] 实现 ProtocolOrchestrator 基础状态机。 +* \[前端\] 搭建 Chat \+ State Panel 布局。 + +### **Phase 2: 智能增强 (Week 2\)** + +* \[后端\] 实现动态 CoT 组装与 XML 解析。 +* \[后端\] 集成 EKB V2 混合检索服务。 +* \[后端\] 实现异步 PICO 提取 Worker。 + +### **Phase 3: 闭环打通 (Week 3\)** + +* \[前端\] 开发 Action Card 组件。 +* \[工具\] 改造样本量计算器,支持 Deep Link 回写。 +* \[后端\] 实现 Reflexion Guard 校验逻辑。 +* \[Ops\] 上线简易配置后台 (Admin UI)。 \ No newline at end of file diff --git a/docs/03-业务模块/AIA-AI智能问答/00-系统设计/后端配置原型图V3.html b/docs/03-业务模块/AIA-AI智能问答/00-系统设计/后端配置原型图V3.html new file mode 100644 index 00000000..cbb4e41a --- /dev/null +++ b/docs/03-业务模块/AIA-AI智能问答/00-系统设计/后端配置原型图V3.html @@ -0,0 +1,381 @@ + + + + + + AIA Protocol Agent Ops - V3.0 + + + + + + + + + +
+
+
+ +
+
+

Protocol Agent Ops

+
+ Env: Production + + Orchestrator V3.0 +
+
+
+ +
+
+ + API Status: Healthy +
+ +
+
+ +
+ + + + + +
+ + +
+

+ + 全局配置中心 +

+

定义 Protocol Agent 的底层逻辑、记忆结构和通用知识库挂载。

+
+ + +
+
+

+ + ID: SAMPLE_SIZE_CALC +

+

配置该阶段的任务指令、思维链逻辑和反思规则。

+
+ +
+ + + +
+
+ + +
+
+ + + + +
+
+

Base System Prompt (基座人设)

+ +
+ +
+
+
+ + + + +
+ + +
+

+ + 记忆结构定义 (Memory Schema) +

+
+
{
+  "scientific_question": "string",
+  "pico": {
+    "p": "string (Patient)",
+    "i": "string (Intervention)",
+    "c": "string (Comparison)",
+    "o": "string (Outcome)"
+  },
+  "sample_size": {
+    "n_total": "number",
+    "power": "number",
+    "alpha": "number"
+  },
+  ...
+}
+
+

此 Schema 定义了 Agent 可以“记住”的结构化数据字段。修改此配置需同步更新后端 Prisma Schema。

+
+ + +
+

+ + 知识库挂载 (EKB Mounting) +

+
+
+
+
+ +
+
+
Clinical Guidelines (临床指南库)
+
包含 2020-2025 全球核心指南 | 12,400 篇
+
+
+
+ Active + +
+
+
+
+
+ +
+
+
RCT Protocols (RCT 方案库)
+
高质量 RCT 注册方案 | 5,300 篇
+
+
+
+ Active + +
+
+
+
+
+ + + + +
+
+
+

Task Instruction (阶段任务指令)

+
这将覆盖到 System Prompt 的 [Stage Instruction] 部分
+
+ +
+
+ + + + +
+ +
+

思维链 SOP 可视化编排

+ +
+ + +
+ + +
+
+
+
+ 1 +
+
+
+ +
+
Output: <completeness_check>
+ +
+
+ +
+
+
+ + +
+
+
+
+ 2 +
+
+
+ +
+
Output: <method_match>
+ +
+
+ +
+
+
+ + + +
+
+ + + + +
+ +
+ +
+

Reflexion Guard (反思卫士)

+

当工具执行完毕并回写数据时,后端会自动运行这些规则。如果校验失败,Agent 将拒绝推进流程并报警。

+
+
+ +
+
+

规则列表 (Rule-based)

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
校验字段逻辑阈值报警消息操作
sample_size.n<10样本量过小,无法通过伦理审查
sample_size.n>100000样本量过大,请确认可行性
+
+ +
+

AI 软校验 (Prompt-based)

+ +
+
+ +
+
+
+ +
+ + + + \ No newline at end of file diff --git a/docs/03-业务模块/AIA-AI智能问答/00-系统设计/研究方案原型图0119.html b/docs/03-业务模块/AIA-AI智能问答/00-系统设计/研究方案原型图0119.html new file mode 100644 index 00000000..2eaa5791 --- /dev/null +++ b/docs/03-业务模块/AIA-AI智能问答/00-系统设计/研究方案原型图0119.html @@ -0,0 +1,442 @@ + + + + + + Protocol Agent V2.0 - 产品原型 + + + + + + + + +
+
+
+ +
+
+

研究方案制定 Agent

+
+ + Orchestrator Online +
+
+
+
+ + +
+
+ + +
+ + +
+ + +
+ + +
+
+ +
+
+
Protocol Agent • 10:00 AM
+
+

您好!我是您的研究方案制定助手。

+

我已经记录了您关于“阿司匹林预防老年高血压患者中风”的科学问题和 PICO 信息。

+

根据当前的 RCT 设计,我们需要进行样本量计算以支撑伦理审查。您准备好开始了吗?

+
+
+
+ + +
+
+ +
+
+
User • 10:02 AM
+
+ 好的,帮我计算一下样本量。 +
+
+
+ + +
+
+ +
+
+
Protocol Agent • 10:02 AM
+ + +
+
+
+ +
+
+

建议执行:样本量计算

+

基于双侧两样本均数比较 (RCT)

+
+
+ + +
+
+ Alpha (α): + 0.05 +
+
+ Power (1-β): + 0.80 +
+
+ Effect Size: + 预估 0.15 vs 0.10 +
+
+ + +

点击将打开工具面板,参数已自动预填

+
+
+
+ + + +
+ + +
+
+ + +
+
+
+ + + + +
+ + + + + + + \ No newline at end of file diff --git a/docs/03-业务模块/AIA-AI智能问答/02-技术设计/Protocol_Agent_Architecture_Design_V3.md b/docs/03-业务模块/AIA-AI智能问答/02-技术设计/Protocol_Agent_Architecture_Design_V3.md new file mode 100644 index 00000000..600f238e --- /dev/null +++ b/docs/03-业务模块/AIA-AI智能问答/02-技术设计/Protocol_Agent_Architecture_Design_V3.md @@ -0,0 +1,163 @@ +# **研究方案制定 Agent (Protocol Agent) 统一架构方案 V3.0** + +版本: v3.0 (SOP编排与后端配置版) +日期: 2026-01-20 +适用范围: 壹证循科技 (AIclinicalresearch) +核心进化: 引入 "Code-as-Skeleton, Data-as-Brain" 理念。将思维链 (CoT) 下沉为后端可配置的 SOP 数据,实现逻辑与代码解耦。 + +## **1\. 核心设计理念** + +我们摒弃传统的“无状态问答”模式,构建一个维护长程记忆、具备反思能力的 **Protocol Agent**。 + +### **1.1 四大核心组件 (The 4 Pillars)** + +* 🧠 **大脑 (Brain): Orchestrator 服务** + * 负责 **规划 (Plan)**:决定当前处于哪个阶段,下一步该做什么。 + * 负责 **反思 (Reflexion)**:对用户的操作结果进行质量校验(如样本量是否合理)。 + * **升级点**:V3 版本中,大脑的思考逻辑(思维链)不再写死在代码里,而是从数据库加载配置。 +* 💾 **记忆 (Memory): ProtocolContext 表** + * 利用 Postgres 的 JSONB 存储“活的”方案数据。 + * **长程记忆**:不仅记住聊天记录,更记住结构化的 PICO、样本量 N 值、观察指标等关键状态,突破 LLM Token 限制。 +* 📚 **知识 (Knowledge): 自建 EKB (Postgres-Only)** + * 提供 **混合检索 (Hybrid Search)** 能力。 + * **结构化检索**:利用 SQL 精准筛选(如“找样本量\>500的RCT研究”)。 + * **语义检索**:利用 pgvector 进行向量相似度匹配。 +* 🛠️ **手脚 (Hands): Deep Link Action** + * 通过 **Action Card** 调用的现有 Web 工具(如样本量计算器、数据清洗工具)。 + * 实现“对话无法完成的复杂操作,跳转到专业工具完成,再把结果同步回来”的闭环。 + +### **1.2 总体架构拓扑 (V3)** + +引入了全新的 **LLM Ops 控制层**,支持运营人员在后台动态调整 Agent 的思考逻辑。 + +graph TD + User\[用户 (React UI)\] \<--\> Entry\[单一入口: Protocol Agent\] + + subgraph "前端交互 (Interaction)" + Chat\[流式对话\] + StatePanel\[实时状态看板\] + DeepLink\[Action Card\] + end + Entry \--\> Chat & StatePanel + + subgraph "后端引擎 (Orchestrator Engine)" + Loader\[配置加载器\] + Executor\[SOP 执行引擎\] + Reflector\[Reflexion 卫士\] + Tracer\[全链路追踪\] + end + + Chat \<--\> Executor + + subgraph "大脑配置中心 (LLM Ops)" + DB\_Prompt\[(Prompt & CoT 表)\] + DB\_Trace\[(Trace 日志表)\] + AdminUI\[运营管理后台\] + end + + AdminUI \--配置 SOP--\> DB\_Prompt + DB\_Prompt \--加载--\> Loader + Loader \--注入--\> Executor + Executor \--写入日志--\> Tracer + Tracer \--可视化--\> AdminUI + + subgraph "记忆与知识 (Memory & Knowledge)" + DB\_Ctx\[(Protocol Context)\] + DB\_EKB\[(Self-built RAG)\] + end + + Executor \<--\> DB\_Ctx & DB\_EKB + +## **2\. 核心机制:后端配置化思维链 (Backend CoT)** + +### **2.1 什么是“配置化 CoT”?** + +我们将 AI 的思考过程拆解为标准作业程序 (SOP),存储为 JSON 数组。这意味着您可以在后台增加一个检查步骤,而无需修改代码。 + +**数据库配置示例 (JSON):** + +\[ + { + "step": 1, + "key": "completeness\_check", + "desc": "检查 PICO 完整性", + "instruction": "检查用户输入是否包含 P, I, C, O 四个要素。如果缺失,标记为 MISSING。" + }, + { + "step": 2, + "key": "methodology\_match", + "desc": "方法学匹配", + "instruction": "根据研究目的推荐设计类型(RCT/队列)。" + }, + { + "step": 3, + "key": "response\_generation", + "desc": "生成回复", + "instruction": "综合以上分析,给出建议。如果 PICO 缺失,请追问。" + } +\] + +### **2.2 运行时表现 (Runtime Behavior)** + +Orchestrator 会将上述 JSON 动态编译为 System Prompt。AI 的输出将包含显式的 **XML 思考标签**: + +\ +用户提供了 P 和 I,但缺失 C (对照组) 和 O (结局)。 +\ + +\ +因缺失关键信息,暂无法确定设计类型,倾向于 RCT。 +\ + +\ +您好,为了帮您设计方案,我还需要了解:您的对照组是什么?主要结局指标是什么? +\ + +**前端展示:** 前端解析 \ 标签,将其放入可折叠的 **"深度思考"** 组件中,让用户看到 AI 的逻辑。 + +## **3\. 业务流程编排** + +### **3.1 完整交互闭环** + +1. **User**: 输入 "我要做阿司匹林的研究"。 +2. **Orchestrator**: + * 加载当前阶段 (SCIENTIFIC\_QUESTION) 的 Prompt 和 CoT 配置。 + * 执行 LLM 生成。 +3. **Reflexion (隐式)**: + * Orchestrator 后台运行微型 LLM 任务,尝试从对话中提取 PICO。 + * 如果提取成功,更新 ProtocolContext。 +4. **State Panel**: 前端通过 SSE 收到更新,右侧面板自动点亮 "P" 和 "I" 字段。 +5. **Plan**: + * Orchestrator 判断当前阶段已完成 (PICO 齐全)。 + * **主动推送**:生成 Action Card "建议进入下一步:选题评价"。 + +### **3.2 深度链接与反思 (Deep Link & Reflexion Loop)** + +当涉及到工具调用(如样本量计算)时: + +1. **Action**: AI 生成 Action Card (Deep Link)。 +2. **Execute**: 用户点击跳转 \-\> 工具页计算 \-\> 点击“同步”。 +3. **Reflexion Guard (后端强制校验)**: + * 数据回写到 DB 时,触发后端校验函数 reflexionGuard()。 + * **规则配置化**:(N \< 10 OR N \> 10000\) \-\> 报警。 + * **反馈**:如果校验失败,AI 主动发送消息:“⚠️ 样本量异常,请检查参数。” + +## **4\. 可观测性与调试 (Observability)** + +为了解决 "Prompt 难调" 的问题,V3 引入了 **Trace 机制**。 + +* **Trace ID**: 每次对话生成一个唯一 ID。 +* **Step Recording**: 记录 Router 决策、Prompt 组装结果、CoT 输出、工具返回。 +* **Replay**: 在 Admin 后台,可以一键重放某个 Trace,修改 Prompt 后重新运行,对比效果。 + +## **5\. 实施路线图 (Roadmap)** + +1. **Week 1: 数据层升级** + * 修改 prompt\_templates 表结构,增加 chain\_config。 + * 建立 agent\_traces 表。 +2. **Week 2: 引擎升级** + * 重构 Orchestrator,实现动态 CoT 组装逻辑。 + * 实现 XML 标签解析器。 +3. **Week 3: 前端适配** + * 升级 Chat 组件,支持渲染 XML 思考过程。 + * 完善右侧 State Panel 的实时同步。 \ No newline at end of file diff --git a/docs/03-业务模块/AIA-AI智能问答/02-技术设计/Protocol_Agent_Backend_Config_Design.md b/docs/03-业务模块/AIA-AI智能问答/02-技术设计/Protocol_Agent_Backend_Config_Design.md new file mode 100644 index 00000000..b42965b9 --- /dev/null +++ b/docs/03-业务模块/AIA-AI智能问答/02-技术设计/Protocol_Agent_Backend_Config_Design.md @@ -0,0 +1,116 @@ +# **Protocol Agent 后端配置架构设计 (V3.0 核心思路)** + +设计目标: 将硬编码的业务逻辑转化为可运营的配置数据。 +核心范式: 全局继承 \+ 阶段重写 \+ 组件化逻辑 + +## **1\. 配置层级概览 (The Configuration Hierarchy)** + +我们不再把 Prompt 看作一个长字符串,而是一个 **“动态组装对象”**。 + +graph TD + Global\[全局配置 (Global Config)\] \--\> Stage\[阶段配置 (Stage Config)\] + + subgraph "Level 1: 全局设定" + Global \--\> BasePrompt\[人设: 资深医学专家\] + Global \--\> ContextDef\[记忆结构: PICO/N值\] + Global \--\> GlobalTools\[通用工具: EKB检索\] + end + + subgraph "Level 2: 阶段设定 (覆盖 7 个模块)" + Stage \--\> TaskPrompt\[任务指令: 算样本量\] + Stage \--\> CoT\[思维链 SOP\] + Stage \--\> Reflexion\[反思卫士规则\] + Stage \--\> StageTools\[专用工具: 计算器\] + end + +## **2\. 详细配置清单 (后端都有哪些需要配置的?)** + +### **2.1 Level 1: 全局配置 (Global Configuration)** + +这是 Agent 的“底色”,在所有阶段都会生效。 + +| 配置项 | 说明 | 示例值 | +| :---- | :---- | :---- | +| **Agent Name** | 智能体名称 | 研究方案制定助手 (Protocol Agent) | +| **Base System Prompt** | **总体 Prompt** | "你是一个严谨的临床研究方法学专家。你的核心目标是辅助用户产出高质量的 Protocol。你必须依据循证医学证据说话..." | +| **Tone of Voice** | 语气风格 | 专业、客观、循循善诱(不要过于热情,保持学术严谨性) | +| **Global Knowledge** | 全局知识库挂载 | 关联 ekb\_schema (自建知识库),允许检索所有 Guideline 类型文档。 | +| **Memory Structure** | 记忆结构定义 | 定义 ProtocolContext 的 JSON Schema(告诉 AI 它能记住哪些字段,如 PICO)。 | + +### **2.2 Level 2: 阶段配置 (Stage Configuration)** + +这是 7 个具体模块的配置区。用户处于哪个阶段,Orchestrator 就加载哪份配置**覆盖**进 System Prompt。 + +| 配置项 | 说明 | 示例值 (以样本量计算为例) | +| :---- | :---- | :---- | +| **Stage ID** | 阶段标识 | SAMPLE\_SIZE\_CALC | +| **Stage Trigger** | 进入条件 (Router) | 当用户 PICO 完整 且 研究类型为 RCT 时。 | +| **Stage Instruction** | **阶段任务指令** | "当前任务是计算样本量。请根据用户提供的 Alpha/Power 参数,推荐合适的计算公式。" | +| **Specific Tools** | 专用工具绑定 | 绑定 tools/st/sample-size (Action Card)。 | +| **Output Schema** | 结构化提取目标 | 定义需要从对话中提取哪些字段存入 DB(如 n\_value, alpha)。 | + +### **2.3 Level 3: 逻辑与风控配置 (Logic & Reflexion)** + +这是您特别关心的 **“思维链”** 和 **“反思卫士”**。 + +#### **A. 思维链 (CoT) 配置** + +我们不再把思考过程写在 Prompt 文本里,而是配置为 **SOP 步骤列表**。 + +* **配置方式**:一个有序数组。 +* **配置内容**: + 1. **Step 1 \[Check\]**: 检查必要参数(Alpha, Power)是否齐全。 + 2. **Step 2 \[Match\]**: 匹配研究设计(是两样本均数比较,还是率的比较?)。 + 3. **Step 3 \[Recall\]**: 回忆/检索类似研究的 Effect Size 作为参考。 + 4. **Step 4 \[Action\]**: 决定是追问用户,还是生成 Action Card。 + +#### **B. 反思卫士 (Reflexion Guard) 配置** + +这是 V3 的核心升级。我们需要配置 **“什么样的情况是错误的”**。 + +可以分为两类配置: + +1. **Prompt-based Reflexion (软校验)**: + * 配置一段 Prompt 让 AI 自查:“请检查用户计算出的 N 值是否过小(\<20)?如果是,请给出伦理预警。” +2. **Rule-based Reflexion (硬校验 \- 高级)**: + * 配置 JSON 规则(后端解析执行): + { + "rules": \[ + { "field": "sample\_size.n", "operator": "\<", "value": 10, "msg": "样本量过小" }, + { "field": "sample\_size.power", "operator": "\<", "value": 0.8, "msg": "效能不足" } + \] + } + +## **3\. 系统运行时的组装逻辑** + +当用户发来一条消息时,后端 Orchestrator 是这样工作的: + +1. **加载全局**:取 Global.BasePrompt。 +2. **定位阶段**:根据当前 Context.stage (比如样本量),取 Stage.TaskPrompt。 +3. **注入 SOP**:取 Stage.CoT,编译成 XML 格式的思考指令。 + * Step 1: \...\ + * Step 2: \...\ +4. **挂载反思**:取 Stage.Reflexion,如果是 Prompt 类型的反思,追加到最后。 +5. **最终合成**: + \[Global Persona\] ... + \[Context Data\] ... + \[Stage Instruction\] ... + \[CoT Steps (SOP)\] ... + \[Reflexion Instructions\] ... + +## **4\. 总结:设计建议** + +**您现在的思路非常清晰:** + +1. **全局控制 (Overall Prompt)** 是必须的,它是 Agent 的“灵魂”,保证了无论在哪个阶段,AI 说话的方式都是一致的。 +2. **分阶段配置** 是“肉体”,负责具体的干活技能。 +3. **思维链 & 反思** 是“神经系统”,负责控制思考的深度和质量。 + +下一步建议: +我将在下一个回复中,为您设计 "后端配置中心" 的 UI 原型。 +这个原型将不再关注调试对话框,而是关注 "如何像搭积木一样配置上述的三层结构"。 +它应该包含: + +* **全局设置页**:配置 Agent 人设。 +* **流程编排页**:一个左侧导航(7个阶段),右侧配置(Prompt \+ CoT \+ Reflexion)的布局。 +* **SOP 编辑器**:专门用来拖拽/编辑思维链步骤的区域。 \ No newline at end of file diff --git a/docs/03-业务模块/AIA-AI智能问答/02-技术设计/Protocol_Agent_Technical_Implementation_V3.md b/docs/03-业务模块/AIA-AI智能问答/02-技术设计/Protocol_Agent_Technical_Implementation_V3.md new file mode 100644 index 00000000..7ea8d32a --- /dev/null +++ b/docs/03-业务模块/AIA-AI智能问答/02-技术设计/Protocol_Agent_Technical_Implementation_V3.md @@ -0,0 +1,244 @@ +# **研究方案制定 Agent 技术实现手册 V3.0** + +版本: v3.0 +关联文档: Protocol\_Agent\_Architecture\_Design\_V3.md +核心内容: 数据库 Schema、核心服务代码、API 协议。 + +## **1\. 数据库设计 (Prisma Schema)** + +### **1.1 记忆层 (Memory Layer)** + +存储“活的”方案数据,实现长程记忆。 + +// aia\_schema.prisma + +model ProtocolContext { + id String @id @default(uuid()) + conversationId String @unique @map("conversation\_id") + userId String @map("user\_id") + + // 状态机 + currentStage String @default("SCIENTIFIC\_QUESTION") + status String @default("IN\_PROGRESS") + + // 活的方案数据 (JSONB) + // 包含: scientific\_question, pico, study\_design, outcomes, sample\_size 等 + data Json @default("{}") + + // V3 新增: 存储上一次的反思结果,避免重复报错 + lastReflexion Json? @map("last\_reflexion") + + createdAt DateTime @default(now()) @map("created\_at") + updatedAt DateTime @updatedAt @map("updated\_at") + + @@table("protocol\_contexts") + @@schema("aia\_schema") +} + +### **1.2 配置层 (Configuration Layer)** + +这是 V3 的核心,支持后端配置 CoT。 + +// capability\_schema.prisma + +model PromptTemplate { + id String @id @default(uuid()) + code String @unique // e.g., AIA\_SAMPLE\_SIZE + version Int @default(1) + + // 基础人设 + content String @db.Text + + // V3 新增: 思维链配置 (JSONB) + // 存储 ChainStep\[\] 数组 + chainConfig Json? @map("chain\_config") + + // V3 新增: 路由规则配置 (JSONB, 可选) + // 用于定义何时切出该阶段 + routerConfig Json? @map("router\_config") + + isActive Boolean @default(true) + updatedAt DateTime @updatedAt + + @@table("prompt\_templates") + @@schema("capability\_schema") +} + +model AgentTrace { + id String @id @default(uuid()) + conversationId String @map("conversation\_id") + stage String + + // 记录完整上下文 + inputPayload Json @map("input\_payload") + outputPayload Json @map("output\_payload") + + // 耗时与消耗 + latencyMs Int @map("latency\_ms") + tokens Int @default(0) + + createdAt DateTime @default(now()) + + @@table("agent\_traces") + @@schema("capability\_schema") +} + +## **2\. 核心服务代码 (Orchestrator Engine)** + +### **2.1 动态 CoT 组装器 (promptBuilder.ts)** + +负责将 DB 中的 SOP 配置编译成 System Prompt。 + +import { prisma } from '@/common/db'; + +interface ChainStep { + key: string; + desc: string; + instruction: string; +} + +export async function buildDynamicSystemPrompt(stage: string, contextData: any) { + // 1\. 加载配置 + const template \= await prisma.promptTemplate.findUnique({ where: { code: stage } }); + if (\!template) throw new Error(\`Prompt not found: ${stage}\`); + + const chainSteps \= template.chainConfig as ChainStep\[\] || \[\]; + + // 2\. 组装基础 Prompt (Persona) + let systemPrompt \= \`${template.content}\\n\\n\`; + + // 3\. 注入当前上下文 (Memory) + systemPrompt \+= \`=== 当前方案状态 (Context) \===\\n${JSON.stringify(contextData, null, 2)}\\n\\n\`; + + // 4\. 注入思维链 SOP (V3 核心) + if (chainSteps.length \> 0\) { + systemPrompt \+= \`=== 思考步骤 (SOP) \===\\n\`; + systemPrompt \+= \`请严格按照以下步骤进行思考,并使用 XML 标签包裹每一步的内容:\\n\`; + chainSteps.forEach((step, idx) \=\> { + systemPrompt \+= \`${idx \+ 1}. \<${step.key}\>: ${step.instruction}\\n\`; + }); + systemPrompt \+= \`最后,在 \ 标签中输出给用户的回复。\\n\`; + } + + return systemPrompt; +} + +### **2.2 编排器主逻辑 (ProtocolOrchestrator.ts)** + +协调 Brain, Memory, Knowledge 和 Hands。 + +export class ProtocolOrchestrator { + + async handleMessage(userId: string, conversationId: string, content: string) { + // 1\. 获取上下文 (Memory) + let ctx \= await prisma.protocolContext.findUnique({ where: { conversationId } }); + + // 2\. \[Reflexion Guard\] 检查数据变更 + // 如果之前状态是 WAITING\_USER 且数据变了,说明工具执行回来了 (Hands 回调) + if (ctx.status \=== 'WAITING\_USER\_ACTION' && await this.checkDataChanged(ctx)) { + const validation \= await this.runReflexionCheck(ctx); + if (\!validation.pass) { + return this.streamResponse(\`⚠️ \*\*校验未通过\*\*: ${validation.reason}\`); + } + // 校验通过,重置状态 + await this.updateStatus(ctx.id, 'IN\_PROGRESS'); + } + + // 3\. \[Knowledge RAG\] 混合检索 (自建 EKB) + // 仅在需要知识的阶段调用 + const ragContext \= await knowledgeService.search(content, ctx.currentStage); + + // 4\. \[Prompt Build\] 动态组装 (Brain) + const systemPrompt \= await buildDynamicSystemPrompt(ctx.currentStage, ctx.data); + + // 5\. \[Execution\] 流式生成 + const traceId \= await traceService.startTrace(conversationId, 'GENERATE'); + + return streamingService.streamGenerate({ + systemPrompt, + userMessage: content \+ (ragContext ? \`\\n\\n参考资料:\\n${ragContext}\` : ''), + onFinish: async (output) \=\> { + await traceService.endTrace(traceId, output); + // 异步触发:结构化提取任务 (更新 Memory) + await pgBoss.send('extract-protocol-data', { conversationId, text: output }); + } + }); + } +} + +## **3\. 交互协议 (Deep Link Protocol)** + +### **3.1 Action Card Payload (Backend \-\> Frontend)** + +Orchestrator 决定需要用户操作时,发送此 JSON。 + +{ + "type": "action\_card", + "data": { + "title": "建议:样本量计算", + "tool\_code": "SAMPLE\_SIZE\_CALC", + "path": "/tools/st/sample-size", + "params": { + "ctx\_id": "uuid-1234", // 关键:传递上下文ID,让工具知道往哪里回写 + "alpha": 0.05, + "power": 0.8 + } + } +} + +### **3.2 同步回写 API (Tool \-\> Backend)** + +工具端(如样本量计算器)计算完成后,调用此接口回写结果。 + +**POST** /api/v1/aia/protocol/:ctxId/sync + +{ + "stage": "SAMPLE\_SIZE", + "data": { + "n\_total": 386, + "method": "t-test", + "params": { "alpha": 0.05, "power": 0.8 } + } +} + +## **4\. 提取与反思 (Extraction & Reflexion)** + +### **4.1 异步提取 Worker** + +负责从非结构化对话中“提纯”信息存入 Memory。 + +// workers/extractionWorker.ts + +export async function extractProtocolData(job) { + const { conversationId, text } \= job.data; + + // 调用廉价模型 (DeepSeek-Flash) 进行 JSON 提取 + const extracted \= await llm.extractJson(text, EXTRACT\_PROMPT); + + if (extracted) { + // 智能合并策略 (Deep Merge) + await prisma.protocolContext.update({ + where: { conversationId }, + data: { data: deepMerge(currentData, extracted) } + }); + } +} + +### **4.2 反思校验规则 (Reflexion Rules)** + +**Brain** 对 **Hands** 操作结果的质检逻辑。 + +function runReflexionCheck(ctx: ProtocolContext) { + const { sample\_size } \= ctx.data; + if (\!sample\_size) return { pass: true }; + + // 规则 1: 伦理红线 + if (sample\_size.n\_total \< 10\) + return { pass: false, reason: "样本量过小 (\<10),无法通过伦理审查" }; + + // 规则 2: 可行性预警 + if (sample\_size.n\_total \> 10000\) + return { pass: false, reason: "样本量过大,请确认经费和周期是否支持" }; + + return { pass: true }; +} diff --git a/docs/03-业务模块/AIA-AI智能问答/04-开发计划/03-前端组件设计.md b/docs/03-业务模块/AIA-AI智能问答/04-开发计划/03-前端组件设计.md index 332b6923..dfeeb880 100644 --- a/docs/03-业务模块/AIA-AI智能问答/04-开发计划/03-前端组件设计.md +++ b/docs/03-业务模块/AIA-AI智能问答/04-开发计划/03-前端组件设计.md @@ -894,3 +894,5 @@ export interface SlashCommand { + + diff --git a/docs/03-业务模块/AIA-AI智能问答/04-开发计划/04-Protocol_Agent开发计划/00-开发计划总览.md b/docs/03-业务模块/AIA-AI智能问答/04-开发计划/04-Protocol_Agent开发计划/00-开发计划总览.md new file mode 100644 index 00000000..c4b83616 --- /dev/null +++ b/docs/03-业务模块/AIA-AI智能问答/04-开发计划/04-Protocol_Agent开发计划/00-开发计划总览.md @@ -0,0 +1,183 @@ +# Protocol Agent 开发计划总览 + +> 版本:v1.0 +> 创建日期:2026-01-24 +> 状态:待开发 + +--- + +## 一、项目背景 + +Protocol Agent(研究方案制定助手)是AIA模块的第13个智能体入口,旨在通过多轮对话引导用户完成临床研究方案的核心要素制定。 + +### 1.1 核心价值 + +- **统一入口**:整合样本量计算、PICO提取等7个工具的能力 +- **长期记忆**:ProtocolContext持久化存储研究方案要素 +- **专业引导**:基于临床研究方法学的思维链引导 +- **平台基础**:为后续统计分析Agent、数据清洗Agent等提供可复用框架 + +### 1.2 设计哲学 + +``` +Code as Skeleton, Data as Brain +代码是骨架(确定性流程),数据是大脑(灵活配置) +``` + +--- + +## 二、决策确认记录 + +| 决策项 | 选择 | 说明 | +|--------|------|------| +| **入口方式** | B - 第13个入口 | Protocol Agent作为独立入口,12个现有智能体保留 | +| **状态流转** | **对话驱动** | 用户在对话中说"继续"或点击"同步"按钮,自然推进阶段 | +| **数据同步** | **内嵌同步按钮** | AI整理好数据后,在回复中显示"同步到方案"按钮 | +| **Action Card** | B - 规则触发 | 基于阶段和条件规则触发,非LLM决定 | +| **Reflexion** | B - Prompt-based | 使用Prompt模板进行质量检查(P2优先级) | +| **MVP范围** | 渐进式演进 | Phase 1聚焦核心流程,后续迭代增强 | +| **阶段数量** | **5个阶段** | 科学问题→PICO→研究设计→样本量→观察指标 | +| **完成功能** | **一键生成** | 5阶段完成后,支持一键生成研究方案并下载Word | + +--- + +## 三、文档索引 + +本开发计划包含以下文档: + +| 序号 | 文档名称 | 内容说明 | +|------|----------|----------| +| 00 | 开发计划总览.md | 本文档,项目概述与决策记录 | +| 01 | 架构设计.md | 五层Agent架构、组件职责、执行流程 | +| 02 | 数据库设计.md | 完整Prisma Schema定义 | +| 03 | 代码结构设计.md | 目录结构、核心接口、类型定义 | +| 04 | 分阶段实施计划.md | Sprint划分、任务列表、里程碑 | + +--- + +## 四、核心架构概览 + +### 4.1 核心交互模式:对话驱动 + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ 对话驱动交互模式 │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ 【用户与AI对话】 │ +│ ↓ │ +│ 【AI收集信息,整理数据】 │ +│ ↓ │ +│ 【AI回复中显示 "✅ 同步到方案" 按钮】 │ +│ ↓ │ +│ 【用户点击同步】→ 数据写入Context → State Panel更新 │ +│ ↓ │ +│ 【AI询问:继续下一阶段吗?】 │ +│ ↓ │ +│ 【用户说"继续"】→ AI识别意图 → 自动进入下一阶段 │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### 4.2 五层架构 + +``` +┌─────────────────────────────────────────────────────────┐ +│ 1. Query Layer 意图识别、实体提取、上下文消歧 │ +├─────────────────────────────────────────────────────────┤ +│ 2. Planner Layer 状态机、策略选择、任务分解 │ +├─────────────────────────────────────────────────────────┤ +│ 3. Executor Layer LLM调用、工具调用、响应构建 │ +├─────────────────────────────────────────────────────────┤ +│ 4. Tools Layer 内部工具、外部工具、RAG、Deep Link │ +├─────────────────────────────────────────────────────────┤ +│ 5. Reflection Layer 质量检查、一致性校验、恢复处理 │ +└─────────────────────────────────────────────────────────┘ +``` + +### 4.3 三层职责划分 + +| 层次 | 职责 | 实现方式 | +|------|------|----------| +| **代码层** | 状态流转、数据存储、API调用、错误处理 | TypeScript代码 | +| **Prompt层** | 意图识别、对话生成、信息提取、质量检查 | Prompt模板 + LLM | +| **工具层** | Deep Link、外部API、知识库检索 | 工具注册 + 调用 | + +--- + +## 五、阶段规划 + +### Phase 1:MVP完整功能(4周) + +**目标**:完成Agent框架 + 5阶段对话 + 一键生成研究方案 + +- ✅ 通用Agent框架搭建 +- ✅ 5个核心阶段(科学问题→PICO→研究设计→样本量→观察指标) +- ✅ 对话驱动 + 同步按钮交互模式 +- ✅ ProtocolContext持久化 +- ✅ State Panel前端展示 +- ✅ **一键生成研究方案** +- ✅ **Word文档导出** + +### Phase 2:知识增强(3周) + +**目标**:集成专家知识库,提升专业深度 + +- 🔲 EKB知识库建设 +- 🔲 RAG检索集成 +- 🔲 Function Calling工具调用 +- 🔲 高级Reflexion质量检查 + +### Phase 3:平台化(2周) + +**目标**:配置化、可观测性、多Agent支持 + +- 🔲 后台配置管理界面 +- 🔲 Prompt在线调试 +- 🔲 完整Trace分析 +- 🔲 统计分析Agent接入准备 + +--- + +## 六、技术栈 + +| 类别 | 技术选型 | 说明 | +|------|----------|------| +| **后端框架** | Fastify v4 + TypeScript | 现有架构 | +| **数据库** | PostgreSQL 15 + Prisma 6 | 现有架构 | +| **LLM** | DeepSeek-V3 (主) / GPT-4 (备) | 通过LLM Gateway | +| **前端框架** | React 19 + Ant Design 6 | 现有架构 | +| **状态管理** | Zustand | 轻量级状态管理 | + +--- + +## 七、风险与缓解 + +| 风险 | 影响 | 缓解措施 | +|------|------|----------| +| LLM幻觉 | 生成不准确的医学信息 | Reflexion检查 + 知识库约束 | +| 上下文丢失 | 长对话信息遗忘 | ProtocolContext持久化 + 摘要策略 | +| 提取不准确 | Context数据错误 | 置信度标记 + 用户确认 | +| Prompt调优耗时 | 延迟交付 | 预留充足调优时间 | + +--- + +## 八、成功标准 + +### Phase 1 交付标准 + +1. **功能完整性**:用户可完成5阶段研究方案要素制定 +2. **数据准确性**:Context提取准确率 > 85% +3. **用户体验**:单阶段平均对话轮数 < 5轮 +4. **系统稳定性**:API响应成功率 > 99% + +--- + +## 九、相关文档 + +- [Protocol Agent PRD v1.0](../00-系统设计/Protocol_Agent_PRD_v1.0.md) +- [MVP简化指南](../00-系统设计/Protocol_Agent_Development_Simplification_Guide.md) +- [架构设计V3](../02-技术设计/Protocol_Agent_Architecture_Design_V3.md) +- [技术实现V3](../02-技术设计/Protocol_Agent_Technical_Implementation_V3.md) +- [后端配置设计](../02-技术设计/Protocol_Agent_Backend_Config_Design.md) + diff --git a/docs/03-业务模块/AIA-AI智能问答/04-开发计划/04-Protocol_Agent开发计划/01-架构设计.md b/docs/03-业务模块/AIA-AI智能问答/04-开发计划/04-Protocol_Agent开发计划/01-架构设计.md new file mode 100644 index 00000000..97a192af --- /dev/null +++ b/docs/03-业务模块/AIA-AI智能问答/04-开发计划/04-Protocol_Agent开发计划/01-架构设计.md @@ -0,0 +1,498 @@ +# Protocol Agent 架构设计 + +> 版本:v1.0 +> 创建日期:2026-01-24 + +--- + +## 一、整体架构图 + +``` +┌────────────────────────────────────────────────────────────────────────────┐ +│ Protocol Agent 架构 │ +├────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ 用户输入 │ +│ │ │ +│ ▼ │ +│ ┌────────────────────────────────────────────────────────────────────┐ │ +│ │ 1. Query Layer (意图识别层) │ │ +│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ +│ │ │ Intent │ │ Entity │ │ Context │ │ │ +│ │ │ Classifier │ │ Extractor │ │ Resolver │ │ │ +│ │ │ (意图分类) │ │ (实体提取) │ │ (上下文消歧) │ │ │ +│ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ +│ └────────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌────────────────────────────────────────────────────────────────────┐ │ +│ │ 2. Planner Layer (规划层) │ │ +│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ +│ │ │ Stage │ │ Strategy │ │ Task │ │ │ +│ │ │ Machine │ │ Selector │ │ Decomposer │ │ │ +│ │ │ (状态机) │ │ (策略选择) │ │ (任务分解) │ │ │ +│ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ +│ └────────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌────────────────────────────────────────────────────────────────────┐ │ +│ │ 3. Executor Layer (执行层) │ │ +│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ +│ │ │ LLM │ │ Tool │ │ Response │ │ │ +│ │ │ Invoker │ │ Invoker │ │ Builder │ │ │ +│ │ │ (LLM调用) │ │ (工具调用) │ │ (响应构建) │ │ │ +│ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ +│ └────────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌────────────────────────────────────────────────────────────────────┐ │ +│ │ 4. Tools Layer (工具层) │ │ +│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ +│ │ │ Internal │ │ External │ │ Deep Link │ │ │ +│ │ │ Tools │ │ Tools │ │ Generator │ │ │ +│ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ +│ │ ┌──────────────┐ ┌──────────────┐ │ │ +│ │ │ RAG │ │ Knowledge │ ← Phase 2 │ │ +│ │ │ Retriever │ │ Base │ │ │ +│ │ └──────────────┘ └──────────────┘ │ │ +│ └────────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌────────────────────────────────────────────────────────────────────┐ │ +│ │ 5. Reflection Layer (反思层) │ │ +│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ +│ │ │ Quality │ │ Consistency │ │ Recovery │ │ │ +│ │ │ Checker │ │ Validator │ │ Handler │ │ │ +│ │ │ (质量检查) │ │ (一致性校验) │ │ (恢复处理) │ │ │ +│ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ +│ └────────────────────────────────────────────────────────────────────┘ │ +│ │ +│ ═══════════════════════════════════════════════════════════════════════ │ +│ │ +│ ┌────────────────────────────────────────────────────────────────────┐ │ +│ │ 横切关注点 (Cross-Cutting) │ │ +│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ +│ │ │ Memory │ │ Config │ │ Trace │ │ Prompt │ │ Event │ │ │ +│ │ │ Manager │ │ Manager │ │ Logger │ │ Manager │ │ Bus │ │ │ +│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │ +│ └────────────────────────────────────────────────────────────────────┘ │ +│ │ +└────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 二、各层详细设计 + +### 2.1 Query Layer(意图识别层) + +**职责**:理解用户输入,识别意图,提取实体 + +| 组件 | 功能 | 实现方式 | Phase | +|------|------|----------|-------| +| IntentClassifier | 识别用户意图类型 | Prompt + LLM | P1 | +| EntityExtractor | 从输入提取关键实体 | Prompt + LLM | P1 | +| ContextResolver | 结合历史消息消歧 | Prompt + LLM | P2 | + +**意图类型定义**: + +```typescript +type IntentType = + | 'provide_info' // 提供信息(主流程) + | 'ask_question' // 询问问题 + | 'request_change' // 请求修改已有数据 + | 'confirm_complete' // 确认阶段完成 + | 'navigate_stage' // 跳转阶段 + | 'request_tool' // 请求使用工具 + | 'chitchat' // 闲聊 + | 'unclear'; // 不明确 +``` + +**Query分析Prompt模板**: + +``` +分析用户消息的意图。当前处于"{{currentStage}}"阶段。 + +用户消息: "{{userMessage}}" + +已有上下文: +{{contextSummary}} + +请判断: +1. 意图类型: [provide_info | ask_question | request_change | confirm_complete | navigate_stage | request_tool | chitchat | unclear] +2. 置信度: 0-1 +3. 提取的实体: [{type, value, confidence}] +4. 是否需要澄清 + +以JSON格式返回。 +``` + +### 2.2 Planner Layer(规划层) + +**职责**:基于意图决定执行策略,管理状态流转 + +| 组件 | 功能 | 实现方式 | Phase | +|------|------|----------|-------| +| StageMachine | 管理阶段状态流转 | 代码 + 配置 | P1 | +| StrategySelector | 选择执行策略 | 规则 + 配置 | P1 | +| TaskDecomposer | 复杂任务分解 | LLM | P3 | + +**状态机设计**: + +``` +┌──────────────┐ 确认 ┌──────────────┐ 确认 ┌──────────────┐ +│ 科学问题 │ ─────────→ │ PICO │ ─────────→ │ 研究设计 │ +│ scientific_q │ │ pico │ │ study_design │ +└──────────────┘ └──────────────┘ └──────────────┘ + │ + │ 确认 + ▼ +┌──────────────┐ 确认 ┌──────────────┐ +│ 终点指标 │ ←───────── │ 样本量 │ +│ endpoints │ │ sample_size │ +└──────────────┘ └──────────────┘ + │ + │ 确认 + ▼ + ┌──────────┐ + │ 完成 │ + │ complete │ + └──────────┘ +``` + +**执行策略**: + +```typescript +type ExecutionStrategy = + | 'guide_and_extract' // 引导对话并提取数据 + | 'answer_question' // 回答用户问题 + | 'update_context' // 更新已有数据 + | 'trigger_tool' // 触发工具调用 + | 'transition_stage' // 执行阶段流转 + | 'request_clarification' // 请求澄清 + | 'handle_chitchat'; // 处理闲聊 +``` + +**策略选择规则**: + +```typescript +function selectStrategy(intent: IntentType, stage: string): ExecutionStrategy { + const rules = { + 'provide_info': 'guide_and_extract', + 'ask_question': 'answer_question', + 'request_change': 'update_context', + 'confirm_complete': 'transition_stage', + 'request_tool': 'trigger_tool', + 'chitchat': 'handle_chitchat', + 'unclear': 'request_clarification' + }; + return rules[intent]; +} +``` + +### 2.3 Executor Layer(执行层) + +**职责**:执行具体任务,调用LLM和工具 + +| 组件 | 功能 | 实现方式 | Phase | +|------|------|----------|-------| +| LLMInvoker | 调用LLM生成响应 | 代码 | P1 | +| ToolInvoker | 调用注册的工具 | 代码 | P1 | +| ResponseBuilder | 构建最终响应 | 代码 | P1 | + +**LLM调用流程**: + +``` +1. PromptBuilder.buildSystemPrompt() → 构建System Prompt +2. MemoryManager.getHistory() → 获取对话历史 +3. LLMGateway.streamChat() → 流式调用LLM +4. ResponseParser.parse() → 解析响应 +5. DataExtractor.extract() → 提取结构化数据 +``` + +### 2.4 Tools Layer(工具层) + +**职责**:提供各类工具能力 + +| 组件 | 功能 | 实现方式 | Phase | +|------|------|----------|-------| +| InternalTools | 内部计算工具 | 代码 | P1 | +| DeepLinkGenerator | 生成跳转链接 | 代码 | P1 | +| ExternalTools | 外部API工具 | 代码 | P2 | +| RAGRetriever | RAG知识检索 | RAG引擎 | P2 | + +**工具注册格式**: + +```typescript +interface ToolDefinition { + toolId: string; + toolName: string; + description: string; // 给LLM看的描述 + toolType: 'internal' | 'external' | 'deep_link' | 'rag'; + + // Function Calling 格式(Phase 2) + functionSchema?: { + name: string; + description: string; + parameters: JSONSchema; + }; + + // 执行配置 + handler: (params: any) => Promise; +} +``` + +**Deep Link Action Card**: + +```typescript +interface ActionCard { + cardId: string; + title: string; + description: string; + triggerType: 'stage_enter' | 'condition' | 'manual'; + actionType: 'deep_link' | 'api_call' | 'modal'; + + // Deep Link配置 + deepLinkConfig?: { + targetAgent: string; // 目标Agent ID + targetPath: string; // 目标路径 + params: Record; // 参数映射 + onComplete: 'update_context' | 'notify' | 'none'; + }; +} +``` + +### 2.5 Reflection Layer(反思层) + +**职责**:质量保障,一致性检查 + +| 组件 | 功能 | 实现方式 | Phase | +|------|------|----------|-------| +| QualityChecker | 检查输出质量 | Prompt + LLM | P1 | +| ConsistencyValidator | 检查数据一致性 | 规则 + LLM | P2 | +| RecoveryHandler | 错误恢复处理 | 代码 | P2 | + +**Reflexion规则类型**: + +```typescript +type ReflexionRuleType = + | 'prompt_based' // Prompt驱动检查 + | 'rule_based' // 规则驱动检查 + | 'hybrid'; // 混合检查 + +interface ReflexionRule { + ruleId: string; + triggerStage: string; // 触发阶段 + triggerTiming: 'on_extract' | 'on_complete' | 'on_transition'; + ruleType: ReflexionRuleType; + + // Prompt-based + promptTemplate?: string; + + // Rule-based + ruleLogic?: { + field: string; + condition: string; + expectedValue: any; + }; + + severity: 'error' | 'warning' | 'info'; + failureAction: 'block' | 'warn' | 'log'; +} +``` + +**Reflexion Prompt模板示例**: + +``` +检查研究方案的PICO要素是否完整和一致。 + +当前PICO数据: +{{picoData}} + +科学问题: +{{scientificQuestion}} + +请检查: +1. P/I/C/O四个要素是否都已定义 +2. 各要素是否与科学问题一致 +3. 是否存在逻辑矛盾 +4. 是否符合临床研究规范 + +返回JSON格式: +{ + "passed": boolean, + "issues": string[], + "suggestions": string[] +} +``` + +--- + +## 三、横切关注点 + +### 3.1 Memory Manager(记忆管理) + +```typescript +interface MemoryManager { + // 短期记忆:对话历史 + getConversationHistory(conversationId: string, limit?: number): Message[]; + + // 中期记忆:会话上下文 + getSessionContext(sessionId: string): AgentSession; + + // 长期记忆:Protocol数据 + getProtocolContext(conversationId: string): ProtocolContext; + + // 记忆摘要(长对话优化) + summarizeHistory(messages: Message[]): string; +} +``` + +### 3.2 Config Manager(配置管理) + +```typescript +interface ConfigManager { + // 加载Agent配置 + getAgentConfig(agentId: string): AgentConfig; + + // 加载阶段配置 + getStageConfig(agentId: string, stageId: string): AgentStage; + + // 加载工具配置 + getToolConfig(toolId: string): AgentTool; + + // 热更新支持(Phase 3) + reloadConfig(agentId: string): void; +} +``` + +### 3.3 Trace Logger(追踪日志) + +```typescript +interface TraceLogger { + // 开始追踪 + startTrace(conversationId: string): string; // 返回traceId + + // 记录步骤 + logStep(traceId: string, step: TraceStep): void; + + // 结束追踪 + endTrace(traceId: string, result: 'success' | 'error'): void; + + // 查询追踪(调试用) + getTrace(traceId: string): AgentTrace[]; +} + +interface TraceStep { + stepType: 'query_analysis' | 'planning' | 'execution' | 'tool_call' | 'reflexion'; + stepInput: any; + stepOutput: any; + durationMs: number; + llmTokens?: { prompt: number; completion: number }; +} +``` + +--- + +## 四、完整执行流程 + +``` +用户发送消息 + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ Step 1: 初始化 │ +│ • 生成 traceId │ +│ • 加载 AgentConfig, StageConfig │ +│ • 加载 ProtocolContext │ +│ • 获取对话历史 │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ Step 2: Query Analysis │ +│ • IntentClassifier.classify(userMessage, context) │ +│ • EntityExtractor.extract(userMessage) │ +│ • ContextResolver.resolve(userMessage, history) │ +│ → 输出: QueryAnalysisResult │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ Step 3: Planning │ +│ • StageMachine.getCurrentState() │ +│ • StrategySelector.select(intent, stage) │ +│ • 确定需要调用的工具 │ +│ → 输出: ExecutionPlan │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ Step 4: Execution │ +│ • PromptBuilder.build(config, context, plan) │ +│ • LLMInvoker.invoke(systemPrompt, history, userMessage) │ +│ • 如需调用工具: ToolInvoker.invoke(toolId, params) │ +│ • ResponseBuilder.build(llmOutput, toolResults) │ +│ → 输出: ExecutionResult │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ Step 5: Data Extraction │ +│ • 从LLM输出提取结构化数据 │ +│ • 验证数据格式 │ +│ • 计算置信度 │ +│ → 输出: ExtractedData │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ Step 6: Reflection (可选) │ +│ • QualityChecker.check(response) │ +│ • ConsistencyValidator.validate(extractedData, context) │ +│ • 如失败: RecoveryHandler.handle() │ +│ → 输出: ReflexionResult │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ Step 7: Finalize │ +│ • 更新 ProtocolContext │ +│ • 保存 Message │ +│ • 评估 Action Cards │ +│ • 记录 Trace │ +│ → 返回响应给用户 │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 五、框架复用设计 + +该框架设计为可复用,未来Agent可直接继承: + +```typescript +// 基类 +abstract class BaseAgentOrchestrator { + abstract getAgentId(): string; + abstract getContextSchema(): TContext; + + async handleMessage(conversationId: string, message: string): Promise { + // 通用流程实现 + } +} + +// Protocol Agent +class ProtocolAgentOrchestrator extends BaseAgentOrchestrator { + getAgentId() { return 'protocol_agent'; } + getContextSchema() { return ProtocolContextSchema; } +} + +// 未来:Statistics Agent +class StatisticsAgentOrchestrator extends BaseAgentOrchestrator { + getAgentId() { return 'statistics_agent'; } + getContextSchema() { return StatisticsContextSchema; } +} +``` + + diff --git a/docs/03-业务模块/AIA-AI智能问答/04-开发计划/04-Protocol_Agent开发计划/02-数据库设计.md b/docs/03-业务模块/AIA-AI智能问答/04-开发计划/04-Protocol_Agent开发计划/02-数据库设计.md new file mode 100644 index 00000000..e218b3f3 --- /dev/null +++ b/docs/03-业务模块/AIA-AI智能问答/04-开发计划/04-Protocol_Agent开发计划/02-数据库设计.md @@ -0,0 +1,846 @@ +# Protocol Agent 数据库设计 + +> 版本:v1.0 +> 创建日期:2026-01-24 + +--- + +## 一、Schema规划 + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ 数据库Schema架构 │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ agent_schema (通用Agent框架 - 可复用) │ +│ ├── agent_configs Agent配置表 │ +│ ├── agent_stages 阶段配置表 │ +│ ├── agent_tools 工具注册表 │ +│ ├── action_cards Action Card配置表 │ +│ ├── reflexion_rules 反思规则表 │ +│ ├── agent_sessions Agent会话表 │ +│ └── agent_traces 追踪日志表 │ +│ │ +│ protocol_schema (研究方案Agent专用) │ +│ ├── protocol_contexts 研究方案上下文表 │ +│ ├── protocol_versions 版本历史表 │ +│ └── protocol_exports 导出记录表 │ +│ │ +│ knowledge_schema (知识库 - Phase 2) │ +│ ├── knowledge_docs 知识文档表 │ +│ ├── knowledge_chunks 知识块表 │ +│ └── knowledge_embeddings 向量嵌入表 │ +│ │ +│ aia_schema (已有 - 复用) │ +│ ├── conversations 对话表 │ +│ └── messages 消息表 │ +│ │ +│ capability_schema (已有 - 复用) │ +│ └── prompt_templates Prompt模板表 │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 二、agent_schema - 通用Agent框架表 + +### 2.1 agent_configs (Agent配置表) + +```prisma +/// Agent配置表 - 定义每个Agent的基础信息 +model AgentConfig { + id String @id @default(uuid()) + agentId String @unique @map("agent_id") + agentName String @map("agent_name") + agentDescription String? @map("agent_description") + baseSystemPrompt String @map("base_system_prompt") @db.Text + + // 全局配置 + toneOfVoice String? @map("tone_of_voice") + globalKnowledge String[] @map("global_knowledge") + memoryStrategy String @default("full") @map("memory_strategy") + maxHistoryTurns Int @default(20) @map("max_history_turns") + + // LLM配置 + defaultModel String @default("deepseek-v3") @map("default_model") + temperature Float @default(0.7) + maxTokens Int @default(4096) @map("max_tokens") + + // 状态 + isActive Boolean @default(true) @map("is_active") + version Int @default(1) + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + // 关联 + stages AgentStage[] @relation("AgentConfigStages") + tools AgentTool[] @relation("AgentConfigTools") + reflexionRules ReflexionRule[] @relation("AgentConfigReflexion") + + @@index([agentId, isActive]) + @@map("agent_configs") + @@schema("agent_schema") +} +``` + +**字段说明**: + +| 字段 | 类型 | 说明 | +|------|------|------| +| agentId | string | Agent唯一标识,如 `protocol_agent` | +| baseSystemPrompt | text | 基础系统Prompt | +| toneOfVoice | string | 语气风格,如"专业但亲和" | +| memoryStrategy | string | 记忆策略:full/sliding_window/summary | +| maxHistoryTurns | int | 最大保留对话轮数 | + +### 2.2 agent_stages (阶段配置表) + +```prisma +/// 阶段配置表 - 定义Agent的各个阶段 +model AgentStage { + id String @id @default(uuid()) + agentConfigId String @map("agent_config_id") + + stageId String @map("stage_id") + stageName String @map("stage_name") + stageOrder Int @map("stage_order") + + // Prompt配置 + instruction String @db.Text + extractionSchema Json @map("extraction_schema") + completionCriteria String? @map("completion_criteria") @db.Text + + // 流转配置 + nextStageId String? @map("next_stage_id") + transitionMode String @default("user_confirm") @map("transition_mode") + transitionCondition Json? @map("transition_condition") + + // 工具配置 + availableToolIds String[] @map("available_tool_ids") + + isActive Boolean @default(true) @map("is_active") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + agentConfig AgentConfig @relation("AgentConfigStages", fields: [agentConfigId], references: [id], onDelete: Cascade) + actionCards ActionCard[] @relation("StageActionCards") + + @@unique([agentConfigId, stageId]) + @@index([agentConfigId, stageOrder]) + @@map("agent_stages") + @@schema("agent_schema") +} +``` + +**extractionSchema 示例** (PICO阶段): + +```json +{ + "type": "object", + "properties": { + "P": { + "type": "object", + "properties": { + "value": { "type": "string", "description": "研究人群" }, + "details": { "type": "string" }, + "confidence": { "type": "number" } + } + }, + "I": { ... }, + "C": { ... }, + "O": { ... } + } +} +``` + +### 2.3 agent_tools (工具注册表) + +```prisma +/// 工具注册表 - 定义Agent可用的工具 +model AgentTool { + id String @id @default(uuid()) + agentConfigId String? @map("agent_config_id") + + toolId String @unique @map("tool_id") + toolName String @map("tool_name") + toolDescription String @map("tool_description") @db.Text + toolType String @map("tool_type") + + // Function定义 + functionSchema Json? @map("function_schema") + + // 执行配置 + handlerType String @map("handler_type") + handlerConfig Json @map("handler_config") + + // 权限和限制 + requiresAuth Boolean @default(false) @map("requires_auth") + rateLimit Int? @map("rate_limit") + timeout Int @default(30000) + + isActive Boolean @default(true) @map("is_active") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + agentConfig AgentConfig? @relation("AgentConfigTools", fields: [agentConfigId], references: [id], onDelete: Cascade) + + @@index([agentConfigId, isActive]) + @@index([toolType]) + @@map("agent_tools") + @@schema("agent_schema") +} +``` + +**toolType 枚举值**: +- `internal`: 内部计算工具 +- `external`: 外部API工具 +- `deep_link`: 深度链接跳转 +- `rag`: RAG知识检索 + +### 2.4 action_cards (Action Card配置表) + +```prisma +/// Action Card配置表 +model ActionCard { + id String @id @default(uuid()) + stageId String @map("stage_id") + + cardId String @map("card_id") + title String + description String? @db.Text + iconType String? @map("icon_type") + + // 触发配置 + triggerType String @map("trigger_type") + triggerCondition Json? @map("trigger_condition") + + // Action配置 + actionType String @map("action_type") + actionPayload Json @map("action_payload") + + // 回调配置 + onCompleteAction String? @map("on_complete_action") + resultMapping Json? @map("result_mapping") + + displayOrder Int @default(0) @map("display_order") + isActive Boolean @default(true) @map("is_active") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + stage AgentStage @relation("StageActionCards", fields: [stageId], references: [id], onDelete: Cascade) + + @@unique([stageId, cardId]) + @@map("action_cards") + @@schema("agent_schema") +} +``` + +**actionPayload 示例** (Deep Link): + +```json +{ + "targetAgent": "sample_calculator", + "targetPath": "/aia/sample-calculator", + "params": { + "studyType": "{{studyDesign.type}}", + "primaryEndpoint": "{{endpoints.primary[0].name}}" + }, + "openMode": "modal" +} +``` + +### 2.5 reflexion_rules (反思规则表) + +```prisma +/// Reflexion规则表 +model ReflexionRule { + id String @id @default(uuid()) + agentConfigId String @map("agent_config_id") + + ruleId String @map("rule_id") + ruleName String @map("rule_name") + ruleDescription String? @map("rule_description") @db.Text + + // 触发配置 + triggerStageId String? @map("trigger_stage_id") + triggerTiming String @map("trigger_timing") + + // 规则内容 + ruleType String @map("rule_type") + promptTemplate String? @map("prompt_template") @db.Text + ruleLogic Json? @map("rule_logic") + + // 失败处理 + severity String @default("warning") + failureAction String @map("failure_action") + + isActive Boolean @default(true) @map("is_active") + priority Int @default(0) + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + agentConfig AgentConfig @relation("AgentConfigReflexion", fields: [agentConfigId], references: [id], onDelete: Cascade) + + @@unique([agentConfigId, ruleId]) + @@index([triggerStageId, triggerTiming]) + @@map("reflexion_rules") + @@schema("agent_schema") +} +``` + +### 2.6 agent_sessions (Agent会话表) + +```prisma +/// Agent会话表 - 扩展conversation的Agent状态 +model AgentSession { + id String @id @default(uuid()) + conversationId String @unique @map("conversation_id") + agentId String @map("agent_id") + userId String @map("user_id") + + // 状态管理 + currentStageId String @map("current_stage_id") + stageStatus String @default("in_progress") @map("stage_status") + + // 元数据 + sessionMetadata Json? @map("session_metadata") + + // 生命周期 + startedAt DateTime @default(now()) @map("started_at") + lastActiveAt DateTime @default(now()) @map("last_active_at") + completedAt DateTime? @map("completed_at") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@index([agentId, userId]) + @@index([currentStageId]) + @@index([lastActiveAt]) + @@map("agent_sessions") + @@schema("agent_schema") +} +``` + +### 2.7 agent_traces (追踪日志表) + +```prisma +/// Agent追踪日志表 - 可观测性 +model AgentTrace { + id String @id @default(uuid()) + conversationId String @map("conversation_id") + messageId String? @map("message_id") + traceId String @map("trace_id") + + // 步骤信息 + stepType String @map("step_type") + stepOrder Int @map("step_order") + + // 输入输出 + stepInput Json? @map("step_input") + stepOutput Json? @map("step_output") + + // LLM信息 + llmModel String? @map("llm_model") + llmPromptTokens Int? @map("llm_prompt_tokens") + llmCompletionTokens Int? @map("llm_completion_tokens") + + // 性能 + durationMs Int? @map("duration_ms") + + // 状态 + status String @default("success") + errorMessage String? @map("error_message") @db.Text + + createdAt DateTime @default(now()) @map("created_at") + + @@index([conversationId, traceId]) + @@index([traceId, stepOrder]) + @@index([createdAt]) + @@index([stepType, status]) + @@map("agent_traces") + @@schema("agent_schema") +} +``` + +--- + +## 三、protocol_schema - 研究方案专用表 + +### 3.1 protocol_contexts (研究方案上下文表) + +```prisma +/// Protocol上下文表 - 研究方案的结构化数据存储 +model ProtocolContext { + id String @id @default(uuid()) + sessionId String @unique @map("session_id") + conversationId String @unique @map("conversation_id") + userId String @map("user_id") + + // ===== Phase 1 核心字段 ===== + + /// 阶段1:科学问题 + scientificQuestion Json? @map("scientific_question") @db.JsonB + + /// 阶段2:PICO + pico Json? @db.JsonB + + /// 阶段3:研究设计 + studyDesign Json? @map("study_design") @db.JsonB + + /// 阶段4:样本量 + sampleSize Json? @map("sample_size") @db.JsonB + + /// 阶段5:终点指标 + endpoints Json? @db.JsonB + + // ===== Phase 2 扩展字段 ===== + + /// 入选标准 + inclusionCriteria Json? @map("inclusion_criteria") @db.JsonB + + /// 排除标准 + exclusionCriteria Json? @map("exclusion_criteria") @db.JsonB + + /// 统计分析计划 + statisticalPlan Json? @map("statistical_plan") @db.JsonB + + // ===== Phase 3 扩展字段 ===== + + /// 伦理考量 + ethicsConsiderations Json? @map("ethics_considerations") @db.JsonB + + /// 研究时间线 + timeline Json? @db.JsonB + + /// 预算估算 + budget Json? @db.JsonB + + // ===== 元数据 ===== + + /// 总体进度 0-100 + overallProgress Float @default(0) @map("overall_progress") + + /// 已完成阶段 + completedStages String[] @map("completed_stages") + + /// 版本号 + version Int @default(1) + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + versions ProtocolVersion[] @relation("ProtocolContextVersions") + + @@index([userId]) + @@index([conversationId]) + @@map("protocol_contexts") + @@schema("protocol_schema") +} +``` + +**各字段JSON结构**: + +#### scientificQuestion + +```json +{ + "content": "探究新型降糖药X对2型糖尿病患者的降糖效果及安全性", + "clinicalBackground": "2型糖尿病患病率逐年上升...", + "researchGap": "目前缺乏针对中国人群的大规模临床研究", + "significance": "填补该领域空白,为临床用药提供依据", + "confidence": 0.95, + "sourceMessageIds": ["msg_123", "msg_125"], + "lastUpdated": "2026-01-24T10:30:00Z" +} +``` + +#### pico + +```json +{ + "P": { + "value": "2型糖尿病患者", + "details": "年龄18-70岁,HbA1c 7.0-10.0%", + "confidence": 0.9 + }, + "I": { + "value": "新型降糖药X", + "details": "口服,每日一次,剂量10mg", + "confidence": 0.95 + }, + "C": { + "value": "二甲双胍", + "details": "口服,每日两次,剂量500mg", + "confidence": 0.88 + }, + "O": { + "value": "HbA1c变化", + "details": "治疗12周后HbA1c相对基线的变化值", + "confidence": 0.92 + }, + "sourceMessageIds": ["msg_130", "msg_132"], + "lastUpdated": "2026-01-24T11:00:00Z" +} +``` + +#### studyDesign + +```json +{ + "type": "RCT", + "phase": "III", + "blinding": "双盲", + "randomization": "中心随机", + "allocationRatio": "1:1", + "controlType": "阳性对照", + "multiCenter": true, + "centerCount": 20, + "duration": "12周治疗期 + 4周随访期", + "confidence": 0.9, + "sourceMessageIds": ["msg_140"], + "lastUpdated": "2026-01-24T11:30:00Z" +} +``` + +#### sampleSize + +```json +{ + "total": 500, + "perGroup": 250, + "groups": 2, + "calculationMethod": "优效性检验", + "assumptions": { + "alpha": 0.05, + "power": 0.8, + "effectSize": 0.5, + "dropoutRate": 0.15, + "standardDeviation": 1.2 + }, + "justification": "基于既往研究,预期效应量0.5%...", + "toolUsed": "sample_calculator", + "confidence": 0.95, + "sourceMessageIds": ["msg_150"], + "lastUpdated": "2026-01-24T12:00:00Z" +} +``` + +#### endpoints + +```json +{ + "primary": [{ + "name": "HbA1c变化值", + "definition": "治疗12周后HbA1c相对基线的变化", + "measurementMethod": "高效液相色谱法", + "timePoint": "第12周", + "analysisMethod": "ANCOVA" + }], + "secondary": [{ + "name": "空腹血糖", + "definition": "...", + "measurementMethod": "...", + "timePoint": "第4、8、12周" + }], + "safety": [{ + "name": "低血糖发生率", + "definition": "血糖<3.9mmol/L的事件" + }], + "exploratory": [], + "sourceMessageIds": ["msg_160", "msg_162"], + "lastUpdated": "2026-01-24T12:30:00Z" +} +``` + +### 3.2 protocol_versions (版本历史表) + +```prisma +/// Protocol版本历史表 - 支持回溯 +model ProtocolVersion { + id String @id @default(uuid()) + contextId String @map("context_id") + + version Int + changeType String @map("change_type") + changedFields String[] @map("changed_fields") + previousSnapshot Json @map("previous_snapshot") @db.JsonB + changeReason String? @map("change_reason") + changedBy String @map("changed_by") + + createdAt DateTime @default(now()) @map("created_at") + + context ProtocolContext @relation("ProtocolContextVersions", fields: [contextId], references: [id], onDelete: Cascade) + + @@unique([contextId, version]) + @@index([contextId, createdAt]) + @@map("protocol_versions") + @@schema("protocol_schema") +} +``` + +### 3.3 protocol_generations (研究方案生成表) + +```prisma +/// Protocol生成记录表 - 一键生成研究方案 +model ProtocolGeneration { + id String @id @default(uuid()) + contextId String @map("context_id") + userId String @map("user_id") + + // 生成内容 + generatedContent String @map("generated_content") @db.Text // 生成的研究方案全文 + contentVersion Int @default(1) @map("content_version") // 版本号(重新生成时递增) + + // 使用的Prompt和参数 + promptUsed String @map("prompt_used") @db.Text + generationParams Json? @map("generation_params") @db.JsonB // 生成参数 + + // LLM调用信息 + modelUsed String @map("model_used") + tokensUsed Int? @map("tokens_used") + durationMs Int? @map("duration_ms") + + // 导出记录 + wordFileKey String? @map("word_file_key") // Word文件OSS Key + pdfFileKey String? @map("pdf_file_key") // PDF文件OSS Key + lastExportedAt DateTime? @map("last_exported_at") + + // 用户编辑(如有) + userEditedContent String? @map("user_edited_content") @db.Text + isEdited Boolean @default(false) @map("is_edited") + + status String @default("completed") // generating, completed, failed + errorMessage String? @map("error_message") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@index([contextId]) + @@index([userId, createdAt]) + @@map("protocol_generations") + @@schema("protocol_schema") +} +``` + +**字段说明**: + +| 字段 | 类型 | 说明 | +|------|------|------| +| generatedContent | text | LLM生成的完整研究方案 | +| contentVersion | int | 版本号,重新生成时递增 | +| promptUsed | text | 使用的Prompt(便于复现和调试) | +| userEditedContent | text | 用户编辑后的内容(可选) | +| wordFileKey | string | 导出的Word文件在OSS中的Key | + +### 3.4 protocol_exports (导出记录表) + +```prisma +/// Protocol导出记录表 +model ProtocolExport { + id String @id @default(uuid()) + contextId String @map("context_id") + userId String @map("user_id") + + exportType String @map("export_type") + exportVersion Int @map("export_version") + fileKey String? @map("file_key") + + status String @default("pending") + errorMessage String? @map("error_message") + + createdAt DateTime @default(now()) @map("created_at") + completedAt DateTime? @map("completed_at") + + @@index([contextId]) + @@index([userId, createdAt]) + @@map("protocol_exports") + @@schema("protocol_schema") +} +``` + +--- + +## 四、knowledge_schema - 知识库表 (Phase 2) + +### 4.1 knowledge_docs (知识文档表) + +```prisma +/// 专家知识文档表 +model KnowledgeDoc { + id String @id @default(uuid()) + agentId String? @map("agent_id") + + docType String @map("doc_type") + title String + description String? @db.Text + content String @db.Text + + source String? + author String? + publishDate DateTime? @map("publish_date") + tags String[] + + status String @default("active") + isPublic Boolean @default(true) @map("is_public") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + chunks KnowledgeChunk[] @relation("DocChunks") + + @@index([agentId, docType]) + @@index([status, isPublic]) + @@map("knowledge_docs") + @@schema("knowledge_schema") +} +``` + +### 4.2 knowledge_chunks (知识块表) + +```prisma +/// 知识块表(用于RAG检索) +model KnowledgeChunk { + id String @id @default(uuid()) + docId String @map("doc_id") + + chunkIndex Int @map("chunk_index") + content String @db.Text + contentHash String @map("content_hash") + + metadata Json? @db.JsonB + + createdAt DateTime @default(now()) @map("created_at") + + doc KnowledgeDoc @relation("DocChunks", fields: [docId], references: [id], onDelete: Cascade) + embedding KnowledgeEmbedding? @relation("ChunkEmbedding") + + @@unique([docId, chunkIndex]) + @@index([contentHash]) + @@map("knowledge_chunks") + @@schema("knowledge_schema") +} +``` + +### 4.3 knowledge_embeddings (向量嵌入表) + +```prisma +/// 向量嵌入表(pgvector) +model KnowledgeEmbedding { + id String @id @default(uuid()) + chunkId String @unique @map("chunk_id") + + embedding Unsupported("vector(1536)") + embeddingModel String @map("embedding_model") + + createdAt DateTime @default(now()) @map("created_at") + + chunk KnowledgeChunk @relation("ChunkEmbedding", fields: [chunkId], references: [id], onDelete: Cascade) + + @@map("knowledge_embeddings") + @@schema("knowledge_schema") +} +``` + +--- + +## 五、数据库迁移计划 + +### 5.1 迁移步骤 + +```bash +# 1. 创建新Schema +CREATE SCHEMA IF NOT EXISTS agent_schema; +CREATE SCHEMA IF NOT EXISTS protocol_schema; +CREATE SCHEMA IF NOT EXISTS knowledge_schema; + +# 2. 更新Prisma schema.prisma中的schemas配置 +schemas = ["...", "agent_schema", "protocol_schema", "knowledge_schema"] + +# 3. 生成迁移 +npx prisma migrate dev --name add_agent_framework + +# 4. 应用迁移 +npx prisma migrate deploy +``` + +### 5.2 初始数据 + +```sql +-- 插入Protocol Agent配置 +INSERT INTO agent_schema.agent_configs ( + id, agent_id, agent_name, base_system_prompt, + memory_strategy, max_history_turns, default_model, is_active +) VALUES ( + gen_random_uuid(), + 'protocol_agent', + '研究方案制定助手', + '你是一位经验丰富的临床研究方法学专家,正在帮助研究者制定研究方案...', + 'full', + 20, + 'deepseek-v3', + true +); + +-- 插入阶段配置(示例:科学问题阶段) +INSERT INTO agent_schema.agent_stages ( + id, agent_config_id, stage_id, stage_name, stage_order, + instruction, extraction_schema, next_stage_id, transition_mode +) VALUES ( + gen_random_uuid(), + (SELECT id FROM agent_schema.agent_configs WHERE agent_id = 'protocol_agent'), + 'scientific_question', + '科学问题澄清', + 1, + '引导用户明确研究的核心科学问题...', + '{"type": "object", "properties": {...}}', + 'pico', + 'user_confirm' +); +``` + +--- + +## 六、ER关系图 + +``` +agent_schema +┌───────────────────┐ +│ AgentConfig │ +│ (agent_configs) │ +└────────┬──────────┘ + │ + ┌────┴────┬─────────────┐ + │ │ │ + ▼ ▼ ▼ +┌─────────┐ ┌─────────┐ ┌─────────────┐ +│AgentStage│ │AgentTool│ │ReflexionRule│ +└────┬────┘ └─────────┘ └─────────────┘ + │ + ▼ +┌─────────────┐ +│ ActionCard │ +└─────────────┘ + +┌─────────────┐ ┌─────────────┐ +│AgentSession │ ───────→│ AgentTrace │ +└─────────────┘ └─────────────┘ + │ + │ conversationId + ▼ +aia_schema +┌─────────────┐ ┌─────────────┐ +│Conversation │────────<│ Message │ +└─────────────┘ └─────────────┘ + +protocol_schema +┌─────────────────┐ ┌─────────────────┐ +│ProtocolContext │────<│ProtocolVersion │ +└─────────────────┘ └─────────────────┘ +``` + diff --git a/docs/03-业务模块/AIA-AI智能问答/04-开发计划/04-Protocol_Agent开发计划/03-代码结构设计.md b/docs/03-业务模块/AIA-AI智能问答/04-开发计划/04-Protocol_Agent开发计划/03-代码结构设计.md new file mode 100644 index 00000000..12692f2c --- /dev/null +++ b/docs/03-业务模块/AIA-AI智能问答/04-开发计划/04-Protocol_Agent开发计划/03-代码结构设计.md @@ -0,0 +1,1127 @@ +# Protocol Agent 代码结构设计 + +> 版本:v1.0 +> 创建日期:2026-01-24 + +--- + +## 一、目录结构 + +``` +apps/api/src/ +├── modules/ +│ │ +│ ├── agent-framework/ # 【新增】通用Agent框架 +│ │ ├── core/ +│ │ │ ├── BaseAgentOrchestrator.ts # 抽象基类 +│ │ │ ├── QueryAnalyzer.ts # Query层-意图分析 +│ │ │ ├── Planner.ts # Planner层-执行规划 +│ │ │ ├── Executor.ts # Executor层-任务执行 +│ │ │ ├── ToolInvoker.ts # 工具调用器 +│ │ │ └── ReflexionEngine.ts # Reflection层-质量反思 +│ │ │ +│ │ ├── memory/ +│ │ │ ├── MemoryManager.ts # 记忆管理器 +│ │ │ └── ContextManager.ts # 上下文管理器 +│ │ │ +│ │ ├── config/ +│ │ │ ├── ConfigLoader.ts # 配置加载器 +│ │ │ └── StageManager.ts # 阶段状态机 +│ │ │ +│ │ ├── prompt/ +│ │ │ ├── PromptBuilder.ts # Prompt构建器 +│ │ │ └── PromptTemplates.ts # Prompt模板常量 +│ │ │ +│ │ ├── trace/ +│ │ │ ├── TraceLogger.ts # 追踪日志记录 +│ │ │ └── TraceAnalyzer.ts # 追踪分析(调试) +│ │ │ +│ │ ├── tools/ +│ │ │ ├── ToolRegistry.ts # 工具注册表 +│ │ │ ├── DeepLinkGenerator.ts # Deep Link生成器 +│ │ │ └── BaseToolHandler.ts # 工具处理基类 +│ │ │ +│ │ ├── types/ +│ │ │ ├── agent.types.ts # Agent核心类型 +│ │ │ ├── query.types.ts # Query相关类型 +│ │ │ ├── plan.types.ts # Plan相关类型 +│ │ │ ├── execution.types.ts # 执行相关类型 +│ │ │ ├── reflexion.types.ts # 反思相关类型 +│ │ │ └── tool.types.ts # 工具相关类型 +│ │ │ +│ │ └── index.ts # 模块导出 +│ │ +│ ├── aia/ +│ │ ├── agents/ +│ │ │ │ +│ │ │ ├── protocol/ # 【新增】Protocol Agent +│ │ │ │ ├── ProtocolOrchestrator.ts # Protocol编排器 +│ │ │ │ ├── ProtocolContextService.ts # Context服务 +│ │ │ │ ├── ProtocolStageConfig.ts # 阶段配置 +│ │ │ │ ├── ProtocolPrompts.ts # 专用Prompt +│ │ │ │ ├── ProtocolExtractor.ts # 数据提取器 +│ │ │ │ ├── tools/ +│ │ │ │ │ ├── SampleCalculatorTool.ts # 样本量计算工具 +│ │ │ │ │ ├── DeepLinkTools.ts # Deep Link工具 +│ │ │ │ │ └── index.ts +│ │ │ │ └── index.ts +│ │ │ │ +│ │ │ └── (future agents)/ +│ │ │ ├── statistics/ # 未来:统计分析Agent +│ │ │ └── data-cleaning/ # 未来:数据清洗Agent +│ │ │ +│ │ ├── routes/ +│ │ │ ├── protocolAgent.routes.ts # 【新增】Protocol Agent路由 +│ │ │ ├── chat.routes.ts # 现有聊天路由 +│ │ │ └── index.ts +│ │ │ +│ │ ├── services/ +│ │ │ ├── chat.service.ts # 现有聊天服务 +│ │ │ ├── conversation.service.ts # 现有对话服务 +│ │ │ └── protocolAgent.service.ts # 【新增】Protocol Agent服务 +│ │ │ +│ │ └── (existing files) +│ │ +│ └── (other modules) +│ +├── shared/ +│ ├── llm/ # 现有LLM Gateway +│ └── (other shared) +│ +└── (other folders) +``` + +--- + +## 二、核心类型定义 + +### 2.1 agent.types.ts - Agent核心类型 + +```typescript +// ============================================================ +// Agent核心类型定义 +// ============================================================ + +/** + * Agent配置(从数据库加载) + */ +export interface AgentConfig { + id: string; + agentId: string; + agentName: string; + baseSystemPrompt: string; + toneOfVoice?: string; + globalKnowledge: string[]; + memoryStrategy: 'full' | 'sliding_window' | 'summary'; + maxHistoryTurns: number; + defaultModel: string; + temperature: number; + maxTokens: number; + stages: StageConfig[]; + tools: ToolConfig[]; + reflexionRules: ReflexionRuleConfig[]; +} + +/** + * 阶段配置 + */ +export interface StageConfig { + id: string; + stageId: string; + stageName: string; + stageOrder: number; + instruction: string; + extractionSchema: JSONSchema; + completionCriteria?: string; + nextStageId: string | null; + transitionMode: 'user_confirm' | 'auto' | 'condition'; + transitionCondition?: Record; + availableToolIds: string[]; + actionCards: ActionCardConfig[]; +} + +/** + * Action Card配置 + */ +export interface ActionCardConfig { + cardId: string; + title: string; + description?: string; + iconType?: string; + triggerType: 'stage_enter' | 'condition' | 'manual'; + triggerCondition?: Record; + actionType: 'deep_link' | 'api_call' | 'modal'; + actionPayload: Record; + onCompleteAction?: string; + resultMapping?: Record; +} + +/** + * Agent会话状态 + */ +export interface AgentSessionState { + sessionId: string; + conversationId: string; + agentId: string; + userId: string; + currentStageId: string; + stageStatus: 'in_progress' | 'completed' | 'paused'; + lastActiveAt: Date; +} + +/** + * Agent响应 + */ +export interface AgentResponse { + traceId: string; + message: string; + thinkingContent?: string; + actionCards: ActionCardPayload[]; + contextUpdate?: Record; + currentStage: string; + stageStatus: string; + suggestions?: string[]; +} +``` + +### 2.2 query.types.ts - Query相关类型 + +```typescript +// ============================================================ +// Query Analysis 类型定义 +// ============================================================ + +/** + * 意图类型 + */ +export type IntentType = + | 'provide_info' // 提供信息 + | 'ask_question' // 询问问题 + | 'request_change' // 请求修改 + | 'confirm_complete' // 确认完成 + | 'navigate_stage' // 跳转阶段 + | 'request_tool' // 请求工具 + | 'chitchat' // 闲聊 + | 'unclear'; // 不明确 + +/** + * 提取的实体 + */ +export interface ExtractedEntity { + type: string; // 实体类型,如 'population', 'intervention' + value: string; // 实体值 + confidence: number; // 置信度 0-1 + span?: { // 在原文中的位置 + start: number; + end: number; + }; +} + +/** + * Query分析结果 + */ +export interface QueryAnalysisResult { + intent: { + primary: IntentType; + confidence: number; + subIntent?: string; + }; + entities: ExtractedEntity[]; + resolvedQuery: string; + sentiment?: 'positive' | 'neutral' | 'confused' | 'frustrated'; + requiresClarification: boolean; + clarificationQuestion?: string; +} + +/** + * Query分析器配置 + */ +export interface QueryAnalyzerConfig { + enableEntityExtraction: boolean; + enableSentimentAnalysis: boolean; + enableContextResolution: boolean; + confidenceThreshold: number; +} +``` + +### 2.3 plan.types.ts - Plan相关类型 + +```typescript +// ============================================================ +// Planner 类型定义 +// ============================================================ + +/** + * 执行策略 + */ +export type ExecutionStrategy = + | 'guide_and_extract' // 引导对话并提取数据 + | 'answer_question' // 回答用户问题 + | 'update_context' // 更新已有数据 + | 'trigger_tool' // 触发工具调用 + | 'transition_stage' // 执行阶段流转 + | 'request_clarification' // 请求澄清 + | 'handle_chitchat'; // 处理闲聊 + +/** + * 执行任务 + */ +export interface ExecutionTask { + taskId: string; + taskType: 'llm_generate' | 'tool_call' | 'data_extract' | 'context_update'; + priority: number; + params: Record; + dependsOn?: string[]; // 依赖的其他任务ID +} + +/** + * 执行计划 + */ +export interface ExecutionPlan { + strategy: ExecutionStrategy; + tasks: ExecutionTask[]; + toolsNeeded: string[]; + promptOverrides?: { + systemPrompt?: string; + userPromptPrefix?: string; + }; + shouldExtract: boolean; + shouldReflect: boolean; + metadata: { + planReason: string; + estimatedTokens?: number; + }; +} + +/** + * 阶段流转结果 + */ +export interface StageTransitionResult { + success: boolean; + previousStage: string; + nextStage?: string; + issues?: string[]; + suggestions?: string[]; + summary?: string; +} +``` + +### 2.4 execution.types.ts - 执行相关类型 + +```typescript +// ============================================================ +// Executor 类型定义 +// ============================================================ + +/** + * LLM响应 + */ +export interface LLMResponse { + content: string; + thinkingContent?: string; + finishReason: 'stop' | 'length' | 'tool_calls'; + usage: { + promptTokens: number; + completionTokens: number; + totalTokens: number; + }; + model: string; +} + +/** + * 提取的数据 + */ +export interface ExtractedData { + hasNewData: boolean; + data: Record; + confidence: number; + sourceMessageId: string; + extractionMethod: 'inline' | 'post_process'; +} + +/** + * 工具调用结果 + */ +export interface ToolCallResult { + toolId: string; + success: boolean; + result?: any; + error?: string; + durationMs: number; +} + +/** + * 执行结果 + */ +export interface ExecutionResult { + response: LLMResponse; + extracted?: ExtractedData; + toolResults?: ToolCallResult[]; + actionCards: ActionCardPayload[]; +} + +/** + * Action Card载荷(返回给前端) + */ +export interface ActionCardPayload { + cardId: string; + title: string; + description?: string; + iconType?: string; + actionType: 'deep_link' | 'api_call' | 'modal'; + actionUrl?: string; + actionParams?: Record; + priority: number; +} +``` + +### 2.5 reflexion.types.ts - 反思相关类型 + +```typescript +// ============================================================ +// Reflexion 类型定义 +// ============================================================ + +/** + * 反思规则配置 + */ +export interface ReflexionRuleConfig { + ruleId: string; + ruleName: string; + triggerStageId?: string; + triggerTiming: 'on_extract' | 'on_complete' | 'on_transition'; + ruleType: 'prompt_based' | 'rule_based' | 'hybrid'; + promptTemplate?: string; + ruleLogic?: RuleLogic; + severity: 'error' | 'warning' | 'info'; + failureAction: 'block' | 'warn' | 'log'; + priority: number; +} + +/** + * 规则逻辑(rule_based类型) + */ +export interface RuleLogic { + field: string; + condition: 'required' | 'min_length' | 'max_length' | 'pattern' | 'custom'; + value?: any; + customValidator?: string; // 自定义验证函数名 +} + +/** + * 反思检查项 + */ +export interface ReflexionCheck { + ruleId: string; + ruleName: string; + passed: boolean; + severity: 'error' | 'warning' | 'info'; + message?: string; + suggestion?: string; +} + +/** + * 反思结果 + */ +export interface ReflexionResult { + passed: boolean; + checks: ReflexionCheck[]; + issues: string[]; + suggestions: string[]; + blockTransition: boolean; +} +``` + +### 2.6 tool.types.ts - 工具相关类型 + +```typescript +// ============================================================ +// Tool 类型定义 +// ============================================================ + +/** + * 工具类型 + */ +export type ToolType = 'internal' | 'external' | 'deep_link' | 'rag'; + +/** + * 工具配置 + */ +export interface ToolConfig { + toolId: string; + toolName: string; + toolDescription: string; + toolType: ToolType; + functionSchema?: FunctionSchema; + handlerType: 'code' | 'api' | 'deep_link'; + handlerConfig: Record; + requiresAuth: boolean; + timeout: number; +} + +/** + * Function Schema (OpenAI格式) + */ +export interface FunctionSchema { + name: string; + description: string; + parameters: { + type: 'object'; + properties: Record; + required?: string[]; + }; +} + +/** + * JSON Schema属性 + */ +export interface JSONSchemaProperty { + type: 'string' | 'number' | 'boolean' | 'array' | 'object'; + description?: string; + enum?: string[]; + items?: JSONSchemaProperty; + properties?: Record; +} + +/** + * 工具处理器接口 + */ +export interface IToolHandler { + toolId: string; + execute(params: Record, context: ToolContext): Promise; + validate?(params: Record): boolean; +} + +/** + * 工具执行上下文 + */ +export interface ToolContext { + conversationId: string; + userId: string; + agentId: string; + currentStage: string; + protocolContext?: Record; +} +``` + +--- + +## 三、核心类实现 + +### 3.1 BaseAgentOrchestrator.ts + +```typescript +/** + * Agent编排器基类 + * 所有Agent都继承此类,实现标准的Query→Plan→Execute→Reflect流程 + */ +export abstract class BaseAgentOrchestrator { + + protected config: AgentConfig; + protected queryAnalyzer: QueryAnalyzer; + protected planner: Planner; + protected executor: Executor; + protected reflexionEngine: ReflexionEngine; + protected memoryManager: MemoryManager; + protected configLoader: ConfigLoader; + protected traceLogger: TraceLogger; + + constructor(dependencies: AgentDependencies) { + // 注入依赖 + } + + /** + * 子类必须实现的抽象方法 + */ + abstract getAgentId(): string; + abstract getContextSchema(): TContext; + abstract getContext(conversationId: string): Promise; + abstract updateContext(conversationId: string, data: Partial): Promise; + + /** + * 主处理入口 + */ + async handleMessage( + conversationId: string, + userMessage: string, + messageId: string + ): Promise { + + const traceId = this.traceLogger.startTrace(conversationId); + + try { + // Step 1: 加载配置和上下文 + const config = await this.configLoader.getAgentConfig(this.getAgentId()); + const context = await this.getContext(conversationId); + const session = await this.getSession(conversationId); + const stageConfig = this.getStageConfig(config, session.currentStageId); + + // Step 2: Query Analysis + const queryResult = await this.queryAnalyzer.analyze( + userMessage, + context, + session + ); + this.traceLogger.logStep(traceId, 'query_analysis', { input: userMessage, output: queryResult }); + + // Step 3: Planning + const plan = await this.planner.createPlan( + queryResult, + stageConfig, + context + ); + this.traceLogger.logStep(traceId, 'planning', { input: queryResult, output: plan }); + + // Step 4: Execution + const execResult = await this.executor.execute( + plan, + config, + stageConfig, + context, + conversationId + ); + this.traceLogger.logStep(traceId, 'execution', { input: plan, output: execResult }); + + // Step 5: Data Extraction + if (plan.shouldExtract && execResult.extracted?.hasNewData) { + await this.updateContext(conversationId, execResult.extracted.data); + } + + // Step 6: Reflection (可选) + let reflexionResult: ReflexionResult | undefined; + if (plan.shouldReflect) { + reflexionResult = await this.reflexionEngine.reflect( + execResult, + context, + stageConfig + ); + this.traceLogger.logStep(traceId, 'reflexion', { output: reflexionResult }); + } + + // Step 7: 构建响应 + const response = this.buildResponse( + execResult, + reflexionResult, + session, + stageConfig + ); + + this.traceLogger.endTrace(traceId, 'success'); + return response; + + } catch (error) { + this.traceLogger.endTrace(traceId, 'error', error); + throw error; + } + } + + /** + * 处理阶段完成 + */ + async handleStageComplete(conversationId: string): Promise { + // ... 阶段流转逻辑 + } + + /** + * 处理Action Card回调 + */ + async handleActionCallback( + conversationId: string, + cardId: string, + result: any + ): Promise { + // ... Action回调处理 + } + + // ... 其他protected方法 +} +``` + +### 3.2 ProtocolOrchestrator.ts + +```typescript +/** + * Protocol Agent 编排器 + * 继承BaseAgentOrchestrator,实现研究方案特定逻辑 + */ +export class ProtocolOrchestrator extends BaseAgentOrchestrator { + + private contextService: ProtocolContextService; + private protocolExtractor: ProtocolExtractor; + + constructor(dependencies: ProtocolAgentDependencies) { + super(dependencies); + this.contextService = dependencies.contextService; + this.protocolExtractor = dependencies.protocolExtractor; + } + + getAgentId(): string { + return 'protocol_agent'; + } + + getContextSchema(): ProtocolContext { + return ProtocolContextSchema; + } + + async getContext(conversationId: string): Promise { + return this.contextService.getOrCreate(conversationId); + } + + async updateContext(conversationId: string, data: Partial): Promise { + await this.contextService.update(conversationId, data); + } + + /** + * 重写:Protocol特定的提取逻辑 + */ + protected async extractData( + llmResponse: LLMResponse, + stageConfig: StageConfig + ): Promise { + return this.protocolExtractor.extract(llmResponse, stageConfig); + } + + /** + * 重写:Protocol特定的阶段完成检查 + */ + protected async validateStageCompletion( + context: ProtocolContext, + stageConfig: StageConfig + ): Promise { + // Protocol特定的验证逻辑 + const stageData = context[stageConfig.stageId as keyof ProtocolContext]; + + // 检查必填字段 + // 检查数据完整性 + // 调用Reflexion引擎 + + return this.reflexionEngine.reflectOnStageComplete(stageData, stageConfig); + } +} +``` + +### 3.3 PromptBuilder.ts + +```typescript +/** + * Prompt构建器 + * 负责构建各阶段的System Prompt + */ +export class PromptBuilder { + + /** + * 构建系统Prompt + */ + buildSystemPrompt(params: { + basePrompt: string; + stageConfig: StageConfig; + context: Record; + toneOfVoice?: string; + }): string { + + const { basePrompt, stageConfig, context, toneOfVoice } = params; + + return ` +# 角色设定 +${basePrompt} + +${toneOfVoice ? `# 语气风格\n${toneOfVoice}\n` : ''} + +# 当前阶段: ${stageConfig.stageName} +${stageConfig.instruction} + +# 已收集的研究方案信息 + +${this.formatContext(context)} + + +# 数据提取要求 +在回复的最后,如果用户提供了新的信息,请以XML格式输出提取到的结构化数据: + +${JSON.stringify(stageConfig.extractionSchema.example || {}, null, 2)} + + +# 注意事项 +- 如果用户信息不完整,温和地追问补充 +- 如果用户表述有歧义,先确认再记录 +- 保持专业但不失亲和力的语气 +- 不要编造或假设用户未提供的信息 +`.trim(); + } + + /** + * 构建意图识别Prompt + */ + buildQueryAnalysisPrompt(params: { + userMessage: string; + currentStage: string; + contextSummary: string; + }): string { + return ` +分析用户消息的意图。 + +当前阶段: ${params.currentStage} +上下文摘要: ${params.contextSummary} + +用户消息: "${params.userMessage}" + +请分析并以JSON格式返回: +{ + "intent": { + "primary": "provide_info|ask_question|request_change|confirm_complete|navigate_stage|request_tool|chitchat|unclear", + "confidence": 0.0-1.0, + "subIntent": "可选的子意图" + }, + "entities": [ + {"type": "entity_type", "value": "entity_value", "confidence": 0.0-1.0} + ], + "requiresClarification": true|false, + "clarificationQuestion": "如需澄清,提供问题" +} +`.trim(); + } + + /** + * 构建Reflexion检查Prompt + */ + buildReflexionPrompt(params: { + stageData: Record; + stageConfig: StageConfig; + rule: ReflexionRuleConfig; + }): string { + if (params.rule.promptTemplate) { + return this.interpolateTemplate(params.rule.promptTemplate, { + stageData: params.stageData, + stageName: params.stageConfig.stageName + }); + } + + return ` +检查${params.stageConfig.stageName}阶段的数据质量。 + +当前数据: +${JSON.stringify(params.stageData, null, 2)} + +完成标准: +${params.stageConfig.completionCriteria || '无特定标准'} + +请检查并以JSON格式返回: +{ + "passed": true|false, + "issues": ["问题1", "问题2"], + "suggestions": ["建议1", "建议2"] +} +`.trim(); + } + + private formatContext(context: Record): string { + // 格式化上下文,过滤空值,美化输出 + const filtered = Object.entries(context) + .filter(([_, v]) => v != null && Object.keys(v).length > 0) + .reduce((acc, [k, v]) => ({ ...acc, [k]: v }), {}); + + return JSON.stringify(filtered, null, 2); + } + + private interpolateTemplate(template: string, vars: Record): string { + return template.replace(/\{\{(\w+(?:\.\w+)*)\}\}/g, (_, path) => { + const value = path.split('.').reduce((obj: any, key: string) => obj?.[key], vars); + return typeof value === 'object' ? JSON.stringify(value) : String(value ?? ''); + }); + } +} +``` + +--- + +## 四、API设计 + +### 4.1 路由定义 + +```typescript +// protocolAgent.routes.ts + +export async function protocolAgentRoutes(fastify: FastifyInstance) { + + // 发送消息(核心对话API) + fastify.post('/api/aia/protocol-agent/chat', { + schema: { + body: { + type: 'object', + required: ['conversationId', 'message'], + properties: { + conversationId: { type: 'string' }, + message: { type: 'string' }, + attachments: { type: 'array', items: { type: 'string' } } + } + } + }, + handler: protocolAgentController.chat + }); + + // 确认阶段完成 + fastify.post('/api/aia/protocol-agent/stage/complete', { + schema: { + body: { + type: 'object', + required: ['conversationId'], + properties: { + conversationId: { type: 'string' } + } + } + }, + handler: protocolAgentController.completeStage + }); + + // 获取当前Context + fastify.get('/api/aia/protocol-agent/context/:conversationId', { + handler: protocolAgentController.getContext + }); + + // 更新Context(State Panel编辑) + fastify.patch('/api/aia/protocol-agent/context/:conversationId', { + schema: { + body: { + type: 'object', + required: ['field', 'value'], + properties: { + field: { type: 'string' }, + value: { type: 'object' } + } + } + }, + handler: protocolAgentController.updateContext + }); + + // Action Card回调 + fastify.post('/api/aia/protocol-agent/action-callback', { + schema: { + body: { + type: 'object', + required: ['conversationId', 'cardId', 'result'], + properties: { + conversationId: { type: 'string' }, + cardId: { type: 'string' }, + result: { type: 'object' } + } + } + }, + handler: protocolAgentController.handleActionCallback + }); + + // 获取追踪日志(调试用) + fastify.get('/api/aia/protocol-agent/trace/:traceId', { + handler: protocolAgentController.getTrace + }); +} +``` + +### 4.2 响应格式 + +```typescript +// Chat响应 +interface ChatResponse { + traceId: string; + message: string; + thinkingContent?: string; + actionCards: ActionCardPayload[]; + contextUpdate: { + field: string; + data: any; + confidence: number; + } | null; + currentStage: string; + stageStatus: 'in_progress' | 'completed'; + stageProgress: { + current: number; + total: number; + completedStages: string[]; + }; +} + +// Stage Complete响应 +interface StageCompleteResponse { + success: boolean; + issues?: string[]; + suggestions?: string[]; + previousStage: string; + nextStage?: string; + summary?: string; + nextStageActionCards?: ActionCardPayload[]; +} + +// Context响应 +interface ContextResponse { + id: string; + conversationId: string; + currentStage: string; + overallProgress: number; + completedStages: string[]; + scientificQuestion: any; + pico: any; + studyDesign: any; + sampleSize: any; + endpoints: any; + updatedAt: string; +} +``` + +--- + +## 五、前端组件设计 + +### 5.1 组件结构 + +``` +apps/web/src/modules/aia/ +├── components/ +│ ├── ProtocolAgent/ +│ │ ├── ProtocolAgentPage.tsx # 主页面 +│ │ ├── ProtocolChat.tsx # 聊天区域 +│ │ ├── StatePanel/ +│ │ │ ├── StatePanel.tsx # 状态面板容器 +│ │ │ ├── StageProgress.tsx # 阶段进度 +│ │ │ ├── ContextDisplay.tsx # Context展示 +│ │ │ └── ContextEditor.tsx # Context编辑 +│ │ ├── ActionCard/ +│ │ │ ├── ActionCardList.tsx # Action Card列表 +│ │ │ ├── ActionCard.tsx # 单个Card +│ │ │ └── DeepLinkModal.tsx # Deep Link弹窗 +│ │ └── index.ts +│ └── (existing components) +│ +├── hooks/ +│ ├── useProtocolAgent.ts # Protocol Agent Hook +│ ├── useProtocolContext.ts # Context管理Hook +│ └── useActionCard.ts # Action Card Hook +│ +├── stores/ +│ └── protocolAgentStore.ts # Zustand Store +│ +└── services/ + └── protocolAgentApi.ts # API调用 +``` + +### 5.2 核心Hook + +```typescript +// useProtocolAgent.ts + +export function useProtocolAgent(conversationId: string) { + const [context, setContext] = useState(null); + const [currentStage, setCurrentStage] = useState('scientific_question'); + const [actionCards, setActionCards] = useState([]); + const [isLoading, setIsLoading] = useState(false); + + // 发送消息 + const sendMessage = async (message: string) => { + setIsLoading(true); + try { + const response = await protocolAgentApi.chat(conversationId, message); + + // 更新状态 + if (response.contextUpdate) { + setContext(prev => ({ + ...prev, + [response.contextUpdate.field]: response.contextUpdate.data + })); + } + setCurrentStage(response.currentStage); + setActionCards(response.actionCards); + + return response; + } finally { + setIsLoading(false); + } + }; + + // 确认阶段完成 + const completeStage = async () => { + const result = await protocolAgentApi.completeStage(conversationId); + if (result.success && result.nextStage) { + setCurrentStage(result.nextStage); + setActionCards(result.nextStageActionCards || []); + } + return result; + }; + + // 更新Context + const updateContext = async (field: string, value: any) => { + await protocolAgentApi.updateContext(conversationId, field, value); + setContext(prev => ({ ...prev, [field]: value })); + }; + + // 处理Action Card点击 + const handleActionCard = async (card: ActionCardPayload) => { + if (card.actionType === 'deep_link') { + // 打开Deep Link + window.open(card.actionUrl, '_blank'); + } else if (card.actionType === 'modal') { + // 打开弹窗 + } + }; + + return { + context, + currentStage, + actionCards, + isLoading, + sendMessage, + completeStage, + updateContext, + handleActionCard + }; +} +``` + +--- + +## 六、依赖注入设计 + +```typescript +// 依赖注入容器配置 + +export function createProtocolAgentDependencies( + prisma: PrismaClient, + llmGateway: LLMGateway +): ProtocolAgentDependencies { + + // 基础服务 + const traceLogger = new TraceLogger(prisma); + const configLoader = new ConfigLoader(prisma); + const memoryManager = new MemoryManager(prisma); + const promptBuilder = new PromptBuilder(); + + // Query层 + const queryAnalyzer = new QueryAnalyzer(llmGateway, promptBuilder); + + // Planner层 + const stageManager = new StageManager(configLoader); + const planner = new Planner(stageManager); + + // Executor层 + const toolRegistry = new ToolRegistry(); + const executor = new Executor(llmGateway, toolRegistry, promptBuilder); + + // Reflexion层 + const reflexionEngine = new ReflexionEngine(llmGateway, promptBuilder); + + // Protocol专用 + const contextService = new ProtocolContextService(prisma); + const protocolExtractor = new ProtocolExtractor(); + + return { + traceLogger, + configLoader, + memoryManager, + queryAnalyzer, + planner, + executor, + reflexionEngine, + contextService, + protocolExtractor + }; +} +``` + + diff --git a/docs/03-业务模块/AIA-AI智能问答/04-开发计划/04-Protocol_Agent开发计划/04-分阶段实施计划.md b/docs/03-业务模块/AIA-AI智能问答/04-开发计划/04-Protocol_Agent开发计划/04-分阶段实施计划.md new file mode 100644 index 00000000..51498cb7 --- /dev/null +++ b/docs/03-业务模块/AIA-AI智能问答/04-开发计划/04-Protocol_Agent开发计划/04-分阶段实施计划.md @@ -0,0 +1,632 @@ +# Protocol Agent 分阶段实施计划 + +> 版本:v1.0 +> 创建日期:2026-01-24 + +--- + +## 一、总体规划 + +### 1.1 阶段划分 + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Protocol Agent 开发路线图 │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Phase 1: MVP完整功能 (4周) │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ │ │ +│ │ Sprint 1 (W1) Sprint 2 (W2) Sprint 3 (W3) Sprint 4 (W4) │ +│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ +│ │ │ 框架搭建 │ → │ 核心对话 │ → │ 前端UI │ → │ 一键生成 │ │ │ +│ │ │ 数据库 │ │ 5阶段流程│ │ State │ │ Word导出 │ │ │ +│ │ │ 类型定义 │ │ Prompt │ │ Panel │ │ Prompt │ │ │ +│ │ │ │ │ Context │ │ 同步按钮│ │ 调优测试 │ │ │ +│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │ +│ │ │ │ +│ │ 交付物:完整MVP,用户可通过对话完成5阶段,一键生成研究方案并下载Word │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ───────────────────────────────────────────────────────────────────────── │ +│ │ +│ Phase 2: 知识增强 (3周) Phase 3: 平台化 (2周) │ +│ ┌─────────────────────────┐ ┌─────────────────────────┐ │ +│ │ □ EKB知识库建设 │ │ □ 后台配置管理 │ │ +│ │ □ RAG检索集成 │ → │ □ Prompt在线调试 │ │ +│ │ □ Function Calling │ │ □ 完整Trace分析 │ │ +│ │ □ 高级Reflexion │ │ □ 多Agent支持 │ │ +│ └─────────────────────────┘ └─────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +### 1.2 里程碑定义 + +| 里程碑 | 时间 | 交付物 | 验收标准 | +|--------|------|--------|----------| +| M1 | Week 2 | Agent框架 + 基础对话 | 能完成单阶段对话,Context正确存储 | +| **M2** | **Week 4** | **MVP完整功能** | **5阶段对话 + 一键生成 + Word下载** | +| M3 | Week 7 | 知识增强版本 | RAG检索集成可用 | +| M4 | Week 9 | 平台化版本 | 后台配置管理可用 | + +--- + +## 二、Phase 1 详细计划(4周) + +### Sprint 1: 基础框架搭建(Week 1) + +**目标**:建立可复用的Agent框架骨架 + +#### 任务列表 + +| ID | 任务 | 优先级 | 预估 | 负责人 | 状态 | +|----|------|--------|------|--------|------| +| 1.1 | 创建agent_schema数据库迁移 | P0 | 4h | - | ⬜ | +| 1.2 | 创建protocol_schema数据库迁移 | P0 | 2h | - | ⬜ | +| 1.3 | 定义核心类型(types/*.ts) | P0 | 3h | - | ⬜ | +| 1.4 | 实现ConfigLoader | P0 | 2h | - | ⬜ | +| 1.5 | 实现BaseAgentOrchestrator抽象类 | P0 | 4h | - | ⬜ | +| 1.6 | 实现QueryAnalyzer | P0 | 3h | - | ⬜ | +| 1.7 | 实现Planner + StageManager | P0 | 3h | - | ⬜ | +| 1.8 | 实现TraceLogger | P1 | 2h | - | ⬜ | +| 1.9 | 创建Protocol Agent初始配置数据 | P0 | 2h | - | ⬜ | + +**Sprint 1 产出**: +- [ ] 数据库Schema创建完成 +- [ ] 核心框架代码结构建立 +- [ ] Agent配置可从数据库加载 + +#### 详细任务说明 + +**1.1 创建agent_schema数据库迁移** + +```bash +# 执行步骤 +1. 更新 schema.prisma,添加 agent_schema 到 schemas 列表 +2. 添加所有 agent_schema 表定义 +3. 运行 npx prisma migrate dev --name add_agent_framework +4. 验证表创建成功 +``` + +**1.5 实现BaseAgentOrchestrator抽象类** + +```typescript +// 核心方法清单 +- constructor(dependencies) +- abstract getAgentId(): string +- abstract getContextSchema(): TContext +- async handleMessage(conversationId, userMessage, messageId): Promise +- async handleStageComplete(conversationId): Promise +- protected getSession(conversationId): Promise +- protected buildResponse(...): AgentResponse +``` + +--- + +### Sprint 2: Protocol Agent核心实现(Week 2) + +**目标**:实现Protocol Agent的完整对话流程 + +#### 任务列表 + +| ID | 任务 | 优先级 | 预估 | 负责人 | 状态 | +|----|------|--------|------|--------|------| +| 2.1 | 实现ProtocolOrchestrator | P0 | 4h | - | ⬜ | +| 2.2 | 实现ProtocolContextService | P0 | 3h | - | ⬜ | +| 2.3 | 实现PromptBuilder | P0 | 3h | - | ⬜ | +| 2.4 | 实现Executor(LLM调用) | P0 | 4h | - | ⬜ | +| 2.5 | 实现ProtocolExtractor(数据提取) | P0 | 4h | - | ⬜ | +| 2.6 | 实现MemoryManager | P1 | 2h | - | ⬜ | +| 2.7 | 编写5个阶段的Prompt配置 | P0 | 4h | - | ⬜ | +| 2.8 | 实现API路由 | P0 | 2h | - | ⬜ | +| 2.9 | 单元测试 | P1 | 3h | - | ⬜ | + +**Sprint 2 产出**: +- [ ] Protocol Agent后端完整实现 +- [ ] 能通过API完成对话 +- [ ] Context正确保存和更新 + +#### 核心交互模式:对话驱动 + 同步确认 + +``` +【不需要显式的"确认完成"按钮】 + +用户通过两种方式推进阶段: + +1. 同步按钮:AI在对话中显示"同步到方案"按钮,用户点击后数据同步 +2. 口头确认:用户说"继续"/"下一步",AI识别意图后自动进入下一阶段 + +示例流程: +┌─────────────────────────────────────────────────────┐ +│ AI: 我已整理出您的科学问题: │ +│ "阿司匹林预防老年高血压患者中风的疗效研究" │ +│ │ +│ ┌──────────────────┐ │ +│ │ ✅ 同步到方案 │ ← 内嵌在AI回复中 │ +│ └──────────────────┘ │ +│ │ +│ 确认后,我们可以继续进行PICO梳理。 │ +├─────────────────────────────────────────────────────┤ +│ User: 好的,继续吧 │ +├─────────────────────────────────────────────────────┤ +│ AI: 好的,让我们进入PICO梳理阶段... │ +└─────────────────────────────────────────────────────┘ +``` + +#### 详细Prompt配置 + +**科学问题阶段 Prompt**: + +``` +你正在帮助研究者明确研究的核心科学问题。 + +引导要点: +1. 询问想解决什么临床问题 +2. 了解目标人群是谁 +3. 明确期望达成的目标 +4. 探讨研究的临床意义 + +当前已知信息: +{{contextSummary}} + +对话策略: +- 用温和专业的语气引导用户 +- 当收集到足够信息后,整理出科学问题,并提供"同步到方案"选项 +- 同步后,询问用户是否继续进行PICO梳理 + +如果用户提供了信息,在回复末尾提取: + +{ + "content": "科学问题表述", + "clinicalBackground": "临床背景", + "researchGap": "研究空白", + "readyToSync": true // 是否可以同步 +} + +``` + +**PICO阶段 Prompt**: + +``` +基于已确定的科学问题,现在需要明确PICO四要素。 + +科学问题:{{scientificQuestion.content}} + +请引导用户确定: +- P (Population): 研究人群的特征和入选标准 +- I (Intervention): 干预措施的具体内容 +- C (Comparison): 对照组的设置 +- O (Outcome): 主要和次要结局指标 + +对话策略: +- 可以分多轮逐步收集各要素 +- 当四要素都明确后,提供"同步到方案"选项 +- 同步后,询问用户是否继续进行研究设计 + +提取格式: + +{ + "P": {"value": "", "details": ""}, + "I": {"value": "", "details": ""}, + "C": {"value": "", "details": ""}, + "O": {"value": "", "details": ""}, + "readyToSync": true +} + +``` + +**观察指标阶段 Prompt**(新增): + +``` +基于PICO中的结局指标(O),现在需要详细设计观察指标。 + +已确定的PICO:{{pico}} +研究设计:{{studyDesign}} + +请引导用户明确: +1. 主要结局指标(Primary Endpoint) + - 指标名称、定义、测量方法、评价时点 +2. 次要结局指标(Secondary Endpoints) +3. 安全性指标(Safety Endpoints) +4. 探索性指标(Exploratory Endpoints,可选) + +对话策略: +- 首先确认主要结局指标,这是最重要的 +- 然后引导设置次要和安全性指标 +- 当指标体系完整后,提供"同步到方案"选项 + +提取格式: + +{ + "primary": [{"name": "", "definition": "", "method": "", "timePoint": ""}], + "secondary": [...], + "safety": [...], + "readyToSync": true +} + +``` + +--- + +### Sprint 3: 前端实现(Week 3) + +**目标**:完成State Panel、同步按钮和Action Card UI + +#### 任务列表 + +| ID | 任务 | 优先级 | 预估 | 负责人 | 状态 | +|----|------|--------|------|--------|------| +| 3.1 | Protocol Agent入口页面 | P0 | 2h | - | ⬜ | +| 3.2 | 实现useProtocolAgent Hook | P0 | 3h | - | ⬜ | +| 3.3 | 实现protocolAgentStore | P1 | 2h | - | ⬜ | +| 3.4 | 实现StatePanel组件 | P0 | 4h | - | ⬜ | +| 3.5 | 实现StageProgress组件(5阶段) | P0 | 2h | - | ⬜ | +| 3.6 | 实现ContextDisplay组件 | P0 | 3h | - | ⬜ | +| 3.7 | **实现SyncButton组件(内嵌AI回复)** | P0 | 3h | - | ⬜ | +| 3.8 | 实现ActionCard组件 | P0 | 2h | - | ⬜ | +| 3.9 | 与现有AIStreamChat集成 | P0 | 3h | - | ⬜ | +| 3.10 | 样式调整和响应式 | P1 | 2h | - | ⬜ | + +**Sprint 3 产出**: +- [ ] 完整的Protocol Agent界面 +- [ ] State Panel实时显示5阶段Context +- [ ] **"同步到方案"按钮内嵌在AI回复中** +- [ ] Action Card交互 + +#### 核心组件:SyncButton(同步按钮) + +```typescript +// SyncButton - 内嵌在AI回复中的同步按钮 +interface SyncButtonProps { + stageId: string; // 当前阶段 + extractedData: any; // 要同步的数据 + onSync: () => void; // 同步回调 + synced: boolean; // 是否已同步 +} + +// 在AI回复中的展示效果: +// ┌─────────────────────────────────────────────┐ +// │ AI: 我已整理出您的科学问题: │ +// │ "阿司匹林预防老年高血压患者中风研究" │ +// │ │ +// │ ┌──────────────────┐ │ +// │ │ ✅ 同步到方案 │ ← SyncButton │ +// │ └──────────────────┘ │ +// │ │ +// │ 同步后我们可以继续下一阶段。 │ +// └─────────────────────────────────────────────┘ +``` + +#### 页面布局设计 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Header: 研究方案制定 Agent 当前阶段: Step 3: 研究设计 │ +├───────────────────────────────────┬─────────────────────────────┤ +│ │ │ +│ Chat Area │ State Panel │ +│ ┌─────────────────────────────┐ │ ┌───────────────────────┐│ +│ │ │ │ │ 📋 方案状态 (Context) ││ +│ │ AI: 您好!我是研究方案... │ │ │ ││ +│ │ │ │ │ 01 科学问题 ✓ ││ +│ │ User: 我想做一个糖尿病... │ │ │ 阿司匹林预防老年... ││ +│ │ │ │ │ ││ +│ │ AI: 好的,我已整理出... │ │ │ 02 PICO ✓ ││ +│ │ ┌────────────────┐ │ │ │ P: ≥65岁高血压患者 ││ +│ │ │ ✅ 同步到方案 │ │ │ │ I: 阿司匹林100mg/d ││ +│ │ └────────────────┘ │ │ │ C: 安慰剂 ││ +│ │ │ │ │ O: 脑卒中发生率 ││ +│ │ User: 继续吧 │ │ │ ││ +│ │ │ │ │ 03 研究设计 (当前) ││ +│ │ AI: 好的,进入研究设计... │ │ │ RCT | 双盲 | 多中心 ││ +│ │ │ │ │ ││ +│ │ │ │ │ 04 样本量 ○ ││ +│ │ │ │ │ 待计算... ││ +│ │ │ │ │ ││ +│ │ │ │ │ 05 观察指标 ○ ││ +│ │ │ │ │ 待定义... ││ +│ └─────────────────────────────┘ │ ├───────────────────────┤│ +│ ┌─────────────────────────────┐ │ │ 进度: ████░░░░ 40% ││ +│ │ [输入您的指令...] [发送]│ │ │ ││ +│ └─────────────────────────────┘ │ │ [🚀 一键生成研究方案] ││ +│ │ │ (5阶段完成后可用) ││ +│ │ └───────────────────────┘│ +│ │ │ +└───────────────────────────────────┴─────────────────────────────┘ +``` + +--- + +### Sprint 4: 一键生成 + 集成测试(Week 4) + +**目标**:实现一键生成研究方案、Prompt调优、端到端测试 + +#### 任务列表 + +| ID | 任务 | 优先级 | 预估 | 负责人 | 状态 | +|----|------|--------|------|--------|------| +| 4.1 | **设计研究方案生成Prompt** | P0 | 4h | - | ⬜ | +| 4.2 | **实现ProtocolGenerationService** | P0 | 3h | - | ⬜ | +| 4.3 | **实现生成API端点** | P0 | 2h | - | ⬜ | +| 4.4 | **实现Word文档导出(docx库)** | P0 | 4h | - | ⬜ | +| 4.5 | **前端:一键生成按钮与预览页** | P0 | 4h | - | ⬜ | +| 4.6 | 实现样本量计算器Action Card | P1 | 2h | - | ⬜ | +| 4.7 | Prompt调优(5个阶段) | P0 | 6h | - | ⬜ | +| 4.8 | 端到端测试 | P0 | 4h | - | ⬜ | +| 4.9 | Bug修复和优化 | P0 | 4h | - | ⬜ | +| 4.10 | 文档更新 | P1 | 2h | - | ⬜ | + +**Sprint 4 产出**: +- [ ] **一键生成研究方案功能** +- [ ] **Word文档下载** +- [ ] 所有Prompt调优完成 +- [ ] 端到端测试通过 +- [ ] **MVP可交付** + +#### 一键生成研究方案 + +**触发条件**:5个阶段(科学问题、PICO、研究设计、样本量、观察指标)全部完成 + +**生成Prompt**: + +```markdown +你是一位资深的临床研究方法学专家,请基于以下核心要素生成一份完整、规范的临床研究方案。 + +## 核心要素 + +### 科学问题 +{{scientificQuestion.content}} + +### PICO要素 +- **研究人群(P)**: {{pico.P.value}} - {{pico.P.details}} +- **干预措施(I)**: {{pico.I.value}} - {{pico.I.details}} +- **对照措施(C)**: {{pico.C.value}} - {{pico.C.details}} +- **结局指标(O)**: {{pico.O.value}} - {{pico.O.details}} + +### 研究设计 +- 研究类型: {{studyDesign.type}} +- 盲法: {{studyDesign.blinding}} +- 随机化: {{studyDesign.randomization}} +- 研究周期: {{studyDesign.duration}} + +### 样本量 +- 总样本量: {{sampleSize.total}} +- 每组: {{sampleSize.perGroup}} +- 计算依据: {{sampleSize.justification}} + +### 观察指标 +**主要结局指标**: {{endpoints.primary}} +**次要结局指标**: {{endpoints.secondary}} +**安全性指标**: {{endpoints.safety}} + +--- + +请生成包含以下章节的完整研究方案: + +1. 研究背景与立题依据 +2. 研究目的 +3. 研究方法(研究类型、设计、干预、对照) +4. 受试者选择(入选标准、排除标准) +5. 观察指标与评价标准 +6. 统计分析计划 +7. 质量控制 +8. 伦理考虑 +9. 研究进度安排 + +请使用专业规范的学术语言,确保内容完整、逻辑清晰。 +``` + +**Word导出**: + +```typescript +// 使用 docx 库生成Word文档 +import { Document, Paragraph, TextRun, HeadingLevel, Packer } from 'docx'; + +async function generateProtocolWord(content: string, metadata: any): Promise { + const doc = new Document({ + sections: [{ + children: [ + // 封面 + new Paragraph({ + children: [new TextRun({ text: '临床研究方案', bold: true, size: 48 })], + heading: HeadingLevel.TITLE, + }), + // ... 正文(Markdown转docx) + ] + }] + }); + return await Packer.toBuffer(doc); +} +``` + +#### Reflexion(P2优先级,可延后) + +MVP阶段简化处理: +- 在同步前由AI在对话中进行基础检查 +- 完整的ReflexionEngine放到Phase 2实现 + +``` +AI回复示例: +"我已整理出您的PICO要素。在同步前,让我确认一下: + ✓ P: 研究人群已明确 + ✓ I: 干预措施已定义 + ✓ C: 对照组已设置 + ✓ O: 结局指标已确定 + + [✅ 同步到方案]" +``` + +--- + +## 三、Phase 2 计划概要(3周) + +### Sprint 5-6: 知识库建设(Week 5-6) + +| ID | 任务 | 优先级 | 预估 | +|----|------|--------|------| +| 6.1 | 创建knowledge_schema数据库迁移 | P0 | 2h | +| 6.2 | 实现KnowledgeDocService | P0 | 4h | +| 6.3 | 实现KnowledgeChunkService | P0 | 3h | +| 6.4 | 集成现有RAG引擎 | P0 | 4h | +| 6.5 | 实现RAGTool | P0 | 3h | +| 6.6 | 准备临床研究方法学知识文档 | P0 | 8h | +| 6.7 | 知识文档分块和向量化 | P0 | 4h | +| 6.8 | RAG检索测试和调优 | P0 | 4h | + +### Sprint 7: 高级功能(Week 7) + +| ID | 任务 | 优先级 | 预估 | +|----|------|--------|------| +| 7.1 | 实现Function Calling支持 | P1 | 4h | +| 7.2 | 实现ToolRegistry完整功能 | P1 | 3h | +| 7.3 | 添加文献检索工具 | P2 | 4h | +| 7.4 | 实现高级Reflexion(一致性校验) | P1 | 4h | +| 7.5 | 更多Action Card场景 | P1 | 4h | +| 7.6 | Phase 2集成测试 | P0 | 4h | + +--- + +## 四、Phase 3 计划概要(2周) + +### Sprint 8-9: 平台化(Week 8-9) + +| ID | 任务 | 优先级 | 预估 | +|----|------|--------|------| +| 8.1 | Agent配置管理后台 | P1 | 8h | +| 8.2 | Prompt在线编辑和调试 | P1 | 6h | +| 8.3 | Trace分析可视化 | P2 | 6h | +| 8.4 | 配置热更新支持 | P2 | 4h | +| 8.5 | 多Agent路由支持 | P1 | 4h | +| 8.6 | 统计分析Agent接入准备 | P1 | 4h | +| 8.7 | 最终测试和文档 | P0 | 4h | + +--- + +## 五、风险管理 + +### 5.1 已识别风险 + +| 风险 | 可能性 | 影响 | 缓解措施 | 负责人 | +|------|--------|------|----------|--------| +| LLM输出不稳定 | 高 | 中 | 多次测试,调优Prompt | - | +| 数据提取准确率低 | 中 | 高 | 增加验证逻辑,用户确认 | - | +| 前端集成复杂度 | 中 | 中 | 复用现有组件 | - | +| Prompt调优耗时 | 高 | 中 | 预留充足时间 | - | +| 知识库内容准备 | 中 | 高 | 提前准备,分批导入 | - | + +### 5.2 应急预案 + +**如果Phase 1延期**: +1. 优先保证核心流程(对话+提取) +2. Reflexion可简化为基础规则检查 +3. Action Card可延后到Phase 2 + +**如果数据提取准确率不达标**: +1. 增加用户确认环节 +2. 使用更强的模型(GPT-4) +3. 简化提取Schema + +--- + +## 六、质量保证 + +### 6.1 测试策略 + +| 测试类型 | 覆盖范围 | 执行时机 | +|----------|----------|----------| +| 单元测试 | 核心Service、工具函数 | 每次提交 | +| 集成测试 | API端点、数据库操作 | 每个Sprint | +| E2E测试 | 完整用户流程 | 里程碑交付 | +| Prompt测试 | 各阶段Prompt效果 | 持续调优 | + +### 6.2 验收标准 + +**Phase 1 验收标准**: +- [ ] 用户可完成5阶段研究方案要素制定 +- [ ] Context数据正确保存和展示 +- [ ] 阶段流转逻辑正确 +- [ ] Reflexion能阻止不完整的阶段提交 +- [ ] Action Card正确触发和展示 +- [ ] API响应时间 < 3s(不含LLM) +- [ ] 无P0级Bug + +### 6.3 Code Review检查点 + +- [ ] 类型定义完整 +- [ ] 错误处理完善 +- [ ] 日志记录规范 +- [ ] 代码风格一致 +- [ ] 无硬编码配置 +- [ ] 有必要的注释 + +--- + +## 七、资源需求 + +### 7.1 人力资源 + +| 角色 | 人数 | Phase 1 | Phase 2 | Phase 3 | +|------|------|---------|---------|---------| +| 后端开发 | 1 | 100% | 80% | 60% | +| 前端开发 | 1 | 60% | 40% | 40% | +| 产品/测试 | 1 | 30% | 30% | 40% | + +### 7.2 技术资源 + +| 资源 | 用途 | 预估费用 | +|------|------|----------| +| DeepSeek API | LLM调用 | ¥500/月 | +| 开发服务器 | 测试环境 | 现有 | +| 数据库 | PostgreSQL | 现有 | + +--- + +## 八、沟通计划 + +### 8.1 会议安排 + +| 会议 | 频率 | 参与者 | 目的 | +|------|------|--------|------| +| 每日站会 | 每日 | 开发团队 | 同步进度,解决阻塞 | +| Sprint评审 | 每周 | 全员 | 演示成果,收集反馈 | +| 技术讨论 | 按需 | 相关开发 | 解决技术难题 | + +### 8.2 文档更新 + +| 文档 | 更新频率 | 负责人 | +|------|----------|--------| +| 开发计划 | 每Sprint | PM | +| 技术文档 | 实时 | 开发者 | +| API文档 | 代码变更时 | 后端 | +| 用户手册 | Phase完成时 | 产品 | + +--- + +## 九、检查清单 + +### Phase 1 启动前检查 + +- [ ] 需求文档已确认 +- [ ] 技术方案已评审 +- [ ] 数据库设计已确认 +- [ ] 开发环境已就绪 +- [ ] 相关人员已到位 + +### 每Sprint完成检查 + +- [ ] 所有任务已完成或明确延期原因 +- [ ] 代码已Code Review +- [ ] 测试已通过 +- [ ] 文档已更新 +- [ ] 下Sprint计划已明确 + +### Phase 1 交付检查 + +- [ ] 所有功能已实现 +- [ ] 验收标准全部达成 +- [ ] 无遗留P0/P1 Bug +- [ ] 文档完整 +- [ ] 演示准备就绪 + diff --git a/docs/03-业务模块/AIA-AI智能问答/04-开发计划/04-Protocol_Agent开发计划/README.md b/docs/03-业务模块/AIA-AI智能问答/04-开发计划/04-Protocol_Agent开发计划/README.md new file mode 100644 index 00000000..c3183bd0 --- /dev/null +++ b/docs/03-业务模块/AIA-AI智能问答/04-开发计划/04-Protocol_Agent开发计划/README.md @@ -0,0 +1,79 @@ +# Protocol Agent 开发计划文档 + +> 创建日期:2026-01-24 +> 状态:待开发 + +--- + +## 📚 文档目录 + +| 序号 | 文档 | 说明 | +|:----:|------|------| +| 00 | [开发计划总览](./00-开发计划总览.md) | 项目背景、决策记录、核心架构概览 | +| 01 | [架构设计](./01-架构设计.md) | 五层Agent架构、各层详细设计、执行流程 | +| 02 | [数据库设计](./02-数据库设计.md) | 完整Prisma Schema、ER图、迁移计划 | +| 03 | [代码结构设计](./03-代码结构设计.md) | 目录结构、核心类型、API设计、前端组件 | +| 04 | [分阶段实施计划](./04-分阶段实施计划.md) | Sprint划分、任务列表、风险管理、验收标准 | + +--- + +## 🎯 项目概述 + +Protocol Agent(研究方案制定助手)是AIA模块的第13个智能体入口,通过多轮对话引导用户完成临床研究方案的核心要素制定。 + +### 核心架构 + +``` +Query → Planner → Executor → Tools → Reflection +(意图识别) → (规划) → (执行) → (工具) → (反思) +``` + +### 开发周期 + +- **Phase 1**: MVP完整功能 (4周) - 包含一键生成研究方案 +- **Phase 2**: 知识增强 (3周) +- **Phase 3**: 平台化 (2周) + +**总周期:9周** + +--- + +## ✅ 已确认决策 + +| 决策项 | 选择 | +|--------|------| +| 入口方式 | B - Protocol Agent作为第13个入口 | +| 状态流转 | B - 用户按钮确认触发 | +| 数据提取 | C - 异步提取+LLM结构化 | +| Action Card | B - 规则触发 | +| Reflexion | B - Prompt-based | + +--- + +## 📋 快速导航 + +### 需要了解业务需求? +→ 查看 [Protocol Agent PRD](../00-系统设计/Protocol_Agent_PRD_v1.0.md) + +### 需要了解技术架构? +→ 查看 [01-架构设计](./01-架构设计.md) + +### 需要了解数据库表结构? +→ 查看 [02-数据库设计](./02-数据库设计.md) + +### 需要开始编码? +→ 查看 [03-代码结构设计](./03-代码结构设计.md) + +### 需要了解任务安排? +→ 查看 [04-分阶段实施计划](./04-分阶段实施计划.md) + +--- + +## 📎 相关文档 + +- [AIA模块当前状态](../00-模块当前状态与开发指南.md) +- [Protocol Agent PRD v1.0](../00-系统设计/Protocol_Agent_PRD_v1.0.md) +- [MVP简化指南](../00-系统设计/Protocol_Agent_Development_Simplification_Guide.md) +- [架构设计V3](../02-技术设计/Protocol_Agent_Architecture_Design_V3.md) +- [技术实现V3](../02-技术设计/Protocol_Agent_Technical_Implementation_V3.md) + diff --git a/docs/03-业务模块/AIA-AI智能问答/06-开发记录/2026-01-18-Prompt管理系统集成.md b/docs/03-业务模块/AIA-AI智能问答/06-开发记录/2026-01-18-Prompt管理系统集成.md index 16f28cbb..7d809381 100644 --- a/docs/03-业务模块/AIA-AI智能问答/06-开发记录/2026-01-18-Prompt管理系统集成.md +++ b/docs/03-业务模块/AIA-AI智能问答/06-开发记录/2026-01-18-Prompt管理系统集成.md @@ -199,3 +199,5 @@ export type AgentStage = 'topic' | 'design' | 'review' | 'data' | 'writing'; + + diff --git a/docs/03-业务模块/AIA-AI智能问答/06-开发记录/2026-01-24-Protocol_Agent_MVP开发完成.md b/docs/03-业务模块/AIA-AI智能问答/06-开发记录/2026-01-24-Protocol_Agent_MVP开发完成.md new file mode 100644 index 00000000..7b205759 --- /dev/null +++ b/docs/03-业务模块/AIA-AI智能问答/06-开发记录/2026-01-24-Protocol_Agent_MVP开发完成.md @@ -0,0 +1,293 @@ +# Protocol Agent MVP 开发完成记录 + +> 日期:2026-01-24 +> 开发者:AI Assistant +> 状态:✅ Sprint 1-3 完成 + +--- + +## 📋 开发概述 + +完成了**Protocol Agent(研究方案制定助手)**的MVP版本开发,建立了可复用的Agent框架,实现了5阶段对话式研究方案制定流程。 + +--- + +## 🎯 完成的功能 + +### 一、通用Agent框架(可复用) + +| 组件 | 功能 | 代码量 | +|------|------|--------| +| **Query Layer** | 意图识别、实体提取 | QueryAnalyzer (271行) | +| **Planner Layer** | 状态机、阶段管理 | StageManager (284行) | +| **Executor Layer** | LLM调用、响应构建 | BaseAgentOrchestrator (562行) | +| **Tools Layer** | 工具注册、调用 | - | +| **Reflection Layer** | 质量检查 | ReflexionEngine (部分) | +| **横切关注点** | 配置加载、追踪日志 | ConfigLoader (267行), TraceLogger (350行) | + +**框架特点**: +- ✅ 清晰的五层架构 +- ✅ 完整的类型定义 (382行) +- ✅ 可扩展设计(为后续统计Agent、数据清洗Agent铺路) + +--- + +### 二、Protocol Agent核心实现 + +#### 后端实现 + +| 模块 | 文件 | 功能 | +|------|------|------| +| **ProtocolOrchestrator** | 322行 | 研究方案Agent编排器 | +| **ProtocolContextService** | 290行 | 上下文CRUD管理 | +| **PromptBuilder** | 286行 | Prompt构建与渲染 | +| **LLMServiceAdapter** | 184行 | 集成现有LLM服务 | +| **ProtocolAgentController** | 288行 | HTTP请求处理 | +| **Routes** | 154行 | API路由定义 | + +**API端点**: +``` +POST /api/v1/aia/protocol-agent/message 发送消息 +POST /api/v1/aia/protocol-agent/sync 同步阶段数据 +GET /api/v1/aia/protocol-agent/context/:id 获取上下文状态 +POST /api/v1/aia/protocol-agent/generate 一键生成方案 +GET /api/v1/aia/protocol-agent/generation/:id 获取生成结果 +POST /api/v1/aia/protocol-agent/generation/:id/export 导出Word +``` + +#### 前端实现 + +| 模块 | 文件 | 功能 | +|------|------|------| +| **ProtocolAgentPage** | 223行 | 主页面(三栏布局) | +| **ChatArea** | 292行 | 聊天区域(扩展功能) | +| **StatePanel** | 60行 | 状态面板 | +| **StageCard** | 225行 | 阶段状态卡片 | +| **SyncButton** | 48行 | 同步按钮 | +| **ActionCard** | 66行 | Action Card | +| **ReflexionMessage** | 52行 | Reflexion消息 | +| **Hooks** | 2个 | useProtocolContext, useProtocolConversations | +| **CSS** | 608行 | 100%还原原型图样式 | + +--- + +### 三、数据库设计 + +#### agent_schema(通用Agent框架) + +| 表名 | 说明 | +|------|------| +| `agent_definitions` | Agent定义 | +| `agent_stages` | 阶段配置 | +| `agent_prompts` | Prompt模板 | +| `agent_sessions` | 会话状态 | +| `agent_traces` | 执行追踪 | +| `reflexion_rules` | 反思规则 | + +#### protocol_schema(Protocol Agent专用) + +| 表名 | 说明 | +|------|------| +| `protocol_contexts` | 研究方案上下文(5阶段数据) | +| `protocol_generations` | 方案生成记录 | + +#### 初始配置数据 + +- ✅ 1个Agent定义(protocol_agent) +- ✅ 5个阶段配置 +- ✅ 9个Prompt模板 +- ✅ 4个Reflexion规则 + +--- + +## 🧪 测试结果 + +### API测试(100%通过) + +| 测试项 | 状态 | +|--------|------| +| 健康检查 | ✅ | +| 数据库配置 | ✅ | +| 未授权访问 (401) | ✅ | +| 获取不存在上下文 (404) | ✅ | +| 登录认证 | ✅ | +| 创建对话 | ✅ | +| **发送消息** | ✅ | +| **同步阶段数据** | ✅ | +| **获取上下文状态** | ✅ | +| **生成方案检查** | ✅ | + +--- + +## 📊 核心设计特点 + +### 5阶段研究方案流程 + +``` +01 科学问题 → 02 PICO → 03 研究设计 → 04 样本量 → 05 观察指标 + ↓ + [🚀 一键生成研究方案] +``` + +### 对话驱动交互 + +``` +用户与AI对话 → AI整理数据 → 显示"✅ 同步到方案"按钮 + ↓ + 用户点击同步 + ↓ + State Panel实时更新 + ↓ + AI询问:"继续下一阶段吗?" + ↓ + 用户说"继续" → 进入下一阶段 +``` + +### 观察指标详细结构 + +- 📊 **基线指标**:人口学特征、临床病史、实验室检查 +- 💊 **暴露指标**:干预措施、对照措施、剂量、持续时间 +- 🎯 **结局指标**:主要结局、次要结局、安全性指标 +- ⚠️ **混杂因素**:需要调整的协变量 + +### UI设计亮点 + +| 特点 | 实现 | +|------|------| +| **Gemini折叠侧边栏** | 48px ↔️ 280px,悬停展开 | +| **100%还原原型图** | Tailwind样式,Lucide图标 | +| **Indigo主题** | 紫蓝渐变,测试徽章 | +| **同步动画** | flash-update闪烁效果 | +| **Reflexion样式** | 紫色左边框 | + +--- + +## 🔧 技术栈 + +### 后端 + +- Fastify + TypeScript +- Prisma (agent_schema + protocol_schema) +- LLMFactory(集成DeepSeek/Qwen/GPT-5/Claude) +- pg-boss(异步任务,Phase 2) + +### 前端 + +- React 19 + TypeScript +- React Router v7 +- Ant Design X(复用Chat组件) +- Tailwind CSS(100%还原原型图) +- Lucide Icons + +--- + +## 📝 待完成功能(Phase 2+) + +### Sprint 4: 一键生成研究方案(Week 4) + +- [ ] 实现完整的方案生成Prompt +- [ ] 集成LLM调用生成完整方案 +- [ ] Word文档导出(docx库) +- [ ] 方案预览与在线编辑 +- [ ] 重新生成功能 + +### Sprint 5-6: 知识增强(Week 5-6) + +- [ ] EKB知识库建设 +- [ ] RAG检索集成 +- [ ] Function Calling工具调用 +- [ ] 更多Action Card场景 + +### Sprint 7-8: 平台化(Week 7-8) + +- [ ] 后台配置管理界面 +- [ ] Prompt在线调试 +- [ ] 完整Trace分析 +- [ ] 多Agent支持准备 + +--- + +## ⚠️ 已知问题 + +1. **前端路由问题**(未解决) + - 点击Protocol Agent卡片后未正常跳转 + - 需要调试路由配置和组件加载 + +2. **LLM响应解析**(待完善) + - 当前使用简单的关键词检测决定是否显示同步按钮 + - 应该解析``XML标签 + +3. **Prompt调优**(待进行) + - 5个阶段的Prompt需要实际测试调优 + - 提取Schema需要优化 + +--- + +## 📈 代码统计 + +### 后端 + +| 类别 | 文件数 | 代码行数 | +|------|--------|----------| +| 类型定义 | 1 | 382 | +| Agent框架 | 5 | 1,734 | +| Protocol Agent | 6 | 1,662 | +| 数据库种子 | 1 | 560 | +| **后端总计** | **13** | **4,338行** | + +### 前端 + +| 类别 | 文件数 | 代码行数 | +|------|--------|----------| +| 页面组件 | 1 | 223 | +| 子组件 | 6 | 849 | +| Hooks | 2 | 226 | +| 类型定义 | 1 | 165 | +| 样式 | 1 | 608 | +| 路由更新 | 3 | - | +| **前端总计** | **14** | **2,071行** | + +### 数据库 + +- ✅ 8张新表(agent_schema: 6张,protocol_schema: 2张) +- ✅ 初始配置数据已seed + +--- + +## 🏆 技术亮点 + +1. **可复用Agent框架** - 为后续Agent开发打下基础 +2. **Query→Plan→Execute→Reflect** - 完整的Agent闭环 +3. **对话驱动交互** - 自然的同步确认机制 +4. **100%还原原型图** - 精致的UI设计 +5. **Gemini折叠侧边栏** - 优秀的UX设计 +6. **详细的观察指标** - 4类指标完整结构 + +--- + +## 📚 相关文档 + +- [开发计划总览](../04-开发计划/04-Protocol_Agent开发计划/00-开发计划总览.md) +- [架构设计](../04-开发计划/04-Protocol_Agent开发计划/01-架构设计.md) +- [数据库设计](../04-开发计划/04-Protocol_Agent开发计划/02-数据库设计.md) +- [代码结构设计](../04-开发计划/04-Protocol_Agent开发计划/03-代码结构设计.md) +- [分阶段实施计划](../04-开发计划/04-Protocol_Agent开发计划/04-分阶段实施计划.md) + +--- + +## 🎉 里程碑 + +- ✅ **M1 (Week 2)**: Agent框架 + 基础对话 +- ✅ **M2 (Week 4)**: 完整5阶段流程 + 前端UI +- ⏳ **M3 (Week 5)**: 一键生成研究方案 + +--- + +**下一步工作**: +1. 修复前端路由问题 +2. 实现一键生成完整研究方案 +3. Word文档导出 +4. Prompt调优测试 + +**预计交付时间**:Week 5(1周后) + diff --git a/docs/03-业务模块/ASL-AI智能文献/04-开发计划/05-全文复筛前端开发计划.md b/docs/03-业务模块/ASL-AI智能文献/04-开发计划/05-全文复筛前端开发计划.md index 6fcd365d..38025226 100644 --- a/docs/03-业务模块/ASL-AI智能文献/04-开发计划/05-全文复筛前端开发计划.md +++ b/docs/03-业务模块/ASL-AI智能文献/04-开发计划/05-全文复筛前端开发计划.md @@ -1305,6 +1305,8 @@ interface FulltextScreeningResult { + + diff --git a/docs/03-业务模块/ASL-AI智能文献/05-开发记录/2025-01-23_全文复筛前端开发完成.md b/docs/03-业务模块/ASL-AI智能文献/05-开发记录/2025-01-23_全文复筛前端开发完成.md index 4c9dad2d..752e143f 100644 --- a/docs/03-业务模块/ASL-AI智能文献/05-开发记录/2025-01-23_全文复筛前端开发完成.md +++ b/docs/03-业务模块/ASL-AI智能文献/05-开发记录/2025-01-23_全文复筛前端开发完成.md @@ -419,6 +419,8 @@ GET /api/v1/asl/fulltext-screening/tasks/:taskId/export + + diff --git a/docs/03-业务模块/ASL-AI智能文献/05-开发记录/2025-01-23_全文复筛前端逻辑调整.md b/docs/03-业务模块/ASL-AI智能文献/05-开发记录/2025-01-23_全文复筛前端逻辑调整.md index 81ff31c0..8754a5b3 100644 --- a/docs/03-业务模块/ASL-AI智能文献/05-开发记录/2025-01-23_全文复筛前端逻辑调整.md +++ b/docs/03-业务模块/ASL-AI智能文献/05-开发记录/2025-01-23_全文复筛前端逻辑调整.md @@ -362,6 +362,8 @@ Linter错误:0个 + + diff --git a/docs/03-业务模块/ASL-AI智能文献/05-开发记录/2025-11-23_Day5_全文复筛API开发.md b/docs/03-业务模块/ASL-AI智能文献/05-开发记录/2025-11-23_Day5_全文复筛API开发.md index bab872e2..1875a9d5 100644 --- a/docs/03-业务模块/ASL-AI智能文献/05-开发记录/2025-11-23_Day5_全文复筛API开发.md +++ b/docs/03-业务模块/ASL-AI智能文献/05-开发记录/2025-11-23_Day5_全文复筛API开发.md @@ -521,6 +521,8 @@ Failed to open file '\\tmp\\extraction_service\\temp_10000_test.pdf' + + diff --git a/docs/03-业务模块/ASL-AI智能文献/05-开发记录/2026-01-18_智能文献检索DeepSearch集成.md b/docs/03-业务模块/ASL-AI智能文献/05-开发记录/2026-01-18_智能文献检索DeepSearch集成.md index 8b62a989..b0b688c1 100644 --- a/docs/03-业务模块/ASL-AI智能文献/05-开发记录/2026-01-18_智能文献检索DeepSearch集成.md +++ b/docs/03-业务模块/ASL-AI智能文献/05-开发记录/2026-01-18_智能文献检索DeepSearch集成.md @@ -180,3 +180,5 @@ UNIFUNCS_API_KEY=sk-xxxx + + diff --git a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_AI_Few-shot示例库.md b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_AI_Few-shot示例库.md index d7e4e2dd..5c52f414 100644 --- a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_AI_Few-shot示例库.md +++ b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_AI_Few-shot示例库.md @@ -587,6 +587,8 @@ df['creatinine'] = pd.to_numeric(df['creatinine'], errors='coerce') + + diff --git a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Bug修复总结_2025-12-08.md b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Bug修复总结_2025-12-08.md index f68752ba..8573755a 100644 --- a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Bug修复总结_2025-12-08.md +++ b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Bug修复总结_2025-12-08.md @@ -425,6 +425,8 @@ npm run dev + + diff --git a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Day3开发计划.md b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Day3开发计划.md index 1345718e..1961409e 100644 --- a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Day3开发计划.md +++ b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Day3开发计划.md @@ -1002,6 +1002,8 @@ export const aiController = new AIController(); + + diff --git a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Day4-5前端开发计划.md b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Day4-5前端开发计划.md index 9a98acc7..42bf897a 100644 --- a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Day4-5前端开发计划.md +++ b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Day4-5前端开发计划.md @@ -1336,6 +1336,8 @@ npm install react-markdown + + diff --git a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Pivot列顺序优化总结.md b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Pivot列顺序优化总结.md index 9281527e..35459655 100644 --- a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Pivot列顺序优化总结.md +++ b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Pivot列顺序优化总结.md @@ -244,6 +244,8 @@ FMA___基线 | FMA___1个月 | FMA___2个月 + + diff --git a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_方案B实施总结_2025-12-09.md b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_方案B实施总结_2025-12-09.md index 68a20353..f1361098 100644 --- a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_方案B实施总结_2025-12-09.md +++ b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_方案B实施总结_2025-12-09.md @@ -402,6 +402,8 @@ formula = "FMA总分(0-100) / 100" + + diff --git a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_缺失值处理_开发进度_2025-12-10.md b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_缺失值处理_开发进度_2025-12-10.md index 828924c8..4514f0f2 100644 --- a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_缺失值处理_开发进度_2025-12-10.md +++ b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_缺失值处理_开发进度_2025-12-10.md @@ -236,6 +236,8 @@ async handleFillnaMice(request, reply) { + + diff --git a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_缺失值处理功能_更新说明.md b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_缺失值处理功能_更新说明.md index 59044c27..8e549098 100644 --- a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_缺失值处理功能_更新说明.md +++ b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_缺失值处理功能_更新说明.md @@ -208,6 +208,8 @@ method: 'mean' | 'median' | 'mode' | 'constant' | 'ffill' | 'bfill' + + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-02_工作总结.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-02_工作总结.md index c5c04b89..ce295948 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-02_工作总结.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-02_工作总结.md @@ -358,6 +358,8 @@ Changes: + + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-06_工具C_Day1开发完成总结.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-06_工具C_Day1开发完成总结.md index 7f631dc1..d8accafe 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-06_工具C_Day1开发完成总结.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-06_工具C_Day1开发完成总结.md @@ -430,6 +430,8 @@ cd path; command + + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-06_工具C_Day2开发完成总结.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-06_工具C_Day2开发完成总结.md index 73228f47..a60222c6 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-06_工具C_Day2开发完成总结.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-06_工具C_Day2开发完成总结.md @@ -659,6 +659,8 @@ import { logger } from '../../../../common/logging/index.js'; + + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_AI对话核心功能增强总结.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_AI对话核心功能增强总结.md index e5c0717f..184f906a 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_AI对话核心功能增强总结.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_AI对话核心功能增强总结.md @@ -663,6 +663,8 @@ Content-Length: 45234 + + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_Bug修复_DataGrid空数据防御.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_Bug修复_DataGrid空数据防御.md index b9903b40..62426325 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_Bug修复_DataGrid空数据防御.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_Bug修复_DataGrid空数据防御.md @@ -315,6 +315,8 @@ Response: + + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_Day5_Ant-Design-X重构完成.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_Day5_Ant-Design-X重构完成.md index 7b09f404..675a6d1b 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_Day5_Ant-Design-X重构完成.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_Day5_Ant-Design-X重构完成.md @@ -468,6 +468,8 @@ Response: + + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_Day5最终总结.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_Day5最终总结.md index 9868fe93..85e4af20 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_Day5最终总结.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_Day5最终总结.md @@ -462,6 +462,8 @@ import { ChatContainer } from '@/shared/components/Chat'; + + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_UI优化与Bug修复.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_UI优化与Bug修复.md index 688ddec4..c5eaf425 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_UI优化与Bug修复.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_UI优化与Bug修复.md @@ -372,6 +372,8 @@ const initialMessages = defaultMessages.length > 0 ? defaultMessages : [{ + + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_后端API完整对接完成.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_后端API完整对接完成.md index d3b4078b..4daf689e 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_后端API完整对接完成.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_后端API完整对接完成.md @@ -412,6 +412,8 @@ python main.py + + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_完整UI优化与功能增强.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_完整UI优化与功能增强.md index f9be9baf..d98e3c24 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_完整UI优化与功能增强.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_完整UI优化与功能增强.md @@ -660,6 +660,8 @@ http://localhost:5173/data-cleaning/tool-c + + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_工具C_Day4前端基础完成.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_工具C_Day4前端基础完成.md index b183eafa..1ae715f7 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_工具C_Day4前端基础完成.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_工具C_Day4前端基础完成.md @@ -270,6 +270,8 @@ Day 5 (6-8小时): + + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/DC模块重建完成总结-Day1.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/DC模块重建完成总结-Day1.md index 242a7159..6fb9c6e5 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/DC模块重建完成总结-Day1.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/DC模块重建完成总结-Day1.md @@ -448,6 +448,8 @@ Docs: docs/03-业务模块/DC-数据清洗整理/06-开发记录/DC模块重建 + + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Phase1-Portal页面开发完成-2025-12-02.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Phase1-Portal页面开发完成-2025-12-02.md index 962b23ce..a477720f 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Phase1-Portal页面开发完成-2025-12-02.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Phase1-Portal页面开发完成-2025-12-02.md @@ -423,6 +423,8 @@ const mockAssets: Asset[] = [ + + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Phase2-ToolB-Step1-2开发完成-2025-12-03.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Phase2-ToolB-Step1-2开发完成-2025-12-03.md index 847edf48..15fbc754 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Phase2-ToolB-Step1-2开发完成-2025-12-03.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Phase2-ToolB-Step1-2开发完成-2025-12-03.md @@ -407,6 +407,8 @@ frontend-v2/src/modules/dc/ + + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Portal页面UI优化-2025-12-02.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Portal页面UI优化-2025-12-02.md index 66617076..4da0de4b 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Portal页面UI优化-2025-12-02.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Portal页面UI优化-2025-12-02.md @@ -367,6 +367,8 @@ + + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Tool-B-MVP完成总结-2025-12-03.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Tool-B-MVP完成总结-2025-12-03.md index 6199fd55..53d078e8 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Tool-B-MVP完成总结-2025-12-03.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Tool-B-MVP完成总结-2025-12-03.md @@ -321,6 +321,8 @@ ConflictDetectionService // 冲突检测(字段级对比) + + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/ToolB-UI优化-2025-12-03.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/ToolB-UI优化-2025-12-03.md index 25b36b97..5edbd5c5 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/ToolB-UI优化-2025-12-03.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/ToolB-UI优化-2025-12-03.md @@ -370,6 +370,8 @@ + + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/ToolB-UI优化-Round2-2025-12-03.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/ToolB-UI优化-Round2-2025-12-03.md index e08227ca..6c1618d1 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/ToolB-UI优化-Round2-2025-12-03.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/ToolB-UI优化-Round2-2025-12-03.md @@ -333,6 +333,8 @@ + + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/ToolB浏览器测试计划-2025-12-03.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/ToolB浏览器测试计划-2025-12-03.md index cb663fe9..288f359d 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/ToolB浏览器测试计划-2025-12-03.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/ToolB浏览器测试计划-2025-12-03.md @@ -397,6 +397,8 @@ + + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/后端API测试报告-2025-12-02.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/后端API测试报告-2025-12-02.md index a625165c..6dbeb108 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/后端API测试报告-2025-12-02.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/后端API测试报告-2025-12-02.md @@ -485,6 +485,8 @@ Tool B后端代码**100%复用**了平台通用能力层,无任何重复开发 + + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/待办事项-下一步工作.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/待办事项-下一步工作.md index 00e16a71..eeff2acd 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/待办事项-下一步工作.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/待办事项-下一步工作.md @@ -331,6 +331,8 @@ + + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/数据库验证报告-2025-12-02.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/数据库验证报告-2025-12-02.md index 992bb1c8..75c0da4c 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/数据库验证报告-2025-12-02.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/数据库验证报告-2025-12-02.md @@ -262,6 +262,8 @@ $ node scripts/check-dc-tables.mjs + + diff --git a/docs/03-业务模块/DC-数据清洗整理/07-技术债务/Tool-B技术债务清单.md b/docs/03-业务模块/DC-数据清洗整理/07-技术债务/Tool-B技术债务清单.md index 848229fd..ae56ecc2 100644 --- a/docs/03-业务模块/DC-数据清洗整理/07-技术债务/Tool-B技术债务清单.md +++ b/docs/03-业务模块/DC-数据清洗整理/07-技术债务/Tool-B技术债务清单.md @@ -495,6 +495,8 @@ ${fields.map((f, i) => `${i + 1}. ${f.name}:${f.desc}`).join('\n')} + + diff --git a/docs/03-业务模块/IIT Manager Agent/02-技术设计/IIT Manager Agent 技术路径与架构设计.md b/docs/03-业务模块/IIT Manager Agent/02-技术设计/IIT Manager Agent 技术路径与架构设计.md index 88a78bec..eca0d1fd 100644 --- a/docs/03-业务模块/IIT Manager Agent/02-技术设计/IIT Manager Agent 技术路径与架构设计.md +++ b/docs/03-业务模块/IIT Manager Agent/02-技术设计/IIT Manager Agent 技术路径与架构设计.md @@ -700,6 +700,8 @@ private async processMessageAsync(xmlData: any) { + + diff --git a/docs/03-业务模块/IIT Manager Agent/04-开发计划/REDCap对接技术方案与实施指南.md b/docs/03-业务模块/IIT Manager Agent/04-开发计划/REDCap对接技术方案与实施指南.md index 5af5871b..a68ab6d8 100644 --- a/docs/03-业务模块/IIT Manager Agent/04-开发计划/REDCap对接技术方案与实施指南.md +++ b/docs/03-业务模块/IIT Manager Agent/04-开发计划/REDCap对接技术方案与实施指南.md @@ -1094,6 +1094,8 @@ async function testIntegration() { + + diff --git a/docs/03-业务模块/IIT Manager Agent/04-开发计划/企业微信注册指南.md b/docs/03-业务模块/IIT Manager Agent/04-开发计划/企业微信注册指南.md index 15aa8896..b3d40add 100644 --- a/docs/03-业务模块/IIT Manager Agent/04-开发计划/企业微信注册指南.md +++ b/docs/03-业务模块/IIT Manager Agent/04-开发计划/企业微信注册指南.md @@ -235,6 +235,8 @@ Content-Type: application/json + + diff --git a/docs/03-业务模块/IIT Manager Agent/06-开发记录/2026-01-04-Dify知识库集成开发记录.md b/docs/03-业务模块/IIT Manager Agent/06-开发记录/2026-01-04-Dify知识库集成开发记录.md index 9465c8a3..07a36831 100644 --- a/docs/03-业务模块/IIT Manager Agent/06-开发记录/2026-01-04-Dify知识库集成开发记录.md +++ b/docs/03-业务模块/IIT Manager Agent/06-开发记录/2026-01-04-Dify知识库集成开发记录.md @@ -655,6 +655,8 @@ REDCap API: exportRecords success { recordCount: 1 } + + diff --git a/docs/03-业务模块/IIT Manager Agent/06-开发记录/Day2-REDCap实时集成开发完成记录.md b/docs/03-业务模块/IIT Manager Agent/06-开发记录/Day2-REDCap实时集成开发完成记录.md index e2bb0443..369dc0a7 100644 --- a/docs/03-业务模块/IIT Manager Agent/06-开发记录/Day2-REDCap实时集成开发完成记录.md +++ b/docs/03-业务模块/IIT Manager Agent/06-开发记录/Day2-REDCap实时集成开发完成记录.md @@ -661,6 +661,8 @@ backend/src/modules/iit-manager/ + + diff --git a/docs/03-业务模块/IIT Manager Agent/06-开发记录/Day3-企业微信集成与端到端测试完成记录.md b/docs/03-业务模块/IIT Manager Agent/06-开发记录/Day3-企业微信集成与端到端测试完成记录.md index 383b1ffd..98aa313e 100644 --- a/docs/03-业务模块/IIT Manager Agent/06-开发记录/Day3-企业微信集成与端到端测试完成记录.md +++ b/docs/03-业务模块/IIT Manager Agent/06-开发记录/Day3-企业微信集成与端到端测试完成记录.md @@ -811,6 +811,8 @@ CREATE TABLE iit_schema.wechat_tokens ( + + diff --git a/docs/03-业务模块/IIT Manager Agent/06-开发记录/Day3-企业微信集成开发完成记录.md b/docs/03-业务模块/IIT Manager Agent/06-开发记录/Day3-企业微信集成开发完成记录.md index 3072b7f1..402c32fa 100644 --- a/docs/03-业务模块/IIT Manager Agent/06-开发记录/Day3-企业微信集成开发完成记录.md +++ b/docs/03-业务模块/IIT Manager Agent/06-开发记录/Day3-企业微信集成开发完成记录.md @@ -568,6 +568,8 @@ Day 3 的开发工作虽然遇到了多个技术问题,但最终成功完成 + + diff --git a/docs/03-业务模块/IIT Manager Agent/06-开发记录/Phase1.5-AI对话集成REDCap完成记录.md b/docs/03-业务模块/IIT Manager Agent/06-开发记录/Phase1.5-AI对话集成REDCap完成记录.md index 99657868..3903cfa7 100644 --- a/docs/03-业务模块/IIT Manager Agent/06-开发记录/Phase1.5-AI对话集成REDCap完成记录.md +++ b/docs/03-业务模块/IIT Manager Agent/06-开发记录/Phase1.5-AI对话集成REDCap完成记录.md @@ -335,6 +335,8 @@ AI: "出生日期:2017-01-04 + + diff --git a/docs/03-业务模块/IIT Manager Agent/06-开发记录/V1.1更新完成报告.md b/docs/03-业务模块/IIT Manager Agent/06-开发记录/V1.1更新完成报告.md index 50ae5e2b..7af0ca09 100644 --- a/docs/03-业务模块/IIT Manager Agent/06-开发记录/V1.1更新完成报告.md +++ b/docs/03-业务模块/IIT Manager Agent/06-开发记录/V1.1更新完成报告.md @@ -279,6 +279,8 @@ Day 4: REDCap EM(Webhook推送)← 作为增强,而非核心 + + diff --git a/docs/03-业务模块/IIT Manager Agent/07-技术债务/IIT Manager Agent 技术债务清单.md b/docs/03-业务模块/IIT Manager Agent/07-技术债务/IIT Manager Agent 技术债务清单.md index eefca42b..bd519038 100644 --- a/docs/03-业务模块/IIT Manager Agent/07-技术债务/IIT Manager Agent 技术债务清单.md +++ b/docs/03-业务模块/IIT Manager Agent/07-技术债务/IIT Manager Agent 技术债务清单.md @@ -693,6 +693,8 @@ const answer = `根据研究方案[1]和CRF表格[2],纳入标准包括: + + diff --git a/docs/03-业务模块/INST-机构管理端/00-模块当前状态与开发指南.md b/docs/03-业务模块/INST-机构管理端/00-模块当前状态与开发指南.md index 29a1385b..f74500bd 100644 --- a/docs/03-业务模块/INST-机构管理端/00-模块当前状态与开发指南.md +++ b/docs/03-业务模块/INST-机构管理端/00-模块当前状态与开发指南.md @@ -453,3 +453,5 @@ export const calculateAvailableQuota = async (tenantId: string) => { + + diff --git a/docs/03-业务模块/INST-机构管理端/README.md b/docs/03-业务模块/INST-机构管理端/README.md index a8ced8d7..6efef700 100644 --- a/docs/03-业务模块/INST-机构管理端/README.md +++ b/docs/03-业务模块/INST-机构管理端/README.md @@ -326,3 +326,5 @@ https://platform.example.com/t/pharma-abc/login + + diff --git a/docs/03-业务模块/PKB-个人知识库/06-开发记录/2026-01-07-前端迁移与批处理功能完善.md b/docs/03-业务模块/PKB-个人知识库/06-开发记录/2026-01-07-前端迁移与批处理功能完善.md index 044ab2fd..2d206224 100644 --- a/docs/03-业务模块/PKB-个人知识库/06-开发记录/2026-01-07-前端迁移与批处理功能完善.md +++ b/docs/03-业务模块/PKB-个人知识库/06-开发记录/2026-01-07-前端迁移与批处理功能完善.md @@ -373,6 +373,8 @@ const newResults = resultsData.map((docResult: any) => ({ + + diff --git a/docs/03-业务模块/PKB-个人知识库/06-开发记录/2026-01-07_PKB模块前端V3设计实现.md b/docs/03-业务模块/PKB-个人知识库/06-开发记录/2026-01-07_PKB模块前端V3设计实现.md index 41f85a33..5738112f 100644 --- a/docs/03-业务模块/PKB-个人知识库/06-开发记录/2026-01-07_PKB模块前端V3设计实现.md +++ b/docs/03-业务模块/PKB-个人知识库/06-开发记录/2026-01-07_PKB模块前端V3设计实现.md @@ -246,6 +246,8 @@ const chatApi = axios.create({ + + diff --git a/docs/03-业务模块/Redcap/01-部署与配置/10-REDCap_Docker部署操作手册.md b/docs/03-业务模块/Redcap/01-部署与配置/10-REDCap_Docker部署操作手册.md index 1ac3b21e..58fd965b 100644 --- a/docs/03-业务模块/Redcap/01-部署与配置/10-REDCap_Docker部署操作手册.md +++ b/docs/03-业务模块/Redcap/01-部署与配置/10-REDCap_Docker部署操作手册.md @@ -783,6 +783,8 @@ docker exec redcap-apache php /tmp/create-redcap-password.php + + diff --git a/docs/03-业务模块/Redcap/README.md b/docs/03-业务模块/Redcap/README.md index a1489ac2..7a811dec 100644 --- a/docs/03-业务模块/Redcap/README.md +++ b/docs/03-业务模块/Redcap/README.md @@ -165,6 +165,8 @@ AIclinicalresearch/redcap-docker-dev/ + + diff --git a/docs/04-开发规范/09-数据库开发规范.md b/docs/04-开发规范/09-数据库开发规范.md index 644e5a71..b34af405 100644 --- a/docs/04-开发规范/09-数据库开发规范.md +++ b/docs/04-开发规范/09-数据库开发规范.md @@ -334,3 +334,5 @@ npx tsx check_iit_asl_data.ts + + diff --git a/docs/04-开发规范/10-模块认证规范.md b/docs/04-开发规范/10-模块认证规范.md index d10ad53c..32c366c3 100644 --- a/docs/04-开发规范/10-模块认证规范.md +++ b/docs/04-开发规范/10-模块认证规范.md @@ -202,3 +202,5 @@ interface DecodedToken { + + diff --git a/docs/05-部署文档/02-SAE部署完全指南(产品经理版).md b/docs/05-部署文档/02-SAE部署完全指南(产品经理版).md index b2872b0a..5873cad6 100644 --- a/docs/05-部署文档/02-SAE部署完全指南(产品经理版).md +++ b/docs/05-部署文档/02-SAE部署完全指南(产品经理版).md @@ -902,6 +902,8 @@ ACR镜像仓库: + + diff --git a/docs/05-部署文档/07-前端Nginx-SAE部署操作手册.md b/docs/05-部署文档/07-前端Nginx-SAE部署操作手册.md index bf83277d..7846717c 100644 --- a/docs/05-部署文档/07-前端Nginx-SAE部署操作手册.md +++ b/docs/05-部署文档/07-前端Nginx-SAE部署操作手册.md @@ -1389,6 +1389,8 @@ SAE应用配置: + + diff --git a/docs/05-部署文档/08-PostgreSQL数据库部署操作手册.md b/docs/05-部署文档/08-PostgreSQL数据库部署操作手册.md index 588a92b1..975d0944 100644 --- a/docs/05-部署文档/08-PostgreSQL数据库部署操作手册.md +++ b/docs/05-部署文档/08-PostgreSQL数据库部署操作手册.md @@ -1205,6 +1205,8 @@ docker exec -e PGPASSWORD="密码" ai-clinical-postgres psql -h RDS地址 -U air + + diff --git a/docs/05-部署文档/10-Node.js后端-Docker镜像构建手册.md b/docs/05-部署文档/10-Node.js后端-Docker镜像构建手册.md index 83880b2a..fc1854b7 100644 --- a/docs/05-部署文档/10-Node.js后端-Docker镜像构建手册.md +++ b/docs/05-部署文档/10-Node.js后端-Docker镜像构建手册.md @@ -616,6 +616,8 @@ scripts/*.ts + + diff --git a/docs/05-部署文档/11-Node.js后端-SAE部署配置清单.md b/docs/05-部署文档/11-Node.js后端-SAE部署配置清单.md index 1c02d603..12673628 100644 --- a/docs/05-部署文档/11-Node.js后端-SAE部署配置清单.md +++ b/docs/05-部署文档/11-Node.js后端-SAE部署配置清单.md @@ -304,6 +304,8 @@ Node.js后端部署成功后: + + diff --git a/docs/05-部署文档/12-Node.js后端-SAE部署操作手册.md b/docs/05-部署文档/12-Node.js后端-SAE部署操作手册.md index 9a62d489..7ab4105b 100644 --- a/docs/05-部署文档/12-Node.js后端-SAE部署操作手册.md +++ b/docs/05-部署文档/12-Node.js后端-SAE部署操作手册.md @@ -527,6 +527,8 @@ Node.js后端 (SAE) ← http://172.17.173.88:3001 + + diff --git a/docs/05-部署文档/13-Node.js后端-镜像修复记录.md b/docs/05-部署文档/13-Node.js后端-镜像修复记录.md index 3f350d7a..52ab5499 100644 --- a/docs/05-部署文档/13-Node.js后端-镜像修复记录.md +++ b/docs/05-部署文档/13-Node.js后端-镜像修复记录.md @@ -242,6 +242,8 @@ curl http://localhost:3001/health + + diff --git a/docs/05-部署文档/14-Node.js后端-pino-pretty问题修复.md b/docs/05-部署文档/14-Node.js后端-pino-pretty问题修复.md index 1ad87cbc..5ce6eb6a 100644 --- a/docs/05-部署文档/14-Node.js后端-pino-pretty问题修复.md +++ b/docs/05-部署文档/14-Node.js后端-pino-pretty问题修复.md @@ -280,6 +280,8 @@ npm run dev + + diff --git a/docs/05-部署文档/16-前端Nginx-部署成功总结.md b/docs/05-部署文档/16-前端Nginx-部署成功总结.md index 91fc51db..44474cc9 100644 --- a/docs/05-部署文档/16-前端Nginx-部署成功总结.md +++ b/docs/05-部署文档/16-前端Nginx-部署成功总结.md @@ -504,6 +504,8 @@ pgm-2zex1m2y3r23hdn5.pg.rds.aliyuncs.com:5432 + + diff --git a/docs/05-部署文档/17-完整部署实战手册-2025版.md b/docs/05-部署文档/17-完整部署实战手册-2025版.md index c8198e8e..46441030 100644 --- a/docs/05-部署文档/17-完整部署实战手册-2025版.md +++ b/docs/05-部署文档/17-完整部署实战手册-2025版.md @@ -1832,6 +1832,8 @@ curl http://8.140.53.236/ + + diff --git a/docs/05-部署文档/18-部署文档使用指南.md b/docs/05-部署文档/18-部署文档使用指南.md index 90af5330..3ff1c688 100644 --- a/docs/05-部署文档/18-部署文档使用指南.md +++ b/docs/05-部署文档/18-部署文档使用指南.md @@ -380,6 +380,8 @@ crpi-cd5ij4pjt65mweeo.cn-beijing.personal.cr.aliyuncs.com/ai-clinical/backend-se + + diff --git a/docs/05-部署文档/19-日常更新快速操作手册.md b/docs/05-部署文档/19-日常更新快速操作手册.md index 2a07bf4f..6d596f82 100644 --- a/docs/05-部署文档/19-日常更新快速操作手册.md +++ b/docs/05-部署文档/19-日常更新快速操作手册.md @@ -702,6 +702,8 @@ docker login --username=gofeng117@163.com \ + + diff --git a/docs/05-部署文档/文档修正报告-20251214.md b/docs/05-部署文档/文档修正报告-20251214.md index da7ee0cd..0078d438 100644 --- a/docs/05-部署文档/文档修正报告-20251214.md +++ b/docs/05-部署文档/文档修正报告-20251214.md @@ -513,6 +513,8 @@ NAT网关成本¥100/月,对初创团队是一笔开销 + + diff --git a/docs/07-运维文档/03-SAE环境变量配置指南.md b/docs/07-运维文档/03-SAE环境变量配置指南.md index a0d03a97..8bedeca9 100644 --- a/docs/07-运维文档/03-SAE环境变量配置指南.md +++ b/docs/07-运维文档/03-SAE环境变量配置指南.md @@ -418,6 +418,8 @@ curl http://你的SAE地址:3001/health + + diff --git a/docs/07-运维文档/05-Redis缓存与队列的区别说明.md b/docs/07-运维文档/05-Redis缓存与队列的区别说明.md index d6bc6c42..b3aaf4e2 100644 --- a/docs/07-运维文档/05-Redis缓存与队列的区别说明.md +++ b/docs/07-运维文档/05-Redis缓存与队列的区别说明.md @@ -750,6 +750,8 @@ const job = await queue.getJob(jobId); + + diff --git a/docs/07-运维文档/06-长时间任务可靠性分析.md b/docs/07-运维文档/06-长时间任务可靠性分析.md index 76e217ce..4db4453c 100644 --- a/docs/07-运维文档/06-长时间任务可靠性分析.md +++ b/docs/07-运维文档/06-长时间任务可靠性分析.md @@ -517,6 +517,8 @@ processLiteraturesInBackground(task.id, projectId, testLiteratures); + + diff --git a/docs/07-运维文档/07-Redis使用需求分析(按模块).md b/docs/07-运维文档/07-Redis使用需求分析(按模块).md index 048206bb..a2d4eba6 100644 --- a/docs/07-运维文档/07-Redis使用需求分析(按模块).md +++ b/docs/07-运维文档/07-Redis使用需求分析(按模块).md @@ -994,6 +994,8 @@ ROI = (¥22,556 - ¥144) / ¥144 × 100% = 15,564% + + diff --git a/docs/07-运维文档/全自动巡检系统设计方案.md b/docs/07-运维文档/全自动巡检系统设计方案.md new file mode 100644 index 00000000..c4d48f0c --- /dev/null +++ b/docs/07-运维文档/全自动巡检系统设计方案.md @@ -0,0 +1,193 @@ +# **全自动巡检系统设计方案 (Synthetic Monitoring)** + +**目标:** 每天早上 06:00 自动验证系统 7 大模块核心功能,确诊“系统健康”,如有异常立即推送到企业微信。 + +**适用环境:** 阿里云 SAE (Job 任务) + +**执行者:** 全自动巡检脚本 (HealthCheck Bot) + +## **1\. 架构设计** + +### **运行原理** + +graph LR + A\[⏰ SAE 定时任务\06:00 AM\] \--\>|启动容器| B\[🩺 巡检脚本\HealthCheck Bot\] + B \--\>|1. HTTP请求| C\[🌐 前端/后端 API\] + B \--\>|2. 数据库查询| D\[🐘 PostgreSQL\] + B \--\>|3. 模型调用| E\[🤖 LLM 服务\] + + B \--\>|✅ 所有检查通过| F\[✅ 记录日志 (静默)\] + B \--\>|❌ 发现异常| G\[🚨 企业微信报警\] + +### **为什么选择 SAE Job?** + +* **无需额外服务器**:不需要为了跑一个脚本单独买 ECS。 +* **环境互通**:脚本在 VPC 内网运行,可以直接连接 RDS 数据库验证数据一致性,也可以通过内网 IP 调用 Python 微服务,**不走公网流量,速度极快**。 +* **配置简单**:和部署后端应用完全一样,只是把“启动命令”改成了“执行一次脚本”。 + +## **2\. 巡检脚本逻辑 (TypeScript 伪代码)** + +建议在 backend 项目中新建一个目录 scripts/health-check/,复用现有的 ORM 和 Service。 + +### **核心检测项 (覆盖 7 大模块)** + +| 模块 | 检测点 (Check Point) | 预期结果 | +| :---- | :---- | :---- | +| **基础层** | **数据库连接** | prisma.$queryRaw('SELECT 1') 返回 1,耗时 \< 100ms | +| **基础层** | **外部 API 连通性** | ping api.deepseek.com 成功 (验证 NAT 网关正常) | +| **AIA** | **AI 问答响应** | 向 DeepSeek 发送 "Hi",能在 5s 内收到回复 (验证 LLM 通路) | +| **PKB** | **向量检索 (RAG)** | 上传一段测试文本,并在 1s 后通过关键词检索到它 (验证 pgvector 和 embedding) | +| **ASL** | **Python 微服务** | 调用 Python 服务 /health 接口,返回 200 (验证 Python 容器存活) | +| **DC** | **数据清洗** | 发送一个简单的 JSON 数据给 Tool C 接口,验证返回清洗结果 | +| **OSS** | **文件存取** | 上传一个 health\_check.txt 到 OSS 并下载,内容一致 (验证存储) | + +### **代码示例** + +// backend/scripts/health-check/run.ts + +import { PrismaClient } from '@prisma/client'; +import axios from 'axios'; +import { sendWecomAlert } from './wecom'; // 复用 IIT 模块的企微通知代码 + +const prisma \= new PrismaClient(); +const REPORT \= { + success: true, + modules: \[\] as string\[\], + errors: \[\] as string\[\] +}; + +async function checkDatabase() { + const start \= Date.now(); + try { + await prisma.$queryRaw\`SELECT 1\`; + REPORT.modules.push(\`✅ Database (${Date.now() \- start}ms)\`); + } catch (e) { + throw new Error(\`Database connection failed: ${e.message}\`); + } +} + +async function checkPythonService() { + // 使用内网地址,复用环境变量 + const url \= process.env.EXTRACTION\_SERVICE\_URL \+ '/health'; + try { + const res \= await axios.get(url, { timeout: 2000 }); + if (res.status \=== 200\) REPORT.modules.push(\`✅ Python Service\`); + } catch (e) { + throw new Error(\`Python Service unreachable: ${e.message}\`); + } +} + +async function checkLLM() { + // 调用简单的 Chat 接口测试 + try { + // 模拟一次简单的 AI 对话... + REPORT.modules.push(\`✅ LLM Gateway\`); + } catch (e) { + throw new Error(\`LLM API failed: ${e.message}\`); + } +} + +// ... 更多检查函数 ... + +async function main() { + console.log('🚀 Starting Daily Health Check...'); + + try { + await checkDatabase(); + await checkPythonService(); + await checkLLM(); + // await checkOSS(); + // await checkRAG(); + + console.log('🎉 All systems healthy\!'); + // 可选:成功也发送一条简短通知,让你早上醒来看到绿色对勾 + // await sendWecomAlert('🟢 每日巡检通过:系统运行正常'); + + } catch (error: any) { + console.error('🔥 Health Check Failed:', error); + REPORT.success \= false; + + // 🚨 发生异常,立即推送报警! + await sendWecomAlert(\`🔴 \*\*线上环境异常报警\*\* 🔴\\n\\n检查时间: ${new Date().toLocaleString()}\\n错误模块: ${error.message}\\n\\n请尽快检查 SAE 控制台!\`); + + process.exit(1); // 让 SAE 任务标记为失败 + } finally { + await prisma.$disconnect(); + } +} + +main(); + +## **3\. 阿里云 SAE 部署实操** + +### **步骤 1: 构建镜像** + +既然脚本在 backend 项目里,你可以**直接复用 Node.js 后端的镜像**! + +不需要重新构建专门的镜像,因为后端镜像里已经包含了 Node 环境、Prisma Client 和所有依赖。 + +### **步骤 2: 创建 SAE 任务 (Job)** + +1. 登录 SAE 控制台 \-\> 任务列表 (Job) \-\> 创建任务。 +2. **应用名称**:clinical-health-check +3. **镜像地址**:选择你们的 backend 镜像 (如 backend:latest)。 +4. **运行命令**: + * 这里的命令会覆盖 Dockerfile 的 CMD。 + * 填写:npx tsx scripts/health-check/run.ts (假设你用 tsx 运行) +5. **调度配置**: + * 并发策略:Forbid (禁止并发,上一次没跑完下一次不跑) + * 定时配置 (Crontab):0 6 \* \* \* (每天 06:00 执行) +6. **环境变量**: + * **直接复制** 生产环境后端应用的所有环境变量 (DATABASE\_URL, WECHAT\_KEY 等)。 +7. **网络配置**: + * 选择和生产环境一样的 VPC 和 VSwitch (确保能连上 RDS)。 + +## **4\. 报警通知模板 (企业微信)** + +当脚本捕获到异常时,发送如下 Markdown 消息到你们的开发群: + +🔴 \*\*严重:线上巡检未通过\*\* + +\> \*\*检测时间\*\*:2026-01-24 06:00:05 +\> \*\*环境\*\*:Aliyun Production + +\*\*异常详情\*\*: +❌ \*\*\[Python Service\]\*\* Connection refused (172.16.x.x:8000) +✅ \*\*\[Database\]\*\* OK +✅ \*\*\[OSS\]\*\* OK + +\*\*建议操作\*\*: +1\. 检查 Python 微服务容器是否重启 +2\. 检查 SAE 内存监控 +3\. \[点击跳转 SAE 控制台\](https://sae.console.aliyun.com/...) + +## **5\. 进阶:阿里云原生监控 (兜底方案)** + +除了自己写脚本,阿里云还有一个**免费且好用**的功能,建议同时开启,作为**双重保险**。 + +### **阿里云 CloudMonitor (云监控) \-\> 站点监控 (Site Monitor)** + +1. **无需写代码**。 +2. 在云监控控制台,创建一个“站点监控”任务。 +3. **监控地址**:输入你们的公网域名 http://8.140.53.236/。 +4. **频率**:每 1 分钟一次。 +5. **报警**:如果 HTTP 状态码 \!= 200,或者响应时间 \> 5秒,发送短信给你们手机。 + +**对比:** + +* **云监控**:只能告诉你“网站打不打得开”(Ping 通不通)。 +* **自研脚本 (本方案)**:能告诉你“**功能坏没坏**”(比如网站能打开,但 AI 回复不了,云监控发现不了,但脚本能发现)。 + +## **6\. 实施路线图** + +1. **Day 1 (本地开发)**: + * 在 backend 项目里写好 run.ts。 + * 在本地运行 npx tsx scripts/health-check/run.ts,确保它能跑通所有检查流程。 + * 测试企业微信推送是否正常。 +2. **Day 2 (SAE 部署)**: + * 不需要重新发版,只要之前的后端镜像里包含了这个脚本文件(通常都会包含 src 目录)。 + * 在 SAE 创建 Job,手动触发一次,看日志是否成功。 +3. **Day 3 (安心睡觉)**: + * 设置定时任务,每天早上收那个绿色的 ✅ 消息。 + +这样做,你每天早上醒来第一眼看手机,如果看到“✅ 每日巡检通过”,你就可以安心刷牙洗脸;如果没收到或者收到红色报警,你在地铁上就能开始安排修复,而不是等用户投诉了才手忙脚乱。 \ No newline at end of file diff --git a/docs/07-运维文档/架构优化建议:2人团队SAE降本增效方案.md b/docs/07-运维文档/架构优化建议:2人团队SAE降本增效方案.md new file mode 100644 index 00000000..b2626779 --- /dev/null +++ b/docs/07-运维文档/架构优化建议:2人团队SAE降本增效方案.md @@ -0,0 +1,124 @@ +# **针对 2 人初创团队的 SAE \+ RDS 架构降本增效方案** + +**现状诊断:** + +* **团队规模:** 2人(极简团队) +* **当前痛点:** 成本压力(双 SAE 环境费用) vs 运维安全(需要隔离测试) +* **关键约束:** 必须防止类似 prisma db push 的生产事故再次发生。 + +## **策略一:数据库层面 —— “同居不同室”** + +你们现在的做法是共享同一个 RDS PostgreSQL 15 实例。这在创业初期是**非常明智**的选择,不需要改变。购买两个 RDS 实例对于目前的业务量来说太浪费了。 + +但是,**“共享”的方式决定了生死**。 + +### **1\. 绝对禁止的做法** + +❌ **Dev 和 Prod 使用同一个数据库名(Database Name),试图靠 Schema 区分。** + +* **风险**:你们的代码中(特别是 Prisma 和 pg-boss)可能硬编码了 Schema 名称(如 platform\_schema)。 +* **后果**:Dev 环境启动后,pg-boss Worker 会连接到数据库,开始抓取 Prod 环境的任务队列进行处理。这会导致生产任务丢失或被错误处理。 + +### **2\. 推荐做法:Database 级别的隔离** + +✅ **在同一个 RDS 实例中,创建两个完全独立的数据库。** + +| 环境变量 | 生产环境 (Prod) | 开发/测试环境 (Dev) | +| :---- | :---- | :---- | +| **DATABASE\_URL** | .../ai\_clinical\_prod | .../ai\_clinical\_dev | +| **用户名** | aiclinical\_prod\_user | aiclinical\_dev\_user | +| **资源限制** | 无限制 | 限制连接数 (避免 Dev 耗尽连接) | + +**实施步骤:** + +1. 进入 RDS 控制台,新建数据库 ai\_clinical\_dev。 +2. 即使是同一个 RDS 实例,由于数据库名不同,数据文件在逻辑上是完全隔离的。 +3. Dev 环境即使执行 prisma db push \--force-reset,也只会清空 ai\_clinical\_dev,**生产环境绝对安全**。 + +## **策略二:计算层面 (SAE) —— “分时租赁与按需启停”** + +对于 SAE,你们最大的优势是**Serverless 的弹性**。如果测试环境 24 小时开着,那就是把 SAE 当 ECS 用,非常亏。 + +### **1\. 生产环境 (Production)** + +* **策略**:保持常驻,配置最小实例数为 1(或 2 保证高可用)。 +* **配置**:开启弹性伸缩,根据 CPU/内存自动扩容。 + +### **2\. 开发/测试环境 (Dev/Test)** + +**核心建议:不要让它 24 小时运行!** + +#### **方案 A:一键启停 (手动省钱法)** + +* **操作**: + * 平时开发:在本地 localhost 使用 Docker Compose \+ 本地 Postgres 开发(完全免费)。 + * 提交代码后:如果需要验证,去 SAE 控制台点击“启动”。 + * 验证完/下班后:去 SAE 控制台点击\*\*“停止”\*\*(由 1 实例缩容为 0)。 +* **成本影响**:SAE 停止的应用**不收取计算费用**(只收取极少的快照存储费)。如果每天只开 2 小时测试,成本降低 90%。 + +#### **方案 B:CI/CD 触发式环境 (自动化法)** + +* **原理**:利用 GitHub Actions 或 阿里云云效。 +* **流程**: + 1. 代码 Push 到 dev 分支。 + 2. CI 流水线构建镜像。 + 3. CI 调用阿里云 API,**更新并启动** SAE Dev 应用。 + 4. 设置一个定时任务(如每晚 22:00),自动调用 API **停止** Dev 应用。 + +## **策略三:极致省钱的替代架构 (如果不使用 SAE 测试环境)** + +如果连 SAE 测试环境的“按需启动”成本都想省,可以考虑以下架构: + +### **1\. "Localhost is King" 模式** + +既然只有 2 个人,且都在内网或可以通过 VPN 协作: + +* **放弃 SAE 测试环境**。 +* **开发验证**:每人本地运行全套 Docker Compose(包含前端、后端、Postgres、MinIO 模拟 OSS)。 +* **集成测试**:其中一人的电脑作为“临时服务器”,通过 ngrok 或 frp 暴露端口给另一人测试。 +* **上线**:直接部署到 SAE 生产环境。 +* **缺点**:缺乏一个稳定的“预发环境”,上线风险略高。 + +### **2\. ECS "脏"环境模式 (Dirty Dev Box)** + +* **做法**:买一台最便宜的 **ECS (突发性能实例 t6/t5)**,或者利用闲置的旧笔记本/工控机放在公司/家里。 +* **部署**:上面装一个 Docker,把所有 Dev 服务(前端、后端、Python、DB)全扔进去。 +* **成本**:一台 2核4G 的 t6 实例,一个月可能只要几十块钱(或者抢占式实例,更便宜)。 +* **优势**:固定 IP,成本极低,随便折腾。 +* **劣势**:需要自己维护 Linux、Docker 环境。 + +## **综合建议:给你们团队的最佳实践** + +考虑到你们已经熟悉了 SAE 且希望减少运维,我推荐 **“改良版 SAE \+ 共享 RDS”** 方案: + +1. **数据库**: + * 继续使用**同一个 RDS 实例**。 + * **必须**创建两个 DB:prod\_db 和 dev\_db。 + * **必须**使用两个数据库账号,防止权限越界。 +2. **SAE 环境**: + * **生产环境**:SAE 专业版(或标准版),常驻。 + * **测试环境**:**SAE 标准版(无需专业版)**。 + * **关键动作**: + * 配置 SAE 测试环境的**定时启停**策略(阿里云 SAE 控制台支持定时启停)。 + * 例如:09:00 启动,20:00 停止。周末不启动。 + * 或者写一个脚本,只在部署新镜像时启动,闲置 1 小时无流量自动缩容到 0(需配合 ALB/CLB 流量监控,略复杂,建议手动或定时停止)。 +3. **本地开发增强**: + * 完善 docker-compose.yml。确保本地开发体验和云端 99% 一致。 + * 这样你们 90% 的测试工作(包括 Python 微服务、数据清洗逻辑)都可以在本地完成,只有最后 10% 需要用到 SAE 测试环境。 + +### **成本对比预估 (月)** + +| 方案 | 生产环境成本 | 测试环境成本 | 运维复杂度 | 推荐度 | +| :---- | :---- | :---- | :---- | :---- | +| **现状 (双 SAE 常驻)** | ¥600+ | ¥300+ | 低 | ⭐⭐ | +| **推荐 (Dev 定时启停)** | ¥600+ | **¥50 (按量付费)** | 中 (需配置一次) | ⭐⭐⭐⭐⭐ | +| **ECS 廉价测试机** | ¥600+ | ¥40-80 | 高 (需维护服务器) | ⭐⭐⭐ | +| **Localhost 只有生产** | ¥600+ | ¥0 | 低 (但在上线时风险极高) | ⭐ | + +### **总结** + +对于 2 人团队,**时间比计算资源更贵**。 + +不要为了省几百块钱去维护一套复杂的自建 ECS 环境。 + +**继续用 SAE,但是要学会“关机”。** 就像你离开房间会关灯一样,没人测试的时候,把 SAE Dev 环境关掉(缩容到 0),这就是云原生最大的红利。 \ No newline at end of file diff --git a/docs/07-运维文档/运营体系设计方案-MVP-V3.0.md b/docs/07-运维文档/运营体系设计方案-MVP-V3.0.md new file mode 100644 index 00000000..b159804e --- /dev/null +++ b/docs/07-运维文档/运营体系设计方案-MVP-V3.0.md @@ -0,0 +1,293 @@ +# **AI 临床科研平台 \- MVP 运营监控体系实施方案 V3.0** + +**文档版本**:V3.0 (全景洞察版) + +**面向对象**:核心开发团队 (2人) + +**更新重点**: + +1. **宏观**:确立 DAU/DAT 双核心指标,防止“假活跃”。 +2. **微观**:新增 **“用户 360 全景画像”**,支持查看单用户的资产存量与行为流水。 +3. **技术**:维持 Postgres-Only 极简架构,不引入新组件。 + +## **1\. 核心理念:关注“生存”与“真实价值”** + +在 MVP 阶段,我们必须警惕“只有管理员登录”的假象。**医生用起来,才是真的活了。** + +| 优先级 | 指标名称 | 定义 | 为什么关注? | +| :---- | :---- | :---- | :---- | +| **P0+** | **活跃医生数 (DAU)** | 今日有登录/使用行为的去重 user\_id 数。 | **真实价值线**。只有医生个人觉得好用,产品才有生命力。这是防止客户流失的前哨指标。 | +| **P0** | **活跃租户数 (DAT)** | 今日有登录行为的去重 tenant\_id 数。 | **商务生死线**。代表医院客户的存活情况。 | +| **P1** | **具体功能渗透率** | 例如:“选题助手”的使用次数 vs “论文润色”的使用次数。 | **产品迭代指引**。如果没人用“润色”,我们就别在这个功能上浪费时间开发了。 | +| **P2** | **价值交付次数** | 用户点击 **“导出/下载”** 的次数。 | **北极星指标**。用户只有认可了 AI 的结果才会导出。这是交付价值的终点。 | + +## **2\. 技术架构:Postgres-Only 极简方案** + +### **2.1 数据库设计 (Schema)** + +在 admin\_schema 下新增一张通用日志表。**不需要**做聚合表,**不需要**做定时统计任务,直接查表即可。 + +**文件路径**:backend/prisma/schema.prisma + +// ... inside admin\_schema ... + +/// 极简运营日志表 (MVP) +model SimpleLog { + id String @id @default(dbgenerated("gen\_random\_uuid()")) @db.Uuid + createdAt DateTime @default(now()) @map("created\_at") + + tenantId String @map("tenant\_id") @db.VarChar(50) + userId String @map("user\_id") @db.Uuid + userName String? @map("user\_name") @db.VarChar(50) + + // V3.0 核心字段设计 + module String @db.VarChar(20) // 大模块: 'AIA', 'ASL', 'DC', 'PKB' + feature String @db.VarChar(50) // 细分功能: 'Topic Agent', 'Tool C', 'RAG Chat' + action String @db.VarChar(20) // 动作: 'USE', 'EXPORT', 'ERROR', 'LOGIN' + + info String? @db.Text // 详情: "生成了3个选题" / "导出Excel" + + @@index(\[createdAt\]) + @@index(\[tenantId\]) + @@index(\[userId\]) // V3.0 新增:支持查询单用户轨迹 + @@index(\[module, feature\]) // 支持按功能统计热度 + @@map("simple\_logs") + @@schema("admin\_schema") +} + +### **2.2 后端服务实现 (ActivityService)** + +负责写入日志和获取大盘数据。 + +**文件路径**:backend/src/common/services/activity.service.ts + +import { PrismaClient } from '@prisma/client'; +// 假设这是全局 Prisma 实例 +import { prisma } from '@/common/database/prisma'; + +export const activityService \= { + /\*\* + \* 核心埋点方法 (Fire-and-Forget 模式) + \* @param feature 具体功能名称,如 'Topic Agent' + \*/ + async log( + tenantId: string, + userId: string, + userName: string, + action: 'LOGIN' | 'USE' | 'EXPORT' | 'ERROR', + module: 'AIA' | 'ASL' | 'DC' | 'PKB' | 'SYSTEM', + feature: string, // 必填:细分功能 + info: any + ) { + // 异步执行,不要 await,避免影响主接口性能 + prisma.simpleLog.create({ + data: { + tenantId, userId, userName, action, module, feature, + info: typeof info \=== 'object' ? JSON.stringify(info) : String(info), + } + }).catch(e \=\> console.error('埋点写入失败(可忽略):', e)); + }, + + /\*\* + \* 运营看板:获取实时流水账 + \*/ + async getLiveFeed(limit \= 100\) { + return prisma.simpleLog.findMany({ + orderBy: { createdAt: 'desc' }, + take: limit + }); + }, + + /\*\* + \* 🟢 获取今日核心大盘数据 (DAU \+ DAT) + \* 用于 Admin 首页顶部展示 + \*/ + async getTodayOverview() { + const todayStart \= new Date(); + todayStart.setHours(0,0,0,0); + + const stats \= await prisma.$queryRaw\` + SELECT + COUNT(DISTINCT user\_id) as dau, + COUNT(DISTINCT tenant\_id) as dat, + COUNT(CASE WHEN action \= 'EXPORT' THEN 1 END) as export\_count + FROM admin\_schema.simple\_logs + WHERE created\_at \>= ${todayStart} + \`; + + return { + dau: Number(stats\[0\].dau), + dat: Number(stats\[0\].dat), + exportCount: Number(stats\[0\].export\_count) + }; + } +}; + +## **3\. 细粒度埋点实施清单 (直接复制给开发)** + +为了满足运营需求,请在以下位置埋点,并务必带上 feature 参数。 + +### **🤖 3.1 AIA 模块 (12个智能体细分)** + +**位置**:aia/services/chat.service.ts \-\> complete 方法 (AI回复完成时) + +**逻辑**:根据当前对话的 agentId 记录不同的 feature。 + +// 示例映射表 +const agentNameMap \= { + 'topic-scoping': '科学问题梳理', + 'pico-analysis': 'PICO梳理', + 'topic-eval': '选题评价', + 'outcome-design': '观察指标设计', + 'crf-design': 'CRF设计', + 'sample-size': '样本量计算', + 'protocol-writing': '方案撰写', + 'methodology-review': '方法学评审', + 'paper-polish': '论文润色', + 'paper-translate': '论文翻译', + // ... 其他智能体 +}; +const feature \= agentNameMap\[agentId\] || 'Unknown Agent'; +activityService.log(..., 'USE', 'AIA', feature, '生成完成'); + +### **🧹 3.2 DC 模块 (清洗工具细分)** + +**位置**:dc/controllers/extraction.controller.ts + +* **Tool B (自动提取)**: log(..., 'USE', 'DC', 'Tool B', '提取任务: 50条') +* **Tool C (数据清洗)**: log(..., 'USE', 'DC', 'Tool C', '执行清洗操作') +* **导出结果**: log(..., 'EXPORT', 'DC', 'Tool C', '导出 Excel') + +### **📚 3.3 ASL 模块 (文献功能细分)** + +**位置**:asl/controllers/research.controller.ts + +* **智能检索**: log(..., 'USE', 'ASL', 'DeepSearch', '关键词: 肺癌') +* **标题摘要筛选**: log(..., 'USE', 'ASL', 'Title/Abstract Screening', '筛选进度: 100/500') +* **全文复筛**: log(..., 'USE', 'ASL', 'Fulltext Review', '复筛完成') + +### **🧠 3.4 PKB 模块 (知识库行为)** + +**位置**:pkb/controllers/... + +* **上传文档**: log(..., 'USE', 'PKB', 'Document Upload', '上传: 3篇') +* **RAG 问答**: log(..., 'USE', 'PKB', 'RAG Chat', '提问: 入排标准是什么?') +* **批处理**: log(..., 'USE', 'PKB', 'Batch Process', '批量提取: 10篇') + +## **4\. 运营看板设计 (Admin Portal) \- 宏观** + +### **界面参考** + +在 Admin 首页展示。 + +#### **\[ 顶部卡片区域 \]** + +| 今日活跃医生 (DAU) | 今日活跃医院 (DAT) | 今日价值交付 (导出) | +| :---- | :---- | :---- | +| **12** 👨‍⚕️ | **3** 🏥 | **5** 🟢 | + +#### **\[ 实时流水账区域 \]** + +| 时间 | 医院 | 医生 | 模块 | 具体功能 | 动作 | 详情 | +| :---- | :---- | :---- | :---- | :---- | :---- | :---- | +| 10:05 | 协和 | 张主任 | **AIA** | **选题评价** | 🔵 USE | 评价得分: 85分 | +| 10:03 | 协和 | 张主任 | **AIA** | **PICO梳理** | 🔵 USE | 梳理完成 | +| 09:55 | 华西 | 李医生 | **DC** | **Tool C** | 🟢 **EXPORT** | 导出 Excel | + +## **5\. 用户 360 全景画像 (User 360 View) \- 微观 🆕** + +**新增模块**:当运营人员点击用户列表中的“详情”时,展示该用户的全生命周期数据。 + +### **5.1 后端聚合服务 (UserOverviewService)** + +不建立宽表,利用 Prisma 并行查询实现\*\*“资产快照 \+ 行为流水”\*\*聚合。 + +**文件路径**:backend/src/modules/admin/services/user-overview.service.ts + +import { prisma } from '@/common/database/prisma'; + +export const userOverviewService \= { + async getUserProfile(userId: string) { + // 并行查询,耗时取决于最慢的那个查询,通常 \< 200ms + const \[user, aiaStats, kbs, dcStats, logs\] \= await Promise.all(\[ + // 1\. 基础信息 + prisma.user.findUnique({ where: { id: userId } }), + + // 2\. AIA 资产 (会话数) + prisma.conversation.count({ where: { userId, isDeleted: false } }), + + // 3\. PKB 资产 (知识库数 \+ 文档数) + prisma.knowledgeBase.findMany({ + where: { userId }, + include: { \_count: { select: { documents: true } } } + }), + + // 4\. DC 资产 (任务数) + prisma.extractionTask.count({ where: { userId } }), + + // 5\. 最近行为 (从 SimpleLog 查最近 20 条) + prisma.simpleLog.findMany({ + where: { userId }, + orderBy: { createdAt: 'desc' }, + take: 20 + }) + \]); + + // 计算文档总数 + const totalDocs \= kbs.reduce((sum, kb) \=\> sum \+ kb.\_count.documents, 0); + + return { + profile: user, + assets: { + aia: { conversationCount: aiaStats }, + pkb: { kbCount: kbs.length, docCount: totalDocs, kbs }, + dc: { taskCount: dcStats } + }, + activities: logs + }; + } +}; + +### **5.2 前端布局设计 (UserDetailPage)** + +使用 Ant Design 组件库实现三段式布局。 + +#### **区域一:资产数字 (Asset Stats)** + +| 💬 AIA 对话 | 📚 PKB 知识库 | 📄 上传文献 | 🧹 DC 清洗任务 | +| :---- | :---- | :---- | :---- | +| **158** 次 | **3** 个 | **450** 篇 | **12** 次 | + +*(点击数字可展开查看具体列表,如查看 3 个知识库的名称)* + +#### **区域二:行为时间轴 (Timeline)** + +展示用户最近的操作轨迹,快速判断用户是否遇到困难。 + +* **10:30** \[AIA\] 使用了 **“选题评价”** (生成结果: 85分) +* **10:15** \[PKB\] 上传了文档 lung\_cancer\_study.pdf 到 **“肺癌课题库”** +* **09:50** \[DC\] 导出了 **Tool C** 清洗结果 (Excel) +* **09:48** \[系统\] 登录系统 + +## **6\. 每日自动化巡检 (SAE Job)** + +复用后端镜像,使用 SAE Job 功能,每天 06:00 执行。 + +* **检查项**:数据库连接、Python 微服务心跳、公网连通性 (NAT)。 +* **通知**:检查失败时发送企业微信报警。 +* **目的**:确保早上醒来时系统是健康的。 + +## **7\. 开发执行计划** + +**总耗时预计:3-4 小时** + +1. **\[DB\] (15min)**: + * 更新 Prisma Schema (新增 feature 字段和索引)。 + * 执行 prisma db push。 +2. **\[Backend\] (60min)**: + * 实现 activityService (埋点 \+ 大盘)。 + * 实现 userOverviewService (360 聚合)。 + * 在 AIA/DC/ASL/PKB 核心 Controller 插入 log() 代码。 +3. **\[Frontend\] (90min)**: + * Admin 首页:实现大盘卡片 \+ 实时流水表格。 + * User 详情页:实现资产统计卡片 \+ 行为 Timeline。 \ No newline at end of file diff --git a/docs/08-项目管理/03-每周计划/2025-12-13-Postgres-Only架构改造完成.md b/docs/08-项目管理/03-每周计划/2025-12-13-Postgres-Only架构改造完成.md index 7300cec2..86dea2b6 100644 --- a/docs/08-项目管理/03-每周计划/2025-12-13-Postgres-Only架构改造完成.md +++ b/docs/08-项目管理/03-每周计划/2025-12-13-Postgres-Only架构改造完成.md @@ -1051,6 +1051,8 @@ Redis 实例:¥500/月 + + diff --git a/docs/08-项目管理/05-技术债务/通用对话服务抽取计划.md b/docs/08-项目管理/05-技术债务/通用对话服务抽取计划.md index 6f293509..57343611 100644 --- a/docs/08-项目管理/05-技术债务/通用对话服务抽取计划.md +++ b/docs/08-项目管理/05-技术债务/通用对话服务抽取计划.md @@ -509,6 +509,8 @@ import { ChatContainer } from '@/shared/components/Chat'; + + diff --git a/docs/08-项目管理/2026-01-11-数据库事故总结.md b/docs/08-项目管理/2026-01-11-数据库事故总结.md index 1b1a178d..1cafa225 100644 --- a/docs/08-项目管理/2026-01-11-数据库事故总结.md +++ b/docs/08-项目管理/2026-01-11-数据库事故总结.md @@ -218,3 +218,5 @@ VALUES ('user-mock-001', '13800000000', ..., 'tenant-mock-001', ...); + + diff --git a/docs/08-项目管理/PKB前端问题修复报告.md b/docs/08-项目管理/PKB前端问题修复报告.md index 0e17ec5f..507cdcb0 100644 --- a/docs/08-项目管理/PKB前端问题修复报告.md +++ b/docs/08-项目管理/PKB前端问题修复报告.md @@ -427,6 +427,8 @@ frontend-v2/src/modules/pkb/ + + diff --git a/docs/08-项目管理/PKB前端验证指南.md b/docs/08-项目管理/PKB前端验证指南.md index ca3def69..5fa472c9 100644 --- a/docs/08-项目管理/PKB前端验证指南.md +++ b/docs/08-项目管理/PKB前端验证指南.md @@ -289,6 +289,8 @@ npm run dev + + diff --git a/docs/08-项目管理/PKB功能审查报告-阶段0.md b/docs/08-项目管理/PKB功能审查报告-阶段0.md index 093bf51f..9fdc506a 100644 --- a/docs/08-项目管理/PKB功能审查报告-阶段0.md +++ b/docs/08-项目管理/PKB功能审查报告-阶段0.md @@ -804,6 +804,8 @@ AIA智能问答模块 + + diff --git a/docs/08-项目管理/PKB和RVW功能迁移计划.md b/docs/08-项目管理/PKB和RVW功能迁移计划.md index 38ecfccb..85156dab 100644 --- a/docs/08-项目管理/PKB和RVW功能迁移计划.md +++ b/docs/08-项目管理/PKB和RVW功能迁移计划.md @@ -949,6 +949,8 @@ CREATE INDEX idx_rvw_tasks_created_at ON rvw_schema.review_tasks(created_at); + + diff --git a/docs/08-项目管理/PKB精细化优化报告.md b/docs/08-项目管理/PKB精细化优化报告.md index 75555cef..76c50df9 100644 --- a/docs/08-项目管理/PKB精细化优化报告.md +++ b/docs/08-项目管理/PKB精细化优化报告.md @@ -602,6 +602,8 @@ const typography = { + + diff --git a/docs/08-项目管理/PKB迁移-超级安全执行计划.md b/docs/08-项目管理/PKB迁移-超级安全执行计划.md index bb5f6f07..325d40b6 100644 --- a/docs/08-项目管理/PKB迁移-超级安全执行计划.md +++ b/docs/08-项目管理/PKB迁移-超级安全执行计划.md @@ -914,6 +914,8 @@ app.use('/api/v1/knowledge', (req, res) => { + + diff --git a/docs/08-项目管理/PKB迁移-阶段1完成报告.md b/docs/08-项目管理/PKB迁移-阶段1完成报告.md index 001f5ecd..f8ccc876 100644 --- a/docs/08-项目管理/PKB迁移-阶段1完成报告.md +++ b/docs/08-项目管理/PKB迁移-阶段1完成报告.md @@ -228,6 +228,8 @@ rm -rf src/modules/pkb + + diff --git a/docs/08-项目管理/PKB迁移-阶段2完成报告.md b/docs/08-项目管理/PKB迁移-阶段2完成报告.md index 9e38f90a..4a9b8c67 100644 --- a/docs/08-项目管理/PKB迁移-阶段2完成报告.md +++ b/docs/08-项目管理/PKB迁移-阶段2完成报告.md @@ -403,6 +403,8 @@ GET /api/v2/pkb/batch-tasks/batch/templates + + diff --git a/docs/08-项目管理/PKB迁移-阶段2进行中.md b/docs/08-项目管理/PKB迁移-阶段2进行中.md index 6c88d011..2389e36f 100644 --- a/docs/08-项目管理/PKB迁移-阶段2进行中.md +++ b/docs/08-项目管理/PKB迁移-阶段2进行中.md @@ -47,6 +47,8 @@ import pkbRoutes from './modules/pkb/routes/index.js'; + + diff --git a/docs/08-项目管理/PKB迁移-阶段3完成报告.md b/docs/08-项目管理/PKB迁移-阶段3完成报告.md index e73a2b34..706d2007 100644 --- a/docs/08-项目管理/PKB迁移-阶段3完成报告.md +++ b/docs/08-项目管理/PKB迁移-阶段3完成报告.md @@ -316,6 +316,8 @@ backend/ + + diff --git a/docs/08-项目管理/PKB迁移-阶段4完成报告.md b/docs/08-项目管理/PKB迁移-阶段4完成报告.md index b446b5d8..2516ff46 100644 --- a/docs/08-项目管理/PKB迁移-阶段4完成报告.md +++ b/docs/08-项目管理/PKB迁移-阶段4完成报告.md @@ -527,6 +527,8 @@ const response = await fetch('/api/v2/pkb/batch-tasks/batch/execute', { + + diff --git a/extraction_service/.dockerignore b/extraction_service/.dockerignore index 1b545132..439fe5eb 100644 --- a/extraction_service/.dockerignore +++ b/extraction_service/.dockerignore @@ -87,6 +87,8 @@ models/ + + diff --git a/extraction_service/operations/__init__.py b/extraction_service/operations/__init__.py index e3b70fe7..ef847321 100644 --- a/extraction_service/operations/__init__.py +++ b/extraction_service/operations/__init__.py @@ -75,6 +75,8 @@ __version__ = '1.0.0' + + diff --git a/extraction_service/operations/dropna.py b/extraction_service/operations/dropna.py index 342ace97..940d1114 100644 --- a/extraction_service/operations/dropna.py +++ b/extraction_service/operations/dropna.py @@ -208,6 +208,8 @@ def get_missing_summary(df: pd.DataFrame) -> dict: + + diff --git a/extraction_service/operations/filter.py b/extraction_service/operations/filter.py index 65b951c0..b437e2b8 100644 --- a/extraction_service/operations/filter.py +++ b/extraction_service/operations/filter.py @@ -168,6 +168,8 @@ def apply_filter( + + diff --git a/extraction_service/operations/unpivot.py b/extraction_service/operations/unpivot.py index ef67d18a..f3164117 100644 --- a/extraction_service/operations/unpivot.py +++ b/extraction_service/operations/unpivot.py @@ -332,6 +332,8 @@ def get_unpivot_preview( + + diff --git a/extraction_service/services/pdf_markdown_processor.py b/extraction_service/services/pdf_markdown_processor.py index a04adfbd..01f744f0 100644 --- a/extraction_service/services/pdf_markdown_processor.py +++ b/extraction_service/services/pdf_markdown_processor.py @@ -148,3 +148,5 @@ def extract_pdf_to_markdown(pdf_path: str) -> Dict[str, Any]: + + diff --git a/extraction_service/test_dc_api.py b/extraction_service/test_dc_api.py index ca33d13f..230ed542 100644 --- a/extraction_service/test_dc_api.py +++ b/extraction_service/test_dc_api.py @@ -342,6 +342,8 @@ if __name__ == "__main__": + + diff --git a/extraction_service/test_execute_simple.py b/extraction_service/test_execute_simple.py index 34f3c72c..5fd884e6 100644 --- a/extraction_service/test_execute_simple.py +++ b/extraction_service/test_execute_simple.py @@ -108,6 +108,8 @@ except Exception as e: + + diff --git a/extraction_service/test_module.py b/extraction_service/test_module.py index 8d22aa89..0b13da79 100644 --- a/extraction_service/test_module.py +++ b/extraction_service/test_module.py @@ -88,6 +88,8 @@ except Exception as e: + + diff --git a/frontend-v2/.dockerignore b/frontend-v2/.dockerignore index 24f8fc3b..031b61f9 100644 --- a/frontend-v2/.dockerignore +++ b/frontend-v2/.dockerignore @@ -107,6 +107,8 @@ vite.config.*.timestamp-* + + diff --git a/frontend-v2/docker-entrypoint.sh b/frontend-v2/docker-entrypoint.sh index 14b5d5b3..c2987980 100644 --- a/frontend-v2/docker-entrypoint.sh +++ b/frontend-v2/docker-entrypoint.sh @@ -74,6 +74,8 @@ exec nginx -g 'daemon off;' + + diff --git a/frontend-v2/nginx.conf b/frontend-v2/nginx.conf index 1a037b27..17e5c127 100644 --- a/frontend-v2/nginx.conf +++ b/frontend-v2/nginx.conf @@ -230,6 +230,8 @@ http { + + diff --git a/frontend-v2/src/common/api/axios.ts b/frontend-v2/src/common/api/axios.ts index 78111006..33b4e122 100644 --- a/frontend-v2/src/common/api/axios.ts +++ b/frontend-v2/src/common/api/axios.ts @@ -59,4 +59,6 @@ export default apiClient; + + diff --git a/frontend-v2/src/framework/auth/api.ts b/frontend-v2/src/framework/auth/api.ts index a0904f45..c36e671e 100644 --- a/frontend-v2/src/framework/auth/api.ts +++ b/frontend-v2/src/framework/auth/api.ts @@ -257,5 +257,7 @@ export async function logout(): Promise { + + diff --git a/frontend-v2/src/framework/auth/index.ts b/frontend-v2/src/framework/auth/index.ts index 16157f09..0b2839f0 100644 --- a/frontend-v2/src/framework/auth/index.ts +++ b/frontend-v2/src/framework/auth/index.ts @@ -23,5 +23,7 @@ export * from './api'; + + diff --git a/frontend-v2/src/framework/auth/moduleApi.ts b/frontend-v2/src/framework/auth/moduleApi.ts index 748db9fb..576a4e04 100644 --- a/frontend-v2/src/framework/auth/moduleApi.ts +++ b/frontend-v2/src/framework/auth/moduleApi.ts @@ -49,3 +49,5 @@ export async function fetchUserModules(): Promise { + + diff --git a/frontend-v2/src/modules/admin/components/ModulePermissionModal.tsx b/frontend-v2/src/modules/admin/components/ModulePermissionModal.tsx index 1060668d..aec716af 100644 --- a/frontend-v2/src/modules/admin/components/ModulePermissionModal.tsx +++ b/frontend-v2/src/modules/admin/components/ModulePermissionModal.tsx @@ -129,3 +129,5 @@ export default ModulePermissionModal; + + diff --git a/frontend-v2/src/modules/admin/index.tsx b/frontend-v2/src/modules/admin/index.tsx index 18372994..8052a489 100644 --- a/frontend-v2/src/modules/admin/index.tsx +++ b/frontend-v2/src/modules/admin/index.tsx @@ -40,3 +40,5 @@ export default AdminModule; + + diff --git a/frontend-v2/src/modules/admin/types/user.ts b/frontend-v2/src/modules/admin/types/user.ts index a0647f66..36ebbcef 100644 --- a/frontend-v2/src/modules/admin/types/user.ts +++ b/frontend-v2/src/modules/admin/types/user.ts @@ -205,3 +205,5 @@ export const TENANT_TYPE_NAMES: Record = { + + diff --git a/frontend-v2/src/modules/aia/components/AgentCard.tsx b/frontend-v2/src/modules/aia/components/AgentCard.tsx index d556cc8f..2bc8316f 100644 --- a/frontend-v2/src/modules/aia/components/AgentCard.tsx +++ b/frontend-v2/src/modules/aia/components/AgentCard.tsx @@ -36,7 +36,7 @@ const getIcon = (iconName: string): React.ComponentType => { export const AgentCard: React.FC = ({ agent, onClick }) => { const Icon = getIcon(agent.icon); - const orderStr = String(agent.order).padStart(2, '0'); + const orderStr = agent.isProtocolAgent ? '🚀' : String(agent.order).padStart(2, '0'); const handleClick = () => { if (agent.isTool && agent.toolUrl) { @@ -47,9 +47,16 @@ export const AgentCard: React.FC = ({ agent, onClick }) => { } }; + const cardClasses = [ + 'agent-card', + `theme-${agent.theme}`, + agent.isTool ? 'tool-card' : '', + agent.isProtocolAgent ? 'protocol-agent-card' : '', + ].filter(Boolean).join(' '); + return (
{/* 工具类:右上角跳转图标 */} @@ -89,3 +96,4 @@ export default AgentCard; + diff --git a/frontend-v2/src/modules/aia/components/AgentHub.tsx b/frontend-v2/src/modules/aia/components/AgentHub.tsx index e7b9a7cc..7eb7710c 100644 --- a/frontend-v2/src/modules/aia/components/AgentHub.tsx +++ b/frontend-v2/src/modules/aia/components/AgentHub.tsx @@ -86,6 +86,39 @@ export const AgentHub: React.FC = ({ onAgentSelect }) => { const isLast = phaseIndex === PHASES.length - 1; const agents = agentsByPhase[phase.phase] || []; + // Protocol Agent 阶段特殊处理(phase 0,单独显示1个卡片) + if (phase.isProtocolAgent) { + return ( +
+ {/* 左侧时间轴 - 星号 */} +
+
+ ★ +
+
+
+ + {/* 阶段内容 */} +
+

+ {phase.name} + 测试 +

+ +
+ {agents.map(agent => ( + + ))} +
+
+
+ ); + } + // 根据阶段确定列数 let gridCols = 'grid-cols-3'; if (phase.phase === 2) gridCols = 'grid-cols-3'; // 4个卡片,每行3个 diff --git a/frontend-v2/src/modules/aia/components/index.ts b/frontend-v2/src/modules/aia/components/index.ts index 02ddccb2..c060bd32 100644 --- a/frontend-v2/src/modules/aia/components/index.ts +++ b/frontend-v2/src/modules/aia/components/index.ts @@ -19,3 +19,5 @@ export { ChatWorkspace } from './ChatWorkspace'; + + diff --git a/frontend-v2/src/modules/aia/constants.ts b/frontend-v2/src/modules/aia/constants.ts index a1506286..65c50552 100644 --- a/frontend-v2/src/modules/aia/constants.ts +++ b/frontend-v2/src/modules/aia/constants.ts @@ -10,6 +10,7 @@ import type { AgentConfig, PhaseConfig } from './types'; * 阶段配置 */ export const PHASES: PhaseConfig[] = [ + { phase: 0, name: '全流程研究方案制定', theme: 'indigo', isProtocolAgent: true }, { phase: 1, name: '选题优化智能体', theme: 'blue' }, { phase: 2, name: '方案设计智能体', theme: 'blue' }, { phase: 3, name: '方案预评审', theme: 'yellow' }, @@ -17,10 +18,27 @@ export const PHASES: PhaseConfig[] = [ { phase: 5, name: '写作助手', theme: 'purple' }, ]; +/** + * Protocol Agent 配置(全流程研究方案制定) + */ +export const PROTOCOL_AGENT: AgentConfig = { + id: 'PROTOCOL_AGENT', + name: '全流程研究方案制定', + icon: 'workflow', + description: '一站式完成研究方案核心要素:科学问题→PICO→研究设计→样本量→观察指标,支持一键生成完整方案。', + theme: 'indigo', + phase: 0, + order: 0, + isProtocolAgent: true, +}; + /** * 12个智能体配置 */ export const AGENTS: AgentConfig[] = [ + // Phase 0: Protocol Agent(全流程) + PROTOCOL_AGENT, + // Phase 1: 选题优化智能体 (3个,每行3个) { id: 'TOPIC_01', @@ -183,3 +201,4 @@ export const BRAND_COLORS = { + diff --git a/frontend-v2/src/modules/aia/index.tsx b/frontend-v2/src/modules/aia/index.tsx index 103815ac..2a68b29f 100644 --- a/frontend-v2/src/modules/aia/index.tsx +++ b/frontend-v2/src/modules/aia/index.tsx @@ -1,49 +1,80 @@ /** * AIA - AI Intelligent Assistant 模块入口 * - * 视图管理: - * - Hub: 智能体大厅(12个模块展示) - * - Chat: 沉浸式对话工作台 + * 路由管理: + * - /aia -> Hub: 智能体大厅(12个模块展示) + * - /aia/chat -> Chat: 沉浸式对话工作台(原12个智能体) + * - /aia/protocol-agent/:conversationId? -> Protocol Agent(全流程方案制定) */ import React, { useState } from 'react'; +import { Routes, Route, useNavigate, useLocation } from 'react-router-dom'; import { AgentHub } from './components/AgentHub'; import { ChatWorkspace } from './components/ChatWorkspace'; +import { ProtocolAgentPage } from './protocol-agent'; import type { AgentConfig } from './types'; const AIAModule: React.FC = () => { - const [currentView, setCurrentView] = useState<'hub' | 'chat'>('hub'); - const [selectedAgent, setSelectedAgent] = useState(null); - const [initialQuery, setInitialQuery] = useState(); + return ( + + {/* 智能体大厅 */} + } /> + + {/* Protocol Agent(全流程研究方案制定) */} + } /> + + {/* 传统智能体对话(向后兼容) */} + } /> + + ); +}; + +/** + * Hub 页面 + */ +const AIAHub: React.FC = () => { + const navigate = useNavigate(); - // 选择智能体,进入对话 const handleAgentSelect = (agent: AgentConfig & { initialQuery?: string }) => { - setSelectedAgent(agent); - setInitialQuery(agent.initialQuery); - setCurrentView('chat'); + console.log('[AIAHub] Agent selected:', agent.id, 'isProtocolAgent:', agent.isProtocolAgent); + + if (agent.isProtocolAgent) { + // Protocol Agent:跳转专属页面 + console.log('[AIAHub] Navigating to /aia/protocol-agent'); + navigate('/aia/protocol-agent'); + } else { + // 传统智能体:跳转对话页面 + console.log('[AIAHub] Navigating to /aia/chat'); + navigate('/aia/chat', { state: { agent, initialQuery: agent.initialQuery } }); + } }; - // 返回大厅 + return ; +}; + +/** + * 传统智能体对话页面 + */ +const AIAChat: React.FC = () => { + const navigate = useNavigate(); + const location = useLocation(); + const state = location.state as { agent: AgentConfig; initialQuery?: string } | null; + const handleBack = () => { - setCurrentView('hub'); - setSelectedAgent(null); - setInitialQuery(undefined); + navigate('/aia'); }; + if (!state?.agent) { + navigate('/aia'); + return null; + } + return ( - <> - {currentView === 'hub' && ( - - )} - - {currentView === 'chat' && selectedAgent && ( - - )} - + ); }; diff --git a/frontend-v2/src/modules/aia/protocol-agent/ProtocolAgentPage.tsx b/frontend-v2/src/modules/aia/protocol-agent/ProtocolAgentPage.tsx new file mode 100644 index 00000000..31c687d8 --- /dev/null +++ b/frontend-v2/src/modules/aia/protocol-agent/ProtocolAgentPage.tsx @@ -0,0 +1,245 @@ +/** + * Protocol Agent 页面 + * + * 100%还原原型图0119的精致设计 + * 三栏布局:可折叠侧边栏 + 聊天区 + 状态面板 + */ + +import React, { useState, useEffect } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { + Bot, Settings, ChevronLeft, Menu, Plus, + MessageSquare, Trash2 +} from 'lucide-react'; +import { ChatArea } from './components/ChatArea'; +import { StatePanel } from './components/StatePanel'; +import { useProtocolContext } from './hooks/useProtocolContext'; +import { useProtocolConversations } from './hooks/useProtocolConversations'; +import './styles/protocol-agent.css'; + +export const ProtocolAgentPage: React.FC = () => { + const navigate = useNavigate(); + const { conversationId } = useParams<{ conversationId?: string }>(); + + // 侧边栏折叠状态 + const [sidebarCollapsed, setSidebarCollapsed] = useState(true); + + // 会话管理 + const { + conversations, + currentConversation, + createConversation, + selectConversation, + deleteConversation, + } = useProtocolConversations(conversationId); + + // 上下文状态 + const { context, refreshContext } = useProtocolContext(currentConversation?.id); + + // 首次进入且无conversationId时,自动创建新对话 + const [isCreating, setIsCreating] = useState(false); + + useEffect(() => { + if (!conversationId && !currentConversation && !isCreating) { + console.log('[ProtocolAgentPage] 自动创建新对话...'); + setIsCreating(true); + createConversation().then(newConv => { + if (newConv) { + console.log('[ProtocolAgentPage] 新对话创建成功:', newConv.id); + navigate(`/aia/protocol-agent/${newConv.id}`, { replace: true }); + } else { + console.error('[ProtocolAgentPage] 创建对话失败'); + setIsCreating(false); + } + }).catch(err => { + console.error('[ProtocolAgentPage] 创建对话异常:', err); + setIsCreating(false); + }); + } + }, [conversationId, currentConversation, isCreating, createConversation, navigate]); + + // 获取当前阶段信息 + const currentStageName = context?.stageName || '科学问题梳理'; + const currentStageIndex = context?.stages?.findIndex(s => s.status === 'current') ?? 0; + + // 创建新对话 + const handleNewConversation = async () => { + const newConv = await createConversation(); + if (newConv) { + navigate(`/aia/protocol-agent/${newConv.id}`); + setSidebarCollapsed(true); + } + }; + + // 选择对话 + const handleSelectConversation = (id: string) => { + selectConversation(id); + navigate(`/aia/protocol-agent/${id}`); + setSidebarCollapsed(true); + }; + + // 返回AgentHub + const handleBack = () => { + navigate('/aia'); + }; + + // 加载状态 + if (isCreating || (!conversationId && !currentConversation)) { + return ( +
+
+
+

正在创建新对话...

+
+
+ ); + } + + return ( +
+ {/* 可折叠侧边栏 - Gemini风格 */} + + + {/* 主工作区 */} +
+ {/* 顶部导航 */} +
+
+ +
+
+ +
+
+

研究方案制定 Agent

+
+ + Orchestrator Online +
+
+
+
+
+
+ 当前阶段: + Step {currentStageIndex + 1}: {currentStageName} +
+ +
+
+ + {/* 聊天 + 状态面板 */} +
+ {/* 聊天区域 */} + + + {/* 状态面板 */} + +
+
+
+ ); +}; + +export default ProtocolAgentPage; + diff --git a/frontend-v2/src/modules/aia/protocol-agent/components/ActionCard.tsx b/frontend-v2/src/modules/aia/protocol-agent/components/ActionCard.tsx new file mode 100644 index 00000000..76bdfa94 --- /dev/null +++ b/frontend-v2/src/modules/aia/protocol-agent/components/ActionCard.tsx @@ -0,0 +1,66 @@ +/** + * Action Card - Deep Link 动作卡片 + * + * 100%还原原型图Action Card设计 + */ + +import React from 'react'; +import { Calculator, ExternalLink, Rocket } from 'lucide-react'; +import type { ActionCard } from '../types'; + +interface ActionCardProps { + card: ActionCard; +} + +const CARD_ICONS: Record> = { + sample_size_calculator: Calculator, + generate_protocol: Rocket, +}; + +export const ActionCardComponent: React.FC = ({ card }) => { + const Icon = CARD_ICONS[card.id] || Calculator; + + const handleClick = () => { + if (card.type === 'tool') { + // 打开工具页面(新标签页或模态框) + window.open(card.actionUrl, '_blank'); + } else if (card.type === 'action') { + // 触发动作(如一键生成) + // 这里会被父组件处理 + console.log('Action triggered:', card.id); + } + }; + + return ( +
+
+
+ +
+
+

{card.title}

+

{card.description}

+
+
+ + {card.type === 'tool' && ( +
+ +

点击将打开工具面板,参数已自动预填

+
+ )} + + {card.type === 'action' && ( + + )} +
+ ); +}; + + diff --git a/frontend-v2/src/modules/aia/protocol-agent/components/ChatArea.tsx b/frontend-v2/src/modules/aia/protocol-agent/components/ChatArea.tsx new file mode 100644 index 00000000..850adca8 --- /dev/null +++ b/frontend-v2/src/modules/aia/protocol-agent/components/ChatArea.tsx @@ -0,0 +1,310 @@ +/** + * Chat Area - Protocol Agent 聊天区域 + * + * 基于通用Chat组件,扩展Protocol Agent特有功能 + */ + +import React, { useState, useRef, useEffect } from 'react'; +import { Send, Sparkles, User } from 'lucide-react'; +import type { ProtocolContext, AgentResponse } from '../types'; +import { SyncButton } from './SyncButton'; +import { ActionCardComponent } from './ActionCard'; +import { ReflexionMessage } from './ReflexionMessage'; +import { getAccessToken } from '../../../../framework/auth/api'; + +interface ChatAreaProps { + conversationId?: string; + context: ProtocolContext | null; + onContextUpdate: () => void; +} + +interface Message { + id: string; + role: 'user' | 'assistant' | 'system'; + content: string; + thinkingContent?: string; + syncButton?: AgentResponse['syncButton']; + actionCards?: AgentResponse['actionCards']; + timestamp: Date; +} + +const API_BASE = '/api/v1/aia/protocol-agent'; + +export const ChatArea: React.FC = ({ + conversationId, + context, + onContextUpdate +}) => { + const [messages, setMessages] = useState([]); + const [input, setInput] = useState(''); + const [loading, setLoading] = useState(false); + const chatContainerRef = useRef(null); + + // 自动滚动到底部 + const scrollToBottom = () => { + if (chatContainerRef.current) { + chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight; + } + }; + + useEffect(() => { + scrollToBottom(); + }, [messages]); + + // 初始化欢迎消息 + useEffect(() => { + if (conversationId && messages.length === 0) { + setMessages([{ + id: 'welcome', + role: 'assistant', + content: `您好!我是研究方案制定助手,将帮助您系统地完成临床研究方案的核心要素设计。 + +我们将一起完成以下5个关键步骤: + +1️⃣ **科学问题梳理** - 明确研究要解决的核心问题 +2️⃣ **PICO要素** - 确定研究人群、干预、对照和结局 +3️⃣ **研究设计** - 选择合适的研究类型和方法 +4️⃣ **样本量计算** - 估算所需的样本量 +5️⃣ **观察指标** - 定义基线、暴露、结局指标和混杂因素 + +完成这5个要素后,您可以**一键生成完整的研究方案**并下载为Word文档。 + +让我们开始吧!请先告诉我,您想研究什么问题?或者描述一下您的研究背景和想法。`, + timestamp: new Date(), + }]); + } + }, [conversationId, messages.length]); + + /** + * 发送消息 + */ + const handleSend = async () => { + if (!input.trim() || !conversationId) return; + + const userMessage: Message = { + id: Date.now().toString(), + role: 'user', + content: input.trim(), + timestamp: new Date(), + }; + + setMessages(prev => [...prev, userMessage]); + setInput(''); + setLoading(true); + + try { + const token = getAccessToken(); + const response = await fetch(`${API_BASE}/message`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}`, + }, + body: JSON.stringify({ + conversationId, + content: userMessage.content, + }), + }); + + if (!response.ok) { + throw new Error('Failed to send message'); + } + + const result = await response.json(); + if (result.success && result.data) { + const aiResponse: AgentResponse = result.data; + + const aiMessage: Message = { + id: (Date.now() + 1).toString(), + role: 'assistant', + content: aiResponse.content, + thinkingContent: aiResponse.thinkingContent, + syncButton: aiResponse.syncButton, + actionCards: aiResponse.actionCards, + timestamp: new Date(), + }; + + setMessages(prev => [...prev, aiMessage]); + + // 刷新上下文状态 + onContextUpdate(); + } + } catch (err) { + console.error('[ChatArea] handleSend error:', err); + // 显示错误消息 + setMessages(prev => [...prev, { + id: (Date.now() + 1).toString(), + role: 'system', + content: '抱歉,消息发送失败。请稍后重试。', + timestamp: new Date(), + }]); + } finally { + setLoading(false); + } + }; + + /** + * 处理同步 + */ + const handleSync = async (stageCode: string, data: Record) => { + if (!conversationId) return; + + try { + const token = getAccessToken(); + const response = await fetch(`${API_BASE}/sync`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}`, + }, + body: JSON.stringify({ + conversationId, + stageCode, + data, + }), + }); + + if (!response.ok) { + throw new Error('Failed to sync data'); + } + + const result = await response.json(); + if (result.success) { + // 添加系统消息 + const systemMsg: Message = { + id: Date.now().toString(), + role: 'system', + content: result.data.message || '✅ 已同步到方案', + timestamp: new Date(), + }; + setMessages(prev => [...prev, systemMsg]); + + // 刷新上下文 + onContextUpdate(); + } + } catch (err) { + console.error('[ChatArea] handleSync error:', err); + } + }; + + // 按Enter发送 + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSend(); + } + }; + + return ( +
+ {/* 聊天历史 */} +
+ {messages.map(msg => ( +
+ {msg.role === 'user' && ( +
+
+ +
+
+
User • {formatTime(msg.timestamp)}
+
+ {msg.content} +
+
+
+ )} + + {msg.role === 'assistant' && ( +
+
+ +
+
+
Protocol Agent • {formatTime(msg.timestamp)}
+
+ {msg.content} +
+ + {/* 同步按钮 */} + {msg.syncButton && ( + + )} + + {/* Action Cards */} + {msg.actionCards && msg.actionCards.length > 0 && ( +
+ {msg.actionCards.map(card => ( + + ))} +
+ )} +
+
+ )} + + {msg.role === 'system' && ( +
+
+ {msg.content} +
+
+ )} +
+ ))} + + {loading && ( +
+
+ +
+
+
+ + + +
+
+
+ )} +
+ + {/* 输入区 */} +
+
+ setInput(e.target.value)} + onKeyDown={handleKeyDown} + disabled={loading || !conversationId} + /> + +
+
+
+ ); +}; + +/** + * 格式化时间 + */ +function formatTime(date: Date): string { + return date.toLocaleTimeString('zh-CN', { + hour: '2-digit', + minute: '2-digit' + }); +} + + diff --git a/frontend-v2/src/modules/aia/protocol-agent/components/ReflexionMessage.tsx b/frontend-v2/src/modules/aia/protocol-agent/components/ReflexionMessage.tsx new file mode 100644 index 00000000..dfb8f982 --- /dev/null +++ b/frontend-v2/src/modules/aia/protocol-agent/components/ReflexionMessage.tsx @@ -0,0 +1,52 @@ +/** + * Reflexion Message - 反思校验消息 + * + * 100%还原原型图Reflexion设计(紫色左边框) + */ + +import React from 'react'; +import { ShieldCheck, CheckCheck } from 'lucide-react'; + +interface ReflexionMessageProps { + content: string; + timestamp?: Date; +} + +export const ReflexionMessage: React.FC = ({ + content, + timestamp = new Date() +}) => { + return ( +
+
+ +
+
+
+ Reflexion Guard • {formatTime(timestamp)} +
+
+
+ + 质量校验通过 +
+
+ {content} +
+
+
+
+ ); +}; + +/** + * 格式化时间 + */ +function formatTime(date: Date): string { + return date.toLocaleTimeString('zh-CN', { + hour: '2-digit', + minute: '2-digit' + }); +} + + diff --git a/frontend-v2/src/modules/aia/protocol-agent/components/StageCard.tsx b/frontend-v2/src/modules/aia/protocol-agent/components/StageCard.tsx new file mode 100644 index 00000000..14d59f19 --- /dev/null +++ b/frontend-v2/src/modules/aia/protocol-agent/components/StageCard.tsx @@ -0,0 +1,237 @@ +/** + * Stage Card - 阶段状态卡片 + * + * 100%还原原型图阶段卡片设计 + */ + +import React from 'react'; +import { Check, Loader2, Edit2 } from 'lucide-react'; +import type { + StageInfo, + ScientificQuestionData, + PICOData, + StudyDesignData, + SampleSizeData, + EndpointsData +} from '../types'; + +interface StageCardProps { + stage: StageInfo; + index: number; +} + +const STAGE_TITLES: Record = { + scientific_question: '科学问题', + pico: 'PICO', + study_design: '研究设计', + sample_size: '样本量', + endpoints: '观察指标', +}; + +export const StageCard: React.FC = ({ stage, index }) => { + const { stageCode, status, data } = stage; + const title = STAGE_TITLES[stageCode] || stage.stageName; + const number = (index + 1).toString().padStart(2, '0'); + + // 根据状态确定样式 + const cardClasses = [ + 'stage-card', + status === 'completed' ? 'completed' : '', + status === 'current' ? 'current' : '', + status === 'pending' ? 'pending' : '', + ].filter(Boolean).join(' '); + + return ( +
+
+

{number} {title}

+ {status === 'completed' && } + {status === 'current' && } +
+ + {data && } +
+ ); +}; + +/** + * 渲染不同阶段的数据 + */ +const StageDataRenderer: React.FC<{ + stageCode: string; + data: ScientificQuestionData | PICOData | StudyDesignData | SampleSizeData | EndpointsData; +}> = ({ stageCode, data }) => { + switch (stageCode) { + case 'scientific_question': + return ; + case 'pico': + return ; + case 'study_design': + return ; + case 'sample_size': + return ; + case 'endpoints': + return ; + default: + return null; + } +}; + +/** + * 科学问题卡片 + */ +const ScientificQuestionCard: React.FC<{ data: ScientificQuestionData }> = ({ data }) => ( +
+

{data.content}

+
+); + +/** + * PICO卡片 + */ +const PICOCard: React.FC<{ data: PICOData }> = ({ data }) => ( +
+
+ P + {data.population} +
+
+ I + {data.intervention} +
+
+ C + {data.comparison} +
+
+ O + {data.outcome} +
+
+); + +/** + * 研究设计卡片 + */ +const StudyDesignCard: React.FC<{ data: StudyDesignData }> = ({ data }) => ( +
+
+ {data.studyType} + {data.design?.map((item, i) => ( + {item} + ))} +
+
+); + +/** + * 样本量卡片 + */ +const SampleSizeCard: React.FC<{ data: SampleSizeData }> = ({ data }) => ( +
+
+ 总样本量 (N) + N = {data.sampleSize} +
+ {data.calculation && ( +
+ {data.calculation.alpha && ( +
+ α = {data.calculation.alpha} +
+ )} + {data.calculation.power && ( +
+ Power = {data.calculation.power} +
+ )} +
+ )} +
+); + +/** + * 观察指标卡片 + */ +const EndpointsCard: React.FC<{ data: EndpointsData }> = ({ data }) => ( +
+ {/* 基线指标 */} + {data.baseline && Object.values(data.baseline).some(arr => arr && arr.length > 0) && ( +
+
📊 基线指标
+
+ {data.baseline.demographics?.map((item, i) => ( + {item} + ))} + {data.baseline.clinicalHistory?.map((item, i) => ( + {item} + ))} +
+
+ )} + + {/* 暴露指标 */} + {data.exposure && ( +
+
💊 暴露指标
+ {data.exposure.intervention && ( +
+ 干预: + {data.exposure.intervention} +
+ )} + {data.exposure.control && ( +
+ 对照: + {data.exposure.control} +
+ )} +
+ )} + + {/* 结局指标 */} + {data.outcomes && ( +
+
🎯 结局指标
+ {data.outcomes.primary && data.outcomes.primary.length > 0 && ( +
+ 主要: + {data.outcomes.primary.map((item, i) => ( + {item} + ))} +
+ )} + {data.outcomes.secondary && data.outcomes.secondary.length > 0 && ( +
+ 次要: + {data.outcomes.secondary.map((item, i) => ( + {item} + ))} +
+ )} + {data.outcomes.safety && data.outcomes.safety.length > 0 && ( +
+ 安全: + {data.outcomes.safety.map((item, i) => ( + {item} + ))} +
+ )} +
+ )} + + {/* 混杂因素 */} + {data.confounders && data.confounders.length > 0 && ( +
+
⚠️ 混杂因素
+
+ {data.confounders.map((item, i) => ( + {item} + ))} +
+
+ )} +
+); + + diff --git a/frontend-v2/src/modules/aia/protocol-agent/components/StatePanel.tsx b/frontend-v2/src/modules/aia/protocol-agent/components/StatePanel.tsx new file mode 100644 index 00000000..b57c458b --- /dev/null +++ b/frontend-v2/src/modules/aia/protocol-agent/components/StatePanel.tsx @@ -0,0 +1,90 @@ +/** + * State Panel - 方案状态面板 + * + * 100%还原原型图右侧状态面板设计 + */ + +import React from 'react'; +import { FileText, Check, Loader2 } from 'lucide-react'; +import type { ProtocolContext, StageInfo } from '../types'; +import { StageCard } from './StageCard'; + +interface StatePanelProps { + context: ProtocolContext | null; +} + +export const StatePanel: React.FC = ({ context }) => { + if (!context) { + return ( + + ); + } + + const { stages, progress, canGenerate } = context; + + return ( + + ); +}; + + diff --git a/frontend-v2/src/modules/aia/protocol-agent/components/SyncButton.tsx b/frontend-v2/src/modules/aia/protocol-agent/components/SyncButton.tsx new file mode 100644 index 00000000..c498a5aa --- /dev/null +++ b/frontend-v2/src/modules/aia/protocol-agent/components/SyncButton.tsx @@ -0,0 +1,50 @@ +/** + * Sync Button - 同步到方案按钮 + * + * 100%还原原型图同步按钮设计 + */ + +import React, { useState } from 'react'; +import { RefreshCw, Check } from 'lucide-react'; +import type { SyncButtonData } from '../types'; + +interface SyncButtonProps { + syncData: SyncButtonData; + onSync: (stageCode: string, data: Record) => Promise; +} + +export const SyncButton: React.FC = ({ syncData, onSync }) => { + const [syncing, setSyncing] = useState(false); + + const handleClick = async () => { + if (syncData.disabled || syncing) return; + + setSyncing(true); + try { + await onSync(syncData.stageCode, syncData.extractedData); + } finally { + setSyncing(false); + } + }; + + return ( +
+ +
+ ); +}; + + diff --git a/frontend-v2/src/modules/aia/protocol-agent/components/index.ts b/frontend-v2/src/modules/aia/protocol-agent/components/index.ts new file mode 100644 index 00000000..e46d21cb --- /dev/null +++ b/frontend-v2/src/modules/aia/protocol-agent/components/index.ts @@ -0,0 +1,12 @@ +/** + * Protocol Agent Components Export + */ + +export { ChatArea } from './ChatArea'; +export { StatePanel } from './StatePanel'; +export { StageCard } from './StageCard'; +export { SyncButton } from './SyncButton'; +export { ActionCardComponent } from './ActionCard'; +export { ReflexionMessage } from './ReflexionMessage'; + + diff --git a/frontend-v2/src/modules/aia/protocol-agent/hooks/index.ts b/frontend-v2/src/modules/aia/protocol-agent/hooks/index.ts new file mode 100644 index 00000000..f62d053c --- /dev/null +++ b/frontend-v2/src/modules/aia/protocol-agent/hooks/index.ts @@ -0,0 +1,8 @@ +/** + * Protocol Agent Hooks Export + */ + +export { useProtocolContext } from './useProtocolContext'; +export { useProtocolConversations } from './useProtocolConversations'; + + diff --git a/frontend-v2/src/modules/aia/protocol-agent/hooks/useProtocolContext.ts b/frontend-v2/src/modules/aia/protocol-agent/hooks/useProtocolContext.ts new file mode 100644 index 00000000..590d4a2e --- /dev/null +++ b/frontend-v2/src/modules/aia/protocol-agent/hooks/useProtocolContext.ts @@ -0,0 +1,79 @@ +/** + * useProtocolContext Hook + * 管理Protocol Agent的上下文状态 + */ + +import { useState, useEffect, useCallback } from 'react'; +import type { ProtocolContext } from '../types'; +import { getAccessToken } from '../../../../framework/auth/api'; + +const API_BASE = '/api/v1/aia/protocol-agent'; + +export function useProtocolContext(conversationId?: string) { + const [context, setContext] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + /** + * 获取上下文状态 + */ + const fetchContext = useCallback(async () => { + if (!conversationId) { + setContext(null); + return; + } + + setLoading(true); + setError(null); + + try { + const token = getAccessToken(); + const response = await fetch(`${API_BASE}/context/${conversationId}`, { + headers: { + 'Authorization': `Bearer ${token}`, + }, + }); + + if (response.status === 404) { + // 上下文不存在,返回空 + setContext(null); + return; + } + + if (!response.ok) { + throw new Error(`Failed to fetch context: ${response.statusText}`); + } + + const result = await response.json(); + if (result.success) { + setContext(result.data); + } + } catch (err) { + console.error('[useProtocolContext] fetchContext error:', err); + setError(err instanceof Error ? err.message : 'Failed to load context'); + } finally { + setLoading(false); + } + }, [conversationId]); + + /** + * 刷新上下文 + */ + const refreshContext = useCallback(() => { + fetchContext(); + }, [fetchContext]); + + // 初次加载和conversationId变化时获取上下文 + useEffect(() => { + fetchContext(); + }, [fetchContext]); + + return { + context, + loading, + error, + refreshContext, + }; +} + + diff --git a/frontend-v2/src/modules/aia/protocol-agent/hooks/useProtocolConversations.ts b/frontend-v2/src/modules/aia/protocol-agent/hooks/useProtocolConversations.ts new file mode 100644 index 00000000..33397540 --- /dev/null +++ b/frontend-v2/src/modules/aia/protocol-agent/hooks/useProtocolConversations.ts @@ -0,0 +1,144 @@ +/** + * useProtocolConversations Hook + * 管理Protocol Agent的会话列表 + */ + +import { useState, useEffect, useCallback } from 'react'; +import type { ProtocolConversation } from '../types'; +import { getAccessToken } from '../../../../framework/auth/api'; + +const API_BASE = '/api/v1/aia'; + +export function useProtocolConversations(initialConversationId?: string) { + const [conversations, setConversations] = useState([]); + const [currentConversation, setCurrentConversation] = useState(null); + const [loading, setLoading] = useState(false); + + /** + * 获取会话列表 + */ + const fetchConversations = useCallback(async () => { + setLoading(true); + try { + const token = getAccessToken(); + const response = await fetch(`${API_BASE}/conversations`, { + headers: { + 'Authorization': `Bearer ${token}`, + }, + }); + + if (!response.ok) { + throw new Error('Failed to fetch conversations'); + } + + const result = await response.json(); + const allConversations = result.data || result; + + // 过滤出Protocol Agent的对话(agentId为PROTOCOL_AGENT) + const protocolConversations = Array.isArray(allConversations) + ? allConversations.filter((conv: any) => + conv.agentId === 'PROTOCOL_AGENT' || conv.agent_id === 'PROTOCOL_AGENT' + ) + : []; + + setConversations(protocolConversations); + + // 如果有initialConversationId,设置为当前对话 + if (initialConversationId) { + const current = protocolConversations.find((c: any) => c.id === initialConversationId); + if (current) { + setCurrentConversation(current); + } + } + } catch (err) { + console.error('[useProtocolConversations] fetchConversations error:', err); + } finally { + setLoading(false); + } + }, [initialConversationId]); + + /** + * 创建新对话 + */ + const createConversation = useCallback(async (): Promise => { + try { + const token = getAccessToken(); + const response = await fetch(`${API_BASE}/conversations`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}`, + }, + body: JSON.stringify({ + agentId: 'PROTOCOL_AGENT', + title: `研究方案-${new Date().toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })}`, + }), + }); + + if (!response.ok) { + throw new Error('Failed to create conversation'); + } + + const result = await response.json(); + const newConv = result.data || result; + + setConversations(prev => [newConv, ...prev]); + setCurrentConversation(newConv); + + return newConv; + } catch (err) { + console.error('[useProtocolConversations] createConversation error:', err); + return null; + } + }, []); + + /** + * 选择对话 + */ + const selectConversation = useCallback((id: string) => { + const conv = conversations.find(c => c.id === id); + if (conv) { + setCurrentConversation(conv); + } + }, [conversations]); + + /** + * 删除对话 + */ + const deleteConversation = useCallback(async (id: string) => { + try { + const token = getAccessToken(); + await fetch(`${API_BASE}/conversations/${id}`, { + method: 'DELETE', + headers: { + 'Authorization': `Bearer ${token}`, + }, + }); + + setConversations(prev => prev.filter(c => c.id !== id)); + + if (currentConversation?.id === id) { + setCurrentConversation(null); + } + } catch (err) { + console.error('[useProtocolConversations] deleteConversation error:', err); + } + }, [currentConversation]); + + // 初次加载 + useEffect(() => { + fetchConversations(); + }, [fetchConversations]); + + return { + conversations, + currentConversation, + loading, + createConversation, + selectConversation, + deleteConversation, + refreshConversations: fetchConversations, + }; +} + + diff --git a/frontend-v2/src/modules/aia/protocol-agent/index.ts b/frontend-v2/src/modules/aia/protocol-agent/index.ts new file mode 100644 index 00000000..14a9dd20 --- /dev/null +++ b/frontend-v2/src/modules/aia/protocol-agent/index.ts @@ -0,0 +1,10 @@ +/** + * Protocol Agent Module Export + */ + +export { default as ProtocolAgentPage } from './ProtocolAgentPage'; +export * from './types'; +export * from './components'; +export * from './hooks'; + + diff --git a/frontend-v2/src/modules/aia/protocol-agent/styles/protocol-agent.css b/frontend-v2/src/modules/aia/protocol-agent/styles/protocol-agent.css new file mode 100644 index 00000000..9aecdd0f --- /dev/null +++ b/frontend-v2/src/modules/aia/protocol-agent/styles/protocol-agent.css @@ -0,0 +1,1208 @@ +/** + * Protocol Agent 样式 + * 100%还原原型图0119.html的精致设计 + */ + +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Noto+Sans+SC:wght@300;400;500;700&display=swap'); + +/* ============================================ */ +/* 全局样式 */ +/* ============================================ */ +.protocol-agent-page { + display: flex; + height: 100vh; + overflow: hidden; + font-family: 'Inter', 'Noto Sans SC', sans-serif; + background: #FFFFFF; +} + +/* ============================================ */ +/* 可折叠侧边栏 - Gemini 风格 */ +/* ============================================ */ +.sidebar { + display: flex; + background: #F9FAFB; + border-right: 1px solid #E5E7EB; + transition: width 0.3s ease; + position: relative; + z-index: 10; +} + +.sidebar.collapsed { + width: 48px; +} + +.sidebar.expanded { + width: 280px; +} + +/* 图标栏(折叠时可见) */ +.sidebar-icons { + width: 48px; + display: flex; + flex-direction: column; + padding: 8px 0; + background: #F9FAFB; + border-right: 1px solid #E5E7EB; +} + +.icon-btn { + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: none; + border-radius: 8px; + cursor: pointer; + color: #6B7280; + transition: all 0.2s; + margin: 0 4px 4px 4px; +} + +.icon-btn:hover { + background: #E5E7EB; + color: #111827; +} + +.icon-btn.menu-btn { + margin-bottom: 12px; +} + +.icon-btn.new-btn { + background: #6366F1; + color: white; + margin-bottom: 12px; +} + +.icon-btn.new-btn:hover { + background: #4F46E5; +} + +/* 最近会话图标 */ +.recent-icons { + flex: 1; + overflow-y: auto; + padding-top: 8px; +} + +.icon-btn.conv-icon { + opacity: 0.6; +} + +.icon-btn.conv-icon:hover { + opacity: 1; +} + +.icon-btn.conv-icon.active { + background: #E0E7FF; + color: #6366F1; + opacity: 1; +} + +.sidebar-bottom { + padding-top: 8px; + border-top: 1px solid #E5E7EB; +} + +/* 侧边栏内容(展开时可见) */ +.sidebar-content { + flex: 1; + display: flex; + flex-direction: column; + background: #FFFFFF; + opacity: 0; + transition: opacity 0.3s ease; + pointer-events: none; +} + +.sidebar.expanded .sidebar-content { + opacity: 1; + pointer-events: auto; +} + +.sidebar.collapsed .sidebar-content { + display: none; +} + +.sidebar-header { + display: flex; + align-items: center; + gap: 8px; + padding: 12px; + border-bottom: 1px solid #E5E7EB; +} + +.sidebar-title { + font-size: 14px; + font-weight: 600; + color: #111827; +} + +.new-chat-btn { + margin: 12px; + padding: 10px 16px; + background: #6366F1; + color: white; + border: none; + border-radius: 8px; + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + font-size: 14px; + font-weight: 500; + transition: all 0.2s; +} + +.new-chat-btn:hover { + background: #4F46E5; +} + +/* 会话列表 */ +.conversations-list { + flex: 1; + overflow-y: auto; + padding: 8px; +} + +.conv-item { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 12px; + border-radius: 8px; + cursor: pointer; + transition: all 0.2s; + color: #6B7280; + font-size: 13px; + position: relative; +} + +.conv-item:hover { + background: #F3F4F6; +} + +.conv-item.active { + background: #EEF2FF; + color: #6366F1; +} + +.conv-title { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.conv-delete { + opacity: 0; + background: none; + border: none; + padding: 4px; + border-radius: 4px; + cursor: pointer; + color: #EF4444; + transition: all 0.2s; +} + +.conv-item:hover .conv-delete { + opacity: 1; +} + +.conv-delete:hover { + background: #FEE2E2; +} + +/* ============================================ */ +/* 主工作区 */ +/* ============================================ */ +.workspace { + flex: 1; + display: flex; + flex-direction: column; + background: #FFFFFF; + overflow: hidden; +} + +/* 顶部导航 */ +.workspace-header { + height: 56px; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 16px; + border-bottom: 1px solid #E5E7EB; + background: #FFFFFF; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); + z-index: 5; +} + +.header-left { + display: flex; + align-items: center; + gap: 12px; +} + +.back-btn { + padding: 8px; + background: none; + border: none; + border-radius: 8px; + cursor: pointer; + color: #6B7280; + transition: all 0.2s; +} + +.back-btn:hover { + background: #F3F4F6; + color: #111827; +} + +.agent-info { + display: flex; + align-items: center; + gap: 12px; +} + +.agent-icon { + width: 32px; + height: 32px; + background: #6366F1; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + color: white; + box-shadow: 0 2px 8px rgba(99, 102, 241, 0.2); +} + +.agent-meta { + display: flex; + flex-direction: column; + gap: 2px; +} + +.agent-name { + font-size: 14px; + font-weight: 700; + color: #111827; + margin: 0; +} + +.agent-status { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: #6B7280; +} + +.status-dot { + width: 8px; + height: 8px; + background: #10B981; + border-radius: 50%; + animation: pulse 2s ease-in-out infinite; +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +.status-text { + font-size: 12px; +} + +.header-right { + display: flex; + align-items: center; + gap: 12px; +} + +.current-stage-badge { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 12px; + background: #F9FAFB; + border: 1px solid #E5E7EB; + border-radius: 20px; +} + +.badge-label { + font-size: 12px; + font-weight: 500; + color: #6B7280; +} + +.badge-value { + font-size: 12px; + font-weight: 700; + color: #6366F1; + background: #EEF2FF; + padding: 2px 8px; + border-radius: 4px; + border: 1px solid #C7D2FE; +} + +/* 工作区主体 */ +.workspace-body { + flex: 1; + display: flex; + overflow: hidden; +} + +/* ============================================ */ +/* 聊天区域 */ +/* ============================================ */ +.chat-area { + flex: 1; + display: flex; + flex-direction: column; + background: #FFFFFF; + position: relative; +} + +/* 聊天容器 */ +.chat-container { + flex: 1; + overflow-y: auto; + padding: 24px; + padding-bottom: 120px; +} + +/* 消息行 */ +.message-row { + display: flex; + gap: 16px; + margin-bottom: 24px; +} + +.message-row.user-row { + flex-direction: row-reverse; +} + +.message-row.assistant-row { + flex-direction: row; +} + +.message-row.system-row { + justify-content: center; + margin: 16px 0; +} + +/* 头像 */ +.avatar { + width: 32px; + height: 32px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.assistant-avatar { + background: #EEF2FF; + color: #6366F1; +} + +.user-avatar { + background: #E5E7EB; + color: #6B7280; +} + +.reflexion-avatar { + background: #F3E8FF; + color: #9333EA; + border: 1px solid #E9D5FF; +} + +/* 消息内容 */ +.message-content { + max-width: 80%; + display: flex; + flex-direction: column; + gap: 4px; +} + +.message-meta { + font-size: 12px; + color: #9CA3AF; + padding: 0 4px; +} + +.message-meta.reflexion-meta { + color: #9333EA; + font-weight: 700; +} + +/* 聊天气泡 */ +.chat-bubble { + padding: 16px; + border-radius: 12px; + font-size: 14px; + line-height: 1.6; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.assistant-bubble { + background: #F3F4F6; + color: #111827; + border-top-left-radius: 2px; +} + +.user-bubble { + background: #4F46E5; + color: white; + border-top-right-radius: 2px; +} + +/* Reflexion 气泡(紫色左边框) */ +.reflexion-bubble { + background: #F3E8FF; + border-left: 4px solid #9333EA; +} + +.reflexion-header { + display: flex; + align-items: center; + gap: 6px; + font-weight: 700; + color: #7C3AED; + font-size: 12px; + margin-bottom: 8px; +} + +.reflexion-content { + color: #111827; +} + +/* 思考中动画 */ +.chat-bubble.thinking { + display: flex; + gap: 4px; + padding: 12px 16px; +} + +.thinking-dot { + width: 6px; + height: 6px; + background: #9CA3AF; + border-radius: 50%; + animation: thinking 1.4s ease-in-out infinite; +} + +.thinking-dot:nth-child(2) { + animation-delay: 0.2s; +} + +.thinking-dot:nth-child(3) { + animation-delay: 0.4s; +} + +@keyframes thinking { + 0%, 60%, 100% { + transform: translateY(0); + } + 30% { + transform: translateY(-8px); + } +} + +/* 系统消息 */ +.system-message { + font-size: 10px; + color: #6B7280; + background: #F3F4F6; + padding: 6px 12px; + border-radius: 12px; + border: 1px solid #E5E7EB; + display: inline-flex; + align-items: center; + gap: 4px; +} + +/* ============================================ */ +/* 同步按钮 */ +/* ============================================ */ +.sync-button-wrapper { + margin-top: 12px; +} + +.sync-button { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 8px 16px; + background: #6366F1; + color: white; + border: none; + border-radius: 8px; + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + box-shadow: 0 2px 8px rgba(99, 102, 241, 0.2); +} + +.sync-button:hover:not(.disabled) { + background: #4F46E5; + box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3); +} + +.sync-button.disabled { + background: #E5E7EB; + color: #9CA3AF; + cursor: not-allowed; + box-shadow: none; +} + +.sync-button.syncing { + pointer-events: none; +} + +.sync-icon.spinning { + animation: spin 1s linear infinite; +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +/* ============================================ */ +/* Action Card */ +/* ============================================ */ +.action-cards { + margin-top: 12px; + display: flex; + flex-direction: column; + gap: 12px; +} + +.action-card { + border: 1px solid #C7D2FE; + background: #EEF2FF; + border-radius: 12px; + padding: 16px; + width: 380px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); + transition: all 0.2s; + cursor: pointer; +} + +.action-card:hover { + border-color: #6366F1; + box-shadow: 0 4px 12px rgba(99, 102, 241, 0.15); +} + +.action-card-header { + display: flex; + gap: 12px; + margin-bottom: 12px; +} + +.action-icon-box { + background: #C7D2FE; + padding: 8px; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + color: #6366F1; + flex-shrink: 0; +} + +.action-info { + flex: 1; +} + +.action-title { + font-size: 14px; + font-weight: 700; + color: #111827; + margin: 0 0 4px 0; +} + +.action-description { + font-size: 12px; + color: #6B7280; + margin: 0; +} + +.action-footer { + display: flex; + flex-direction: column; + gap: 8px; +} + +.action-button { + width: 100%; + padding: 8px 16px; + background: #6366F1; + color: white; + border: none; + border-radius: 8px; + font-size: 13px; + font-weight: 600; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + transition: all 0.2s; + box-shadow: 0 2px 4px rgba(99, 102, 241, 0.15); +} + +.action-button:hover { + background: #4F46E5; + box-shadow: 0 4px 8px rgba(99, 102, 241, 0.25); +} + +.action-button.primary { + background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%); +} + +.action-hint { + font-size: 10px; + color: #9CA3AF; + text-align: center; + margin: 0; +} + +/* ============================================ */ +/* 输入区 */ +/* ============================================ */ +.input-area { + position: absolute; + bottom: 0; + left: 0; + right: 0; + padding: 16px; + border-top: 1px solid #F3F4F6; + background: #FFFFFF; +} + +.input-wrapper { + position: relative; + display: flex; +} + +.input-field { + flex: 1; + padding: 12px 48px 12px 16px; + background: #F9FAFB; + border: 1px solid #E5E7EB; + border-radius: 12px; + font-size: 14px; + color: #111827; + transition: all 0.2s; + outline: none; +} + +.input-field:focus { + background: #FFFFFF; + border-color: #6366F1; + box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1); +} + +.input-field::placeholder { + color: #9CA3AF; +} + +.input-field:disabled { + background: #F3F4F6; + color: #9CA3AF; + cursor: not-allowed; +} + +.send-btn { + position: absolute; + right: 4px; + top: 50%; + transform: translateY(-50%); + width: 36px; + height: 36px; + background: #6366F1; + color: white; + border: none; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s; +} + +.send-btn:hover:not(:disabled) { + background: #4F46E5; +} + +.send-btn:disabled { + background: #E5E7EB; + color: #9CA3AF; + cursor: not-allowed; +} + +/* ============================================ */ +/* 状态面板 */ +/* ============================================ */ +.state-panel { + width: 350px; + background: #F9FAFB; + border-left: 1px solid #E5E7EB; + display: flex; + flex-direction: column; +} + +.panel-header { + padding: 16px; + border-bottom: 1px solid #E5E7EB; + background: #FFFFFF; + display: flex; + justify-content: space-between; + align-items: center; +} + +.panel-title { + font-size: 14px; + font-weight: 700; + color: #111827; + display: flex; + align-items: center; + gap: 8px; + margin: 0; +} + +.status-badge { + font-size: 10px; + font-weight: 600; + padding: 4px 8px; + border-radius: 12px; + text-transform: uppercase; +} + +.status-badge.waiting { + background: #FEF3C7; + color: #92400E; +} + +.status-badge.synced { + background: #D1FAE5; + color: #065F46; +} + +.status-badge.completed { + background: #DBEAFE; + color: #1E40AF; +} + +/* 进度条 */ +.progress-bar-container { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 16px; + background: #FFFFFF; + border-bottom: 1px solid #E5E7EB; +} + +.progress-bar { + flex: 1; + height: 6px; + background: #E5E7EB; + border-radius: 3px; + overflow: hidden; +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, #6366F1 0%, #4F46E5 100%); + transition: width 0.5s ease; +} + +.progress-text { + font-size: 12px; + font-weight: 700; + color: #6366F1; + min-width: 40px; + text-align: right; +} + +/* 面板主体 */ +.panel-body { + flex: 1; + overflow-y: auto; + padding: 16px; +} + +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; +} + +/* ============================================ */ +/* Stage Card - 阶段卡片 */ +/* ============================================ */ +.stage-card { + background: #FFFFFF; + padding: 12px; + border-radius: 8px; + border: 1px solid #E5E7EB; + margin-bottom: 16px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03); + transition: all 0.3s ease; +} + +.stage-card.current { + border-color: #A5B4FC; + border-width: 2px; + box-shadow: 0 2px 8px rgba(99, 102, 241, 0.12); +} + +.stage-card.completed { + border-color: #D1FAE5; + background: #F0FDF4; +} + +.stage-card.pending { + opacity: 0.6; +} + +.stage-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} + +.stage-number { + font-size: 12px; + font-weight: 700; + color: #9CA3AF; + text-transform: uppercase; + letter-spacing: 0.05em; + margin: 0; +} + +.stage-card.current .stage-number { + color: #6366F1; +} + +.stage-card.completed .stage-number { + color: #10B981; +} + +.check-icon { + color: #10B981; +} + +.loader-icon { + color: #6366F1; +} + +/* 阶段数据 */ +.stage-data { + font-size: 12px; + color: #374151; + line-height: 1.5; +} + +.data-content { + margin: 0; + font-weight: 500; +} + +/* PICO数据 */ +.pico-data { + display: flex; + flex-direction: column; + gap: 8px; +} + +.pico-item { + display: flex; + gap: 8px; + align-items: flex-start; +} + +.pico-label { + display: inline-block; + width: 24px; + height: 20px; + line-height: 20px; + text-align: center; + border-radius: 4px; + font-weight: 700; + font-size: 10px; + flex-shrink: 0; +} + +.pico-label.p { + background: #DBEAFE; + color: #1E40AF; +} + +.pico-label.i { + background: #E0E7FF; + color: #4338CA; +} + +.pico-label.c { + background: #FED7AA; + color: #C2410C; +} + +.pico-label.o { + background: #D1FAE5; + color: #065F46; +} + +.pico-value { + flex: 1; + font-size: 12px; + color: #374151; +} + +/* 研究设计标签 */ +.design-tags { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.design-tag { + font-size: 10px; + border: 1px solid #E5E7EB; + padding: 4px 8px; + border-radius: 4px; + background: #F9FAFB; + color: #374151; +} + +/* 样本量数据 */ +.sample-size-data { + display: flex; + flex-direction: column; + gap: 8px; +} + +.sample-size-row { + display: flex; + justify-content: space-between; + align-items: center; +} + +.sample-size-row .label { + font-size: 12px; + color: #6B7280; +} + +.sample-size-row .value { + font-size: 18px; + font-weight: 700; + color: #6366F1; + font-family: 'Monaco', 'Consolas', monospace; +} + +.calculation-params { + display: flex; + gap: 12px; + font-size: 11px; + color: #6B7280; +} + +/* 观察指标数据 */ +.endpoints-data { + display: flex; + flex-direction: column; + gap: 12px; +} + +.endpoint-section { + display: flex; + flex-direction: column; + gap: 6px; +} + +.endpoint-title { + font-size: 11px; + font-weight: 700; + color: #6B7280; +} + +.endpoint-tags { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.endpoint-tag { + font-size: 10px; + background: #F3F4F6; + color: #374151; + padding: 3px 8px; + border-radius: 4px; + border: 1px solid #E5E7EB; +} + +.endpoint-tag.primary { + background: #DBEAFE; + border-color: #BFDBFE; + color: #1E40AF; +} + +.endpoint-tag.safety { + background: #FEE2E2; + border-color: #FECACA; + color: #991B1B; +} + +.endpoint-tag.confounder { + background: #FEF3C7; + border-color: #FDE68A; + color: #92400E; +} + +.endpoint-item { + display: flex; + gap: 6px; + font-size: 11px; +} + +.item-label { + color: #9CA3AF; + font-weight: 500; +} + +.item-value { + color: #374151; + flex: 1; +} + +.endpoint-subsection { + display: flex; + flex-wrap: wrap; + gap: 6px; + align-items: center; +} + +.subsection-label { + font-size: 10px; + color: #9CA3AF; + font-weight: 600; + min-width: 40px; +} + +/* ============================================ */ +/* 一键生成按钮 */ +/* ============================================ */ +.generate-section { + margin-top: 24px; + padding: 16px; + background: linear-gradient(135deg, #EEF2FF 0%, #E0E7FF 100%); + border: 2px solid #C7D2FE; + border-radius: 12px; + text-align: center; +} + +.generate-btn { + width: 100%; + padding: 12px 20px; + background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%); + color: white; + border: none; + border-radius: 10px; + font-size: 14px; + font-weight: 700; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + transition: all 0.2s; + box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3); +} + +.generate-btn:hover { + transform: translateY(-2px); + box-shadow: 0 6px 16px rgba(99, 102, 241, 0.4); +} + +.generate-icon { + font-size: 18px; +} + +.generate-text { + font-size: 14px; +} + +.generate-hint { + font-size: 10px; + color: #6B7280; + margin: 8px 0 0 0; +} + +/* ============================================ */ +/* 闪烁动画(同步更新) */ +/* ============================================ */ +@keyframes flash { + 0% { background-color: #DBEAFE; } + 100% { background-color: transparent; } +} + +.flash-update { + animation: flash 1.5s ease-out; +} + +/* ============================================ */ +/* 滚动条美化 */ +/* ============================================ */ +.chat-container::-webkit-scrollbar, +.panel-body::-webkit-scrollbar, +.conversations-list::-webkit-scrollbar { + width: 6px; +} + +.chat-container::-webkit-scrollbar-track, +.panel-body::-webkit-scrollbar-track, +.conversations-list::-webkit-scrollbar-track { + background: transparent; +} + +.chat-container::-webkit-scrollbar-thumb, +.panel-body::-webkit-scrollbar-thumb, +.conversations-list::-webkit-scrollbar-thumb { + background: #CBD5E1; + border-radius: 3px; +} + +.chat-container::-webkit-scrollbar-thumb:hover, +.panel-body::-webkit-scrollbar-thumb:hover, +.conversations-list::-webkit-scrollbar-thumb:hover { + background: #94A3B8; +} + +/* ============================================ */ +/* 响应式 */ +/* ============================================ */ +@media (max-width: 1024px) { + .state-panel { + display: none; + } + + .sidebar.expanded { + position: absolute; + z-index: 20; + height: 100%; + box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1); + } +} + diff --git a/frontend-v2/src/modules/aia/protocol-agent/types.ts b/frontend-v2/src/modules/aia/protocol-agent/types.ts new file mode 100644 index 00000000..83e5be25 --- /dev/null +++ b/frontend-v2/src/modules/aia/protocol-agent/types.ts @@ -0,0 +1,175 @@ +/** + * Protocol Agent 类型定义 + */ + +export type ProtocolStageCode = + | 'scientific_question' + | 'pico' + | 'study_design' + | 'sample_size' + | 'endpoints'; + +export type StageStatus = 'completed' | 'current' | 'pending'; + +/** + * 科学问题数据 + */ +export interface ScientificQuestionData { + content: string; + background?: string; + significance?: string; + confirmed?: boolean; + confirmedAt?: string; +} + +/** + * PICO数据 + */ +export interface PICOData { + population: string; + intervention: string; + comparison: string; + outcome: string; + confirmed?: boolean; + confirmedAt?: string; +} + +/** + * 研究设计数据 + */ +export interface StudyDesignData { + studyType: string; + design: string[]; + features?: string[]; + confirmed?: boolean; + confirmedAt?: string; +} + +/** + * 样本量数据 + */ +export interface SampleSizeData { + sampleSize: number; + calculation?: { + alpha?: number; + power?: number; + effectSize?: number; + dropoutRate?: number; + }; + confirmed?: boolean; + confirmedAt?: string; +} + +/** + * 观察指标数据 + */ +export interface EndpointsData { + baseline?: { + demographics?: string[]; + clinicalHistory?: string[]; + laboratoryTests?: string[]; + }; + exposure?: { + intervention?: string; + control?: string; + dosage?: string; + duration?: string; + }; + outcomes?: { + primary?: string[]; + secondary?: string[]; + safety?: string[]; + }; + confounders?: string[]; + confirmed?: boolean; + confirmedAt?: string; +} + +/** + * 阶段状态 + */ +export interface StageInfo { + stageCode: ProtocolStageCode; + stageName: string; + status: StageStatus; + data: ScientificQuestionData | PICOData | StudyDesignData | SampleSizeData | EndpointsData | null; +} + +/** + * Protocol上下文 + */ +export interface ProtocolContext { + currentStage: ProtocolStageCode; + stageName: string; + progress: number; + stages: StageInfo[]; + canGenerate: boolean; +} + +/** + * 同步按钮数据 + */ +export interface SyncButtonData { + stageCode: ProtocolStageCode; + extractedData: Record; + label: string; + disabled: boolean; +} + +/** + * Action Card数据 + */ +export interface ActionCard { + id: string; + type: 'tool' | 'action'; + title: string; + description: string; + actionUrl: string; +} + +/** + * Agent响应 + */ +export interface AgentResponse { + content: string; + thinkingContent?: string; + stage: ProtocolStageCode; + stageName: string; + syncButton?: SyncButtonData; + actionCards?: ActionCard[]; + tokensUsed?: number; + modelUsed?: string; +} + +/** + * 对话 + */ +export interface ProtocolConversation { + id: string; + title: string; + createdAt: string; + updatedAt: string; +} + +/** + * 方案生成请求 + */ +export interface GenerateProtocolRequest { + conversationId: string; + options?: { + sections?: string[]; + style?: 'academic' | 'concise'; + }; +} + +/** + * 方案生成响应 + */ +export interface GenerateProtocolResponse { + generationId: string; + status: 'generating' | 'completed'; + content?: string; + estimatedTime?: number; +} + + diff --git a/frontend-v2/src/modules/aia/styles/agent-card.css b/frontend-v2/src/modules/aia/styles/agent-card.css index 1c06d40c..7aba197d 100644 --- a/frontend-v2/src/modules/aia/styles/agent-card.css +++ b/frontend-v2/src/modules/aia/styles/agent-card.css @@ -208,6 +208,62 @@ transform: translate(2px, -2px); } +/* === Indigo主题(Protocol Agent 全流程) === */ +.agent-card.theme-indigo { + background: linear-gradient(135deg, #EEF2FF 0%, #E0E7FF 100%); + border-color: #C7D2FE; + border-width: 2px; +} + +.agent-card.theme-indigo:hover { + border-color: #6366F1; + background: linear-gradient(135deg, #E0E7FF 0%, #C7D2FE 100%); + box-shadow: 0 8px 20px rgba(99, 102, 241, 0.2); +} + +.agent-card.theme-indigo .icon-box { + background-color: #6366F1; + border-color: #4F46E5; +} + +.agent-card.theme-indigo .agent-icon { + color: #FFFFFF; +} + +.agent-card.theme-indigo:hover .icon-box { + background-color: #4F46E5; + border-color: #4338CA; + transform: scale(1.08); +} + +.agent-card.theme-indigo .num-watermark { + color: #C7D2FE; +} + +.agent-card.theme-indigo:hover .num-watermark { + color: #A5B4FC; +} + +/* Protocol Agent 特殊标记 */ +.agent-card.protocol-agent-card { + position: relative; +} + +.agent-card.protocol-agent-card::before { + content: '测试'; + position: absolute; + top: 8px; + right: 8px; + font-size: 10px; + font-weight: 600; + color: #6366F1; + background: #E0E7FF; + padding: 2px 6px; + border-radius: 4px; + border: 1px solid #C7D2FE; +} + + diff --git a/frontend-v2/src/modules/aia/styles/agent-hub.css b/frontend-v2/src/modules/aia/styles/agent-hub.css index 9497d690..162a284e 100644 --- a/frontend-v2/src/modules/aia/styles/agent-hub.css +++ b/frontend-v2/src/modules/aia/styles/agent-hub.css @@ -11,6 +11,7 @@ --brand-teal: #0D9488; --brand-purple: #9333EA; --brand-yellow: #CA8A04; + --brand-indigo: #6366F1; } /* === 整体布局 === */ @@ -182,6 +183,19 @@ background-color: var(--brand-purple); } +.timeline-dot.theme-indigo { + background-color: var(--brand-indigo); +} + +/* Protocol Agent 特殊样式 */ +.timeline-dot.protocol-dot { + width: 28px; + height: 28px; + font-size: 14px; + background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%); + box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3); +} + .timeline-line { position: absolute; top: 28px; @@ -220,6 +234,31 @@ border-radius: 4px; } +/* Beta徽章(Protocol Agent) */ +.beta-badge { + font-size: 10px; + font-weight: 500; + color: var(--brand-indigo); + background-color: #EEF2FF; + border: 1px solid #C7D2FE; + padding: 2px 8px; + border-radius: 4px; +} + +/* Protocol Agent 阶段标题 */ +.protocol-title { + color: var(--brand-indigo); +} + +/* Protocol Agent 网格(单列,宽度限制) */ +.protocol-grid { + max-width: 340px; +} + +.agents-grid.grid-cols-1 { + grid-template-columns: 1fr; +} + /* === 卡片网格 === */ .agents-grid { display: grid; diff --git a/frontend-v2/src/modules/aia/types.ts b/frontend-v2/src/modules/aia/types.ts index 84b52950..ff7d3147 100644 --- a/frontend-v2/src/modules/aia/types.ts +++ b/frontend-v2/src/modules/aia/types.ts @@ -5,7 +5,7 @@ /** * 智能体阶段主题色 */ -export type AgentTheme = 'blue' | 'yellow' | 'teal' | 'purple'; +export type AgentTheme = 'blue' | 'yellow' | 'teal' | 'purple' | 'indigo'; /** * 智能体配置 @@ -18,8 +18,9 @@ export interface AgentConfig { theme: AgentTheme; phase: number; order: number; - isTool?: boolean; // 是否为工具类(跳转外部) - toolUrl?: string; // 工具跳转地址 + isTool?: boolean; // 是否为工具类(跳转外部) + toolUrl?: string; // 工具跳转地址 + isProtocolAgent?: boolean; // 是否为Protocol Agent(全流程) } /** @@ -30,6 +31,7 @@ export interface PhaseConfig { name: string; theme: AgentTheme; isTool?: boolean; + isProtocolAgent?: boolean; // 是否为Protocol Agent阶段 } /** diff --git a/frontend-v2/src/modules/asl/components/FulltextDetailDrawer.tsx b/frontend-v2/src/modules/asl/components/FulltextDetailDrawer.tsx index c597f74e..201a714d 100644 --- a/frontend-v2/src/modules/asl/components/FulltextDetailDrawer.tsx +++ b/frontend-v2/src/modules/asl/components/FulltextDetailDrawer.tsx @@ -577,6 +577,8 @@ export default FulltextDetailDrawer; + + diff --git a/frontend-v2/src/modules/dc/hooks/useAssets.ts b/frontend-v2/src/modules/dc/hooks/useAssets.ts index 60afe7da..8d7a209b 100644 --- a/frontend-v2/src/modules/dc/hooks/useAssets.ts +++ b/frontend-v2/src/modules/dc/hooks/useAssets.ts @@ -170,6 +170,8 @@ export const useAssets = (activeTab: AssetTabType) => { + + diff --git a/frontend-v2/src/modules/dc/hooks/useRecentTasks.ts b/frontend-v2/src/modules/dc/hooks/useRecentTasks.ts index 5ccbefd2..a82e860d 100644 --- a/frontend-v2/src/modules/dc/hooks/useRecentTasks.ts +++ b/frontend-v2/src/modules/dc/hooks/useRecentTasks.ts @@ -160,6 +160,8 @@ export const useRecentTasks = () => { + + diff --git a/frontend-v2/src/modules/dc/pages/tool-c/hooks/useSessionStatus.ts b/frontend-v2/src/modules/dc/pages/tool-c/hooks/useSessionStatus.ts index 792d3af2..85cc9eb4 100644 --- a/frontend-v2/src/modules/dc/pages/tool-c/hooks/useSessionStatus.ts +++ b/frontend-v2/src/modules/dc/pages/tool-c/hooks/useSessionStatus.ts @@ -130,6 +130,8 @@ export function useSessionStatus({ + + diff --git a/frontend-v2/src/modules/dc/pages/tool-c/types/index.ts b/frontend-v2/src/modules/dc/pages/tool-c/types/index.ts index 6455910b..4f64f5cf 100644 --- a/frontend-v2/src/modules/dc/pages/tool-c/types/index.ts +++ b/frontend-v2/src/modules/dc/pages/tool-c/types/index.ts @@ -122,6 +122,8 @@ export interface DataStats { + + diff --git a/frontend-v2/src/modules/dc/types/portal.ts b/frontend-v2/src/modules/dc/types/portal.ts index f755b0c9..8aa10286 100644 --- a/frontend-v2/src/modules/dc/types/portal.ts +++ b/frontend-v2/src/modules/dc/types/portal.ts @@ -118,6 +118,8 @@ export type AssetTabType = 'all' | 'processed' | 'raw'; + + diff --git a/frontend-v2/src/modules/pkb/pages/KnowledgePage.tsx b/frontend-v2/src/modules/pkb/pages/KnowledgePage.tsx index f1ca97f3..a70780fd 100644 --- a/frontend-v2/src/modules/pkb/pages/KnowledgePage.tsx +++ b/frontend-v2/src/modules/pkb/pages/KnowledgePage.tsx @@ -305,6 +305,8 @@ export default KnowledgePage; + + diff --git a/frontend-v2/src/modules/pkb/types/workspace.ts b/frontend-v2/src/modules/pkb/types/workspace.ts index 0ced2dea..f4f03cb6 100644 --- a/frontend-v2/src/modules/pkb/types/workspace.ts +++ b/frontend-v2/src/modules/pkb/types/workspace.ts @@ -60,6 +60,8 @@ export interface BatchTemplate { + + diff --git a/frontend-v2/src/modules/rvw/components/AgentModal.tsx b/frontend-v2/src/modules/rvw/components/AgentModal.tsx index 648af4c6..30390ecb 100644 --- a/frontend-v2/src/modules/rvw/components/AgentModal.tsx +++ b/frontend-v2/src/modules/rvw/components/AgentModal.tsx @@ -138,6 +138,8 @@ export default function AgentModal({ visible, taskCount, onClose, onConfirm }: A + + diff --git a/frontend-v2/src/modules/rvw/components/BatchToolbar.tsx b/frontend-v2/src/modules/rvw/components/BatchToolbar.tsx index f78e508b..98a91d0e 100644 --- a/frontend-v2/src/modules/rvw/components/BatchToolbar.tsx +++ b/frontend-v2/src/modules/rvw/components/BatchToolbar.tsx @@ -58,6 +58,8 @@ export default function BatchToolbar({ selectedCount, onRunBatch, onClearSelecti + + diff --git a/frontend-v2/src/modules/rvw/components/FilterChips.tsx b/frontend-v2/src/modules/rvw/components/FilterChips.tsx index 08802069..f026733e 100644 --- a/frontend-v2/src/modules/rvw/components/FilterChips.tsx +++ b/frontend-v2/src/modules/rvw/components/FilterChips.tsx @@ -81,6 +81,8 @@ export default function FilterChips({ filters, counts, onFilterChange }: FilterC + + diff --git a/frontend-v2/src/modules/rvw/components/Header.tsx b/frontend-v2/src/modules/rvw/components/Header.tsx index 73f6e071..36607a81 100644 --- a/frontend-v2/src/modules/rvw/components/Header.tsx +++ b/frontend-v2/src/modules/rvw/components/Header.tsx @@ -71,6 +71,8 @@ export default function Header({ onUpload }: HeaderProps) { + + diff --git a/frontend-v2/src/modules/rvw/components/ReportDetail.tsx b/frontend-v2/src/modules/rvw/components/ReportDetail.tsx index 4eacb5a8..c8435e3c 100644 --- a/frontend-v2/src/modules/rvw/components/ReportDetail.tsx +++ b/frontend-v2/src/modules/rvw/components/ReportDetail.tsx @@ -125,6 +125,8 @@ export default function ReportDetail({ report, onBack }: ReportDetailProps) { + + diff --git a/frontend-v2/src/modules/rvw/components/ScoreRing.tsx b/frontend-v2/src/modules/rvw/components/ScoreRing.tsx index b8da1dc9..0cfafaa0 100644 --- a/frontend-v2/src/modules/rvw/components/ScoreRing.tsx +++ b/frontend-v2/src/modules/rvw/components/ScoreRing.tsx @@ -53,6 +53,8 @@ export default function ScoreRing({ score, size = 'medium', showLabel = true }: + + diff --git a/frontend-v2/src/modules/rvw/components/Sidebar.tsx b/frontend-v2/src/modules/rvw/components/Sidebar.tsx index 40928e75..cb678cb3 100644 --- a/frontend-v2/src/modules/rvw/components/Sidebar.tsx +++ b/frontend-v2/src/modules/rvw/components/Sidebar.tsx @@ -88,6 +88,8 @@ export default function Sidebar({ currentView, onViewChange, onSettingsClick }: + + diff --git a/frontend-v2/src/modules/rvw/components/index.ts b/frontend-v2/src/modules/rvw/components/index.ts index 76ca5c6f..f7d82905 100644 --- a/frontend-v2/src/modules/rvw/components/index.ts +++ b/frontend-v2/src/modules/rvw/components/index.ts @@ -30,6 +30,8 @@ export { default as TaskDetail } from './TaskDetail'; + + diff --git a/frontend-v2/src/modules/rvw/pages/Dashboard.tsx b/frontend-v2/src/modules/rvw/pages/Dashboard.tsx index 6257572e..3a200491 100644 --- a/frontend-v2/src/modules/rvw/pages/Dashboard.tsx +++ b/frontend-v2/src/modules/rvw/pages/Dashboard.tsx @@ -299,6 +299,8 @@ export default function Dashboard() { + + diff --git a/frontend-v2/src/modules/rvw/styles/index.css b/frontend-v2/src/modules/rvw/styles/index.css index 92a58039..f4c13dc5 100644 --- a/frontend-v2/src/modules/rvw/styles/index.css +++ b/frontend-v2/src/modules/rvw/styles/index.css @@ -248,6 +248,8 @@ + + diff --git a/frontend-v2/src/pages/admin/tenants/TenantListPage.tsx b/frontend-v2/src/pages/admin/tenants/TenantListPage.tsx index b5f280ae..9cc03189 100644 --- a/frontend-v2/src/pages/admin/tenants/TenantListPage.tsx +++ b/frontend-v2/src/pages/admin/tenants/TenantListPage.tsx @@ -352,3 +352,5 @@ export default TenantListPage; + + diff --git a/frontend-v2/src/pages/admin/tenants/api/tenantApi.ts b/frontend-v2/src/pages/admin/tenants/api/tenantApi.ts index 1f10fb88..3913a444 100644 --- a/frontend-v2/src/pages/admin/tenants/api/tenantApi.ts +++ b/frontend-v2/src/pages/admin/tenants/api/tenantApi.ts @@ -261,3 +261,5 @@ export async function fetchModuleList(): Promise { + + diff --git a/frontend-v2/src/shared/components/Chat/AIStreamChat.tsx b/frontend-v2/src/shared/components/Chat/AIStreamChat.tsx index 4a3c8201..e7d7cb08 100644 --- a/frontend-v2/src/shared/components/Chat/AIStreamChat.tsx +++ b/frontend-v2/src/shared/components/Chat/AIStreamChat.tsx @@ -480,3 +480,5 @@ export default AIStreamChat; + + diff --git a/frontend-v2/src/shared/components/Chat/ConversationList.tsx b/frontend-v2/src/shared/components/Chat/ConversationList.tsx index 02a9d13b..e94dcace 100644 --- a/frontend-v2/src/shared/components/Chat/ConversationList.tsx +++ b/frontend-v2/src/shared/components/Chat/ConversationList.tsx @@ -180,3 +180,5 @@ export default ConversationList; + + diff --git a/frontend-v2/src/shared/components/Chat/hooks/index.ts b/frontend-v2/src/shared/components/Chat/hooks/index.ts index 575a0ab6..247fdc7c 100644 --- a/frontend-v2/src/shared/components/Chat/hooks/index.ts +++ b/frontend-v2/src/shared/components/Chat/hooks/index.ts @@ -32,3 +32,5 @@ export type { + + diff --git a/frontend-v2/src/shared/components/Chat/hooks/useAIStream.ts b/frontend-v2/src/shared/components/Chat/hooks/useAIStream.ts index 6be2d64d..9e5a98e5 100644 --- a/frontend-v2/src/shared/components/Chat/hooks/useAIStream.ts +++ b/frontend-v2/src/shared/components/Chat/hooks/useAIStream.ts @@ -324,3 +324,5 @@ export default useAIStream; + + diff --git a/frontend-v2/src/shared/components/Chat/hooks/useConversations.ts b/frontend-v2/src/shared/components/Chat/hooks/useConversations.ts index f0536a46..1ddf595a 100644 --- a/frontend-v2/src/shared/components/Chat/hooks/useConversations.ts +++ b/frontend-v2/src/shared/components/Chat/hooks/useConversations.ts @@ -253,3 +253,5 @@ export default useConversations; + + diff --git a/frontend-v2/src/shared/components/Chat/styles/ai-stream-chat.css b/frontend-v2/src/shared/components/Chat/styles/ai-stream-chat.css index 57f7688b..7c666f39 100644 --- a/frontend-v2/src/shared/components/Chat/styles/ai-stream-chat.css +++ b/frontend-v2/src/shared/components/Chat/styles/ai-stream-chat.css @@ -288,3 +288,5 @@ + + diff --git a/frontend-v2/src/shared/components/Chat/styles/conversation-list.css b/frontend-v2/src/shared/components/Chat/styles/conversation-list.css index 72c41f97..47e2af51 100644 --- a/frontend-v2/src/shared/components/Chat/styles/conversation-list.css +++ b/frontend-v2/src/shared/components/Chat/styles/conversation-list.css @@ -224,3 +224,5 @@ + + diff --git a/frontend-v2/src/shared/components/Chat/styles/thinking.css b/frontend-v2/src/shared/components/Chat/styles/thinking.css index f51402cf..879472b1 100644 --- a/frontend-v2/src/shared/components/Chat/styles/thinking.css +++ b/frontend-v2/src/shared/components/Chat/styles/thinking.css @@ -161,3 +161,5 @@ + + diff --git a/frontend-v2/src/shared/components/index.ts b/frontend-v2/src/shared/components/index.ts index 275b1cc3..48f81180 100644 --- a/frontend-v2/src/shared/components/index.ts +++ b/frontend-v2/src/shared/components/index.ts @@ -73,6 +73,8 @@ export { default as Placeholder } from './Placeholder'; + + diff --git a/frontend-v2/src/vite-env.d.ts b/frontend-v2/src/vite-env.d.ts index 984bb2b5..3ad6e236 100644 --- a/frontend-v2/src/vite-env.d.ts +++ b/frontend-v2/src/vite-env.d.ts @@ -53,6 +53,8 @@ interface ImportMeta { + + diff --git a/frontend/src/pages/rvw/components/BatchToolbar.tsx b/frontend/src/pages/rvw/components/BatchToolbar.tsx index 98a91d0e..e8c8a644 100644 --- a/frontend/src/pages/rvw/components/BatchToolbar.tsx +++ b/frontend/src/pages/rvw/components/BatchToolbar.tsx @@ -60,6 +60,8 @@ export default function BatchToolbar({ selectedCount, onRunBatch, onClearSelecti + + diff --git a/frontend/src/pages/rvw/components/EditorialReport.tsx b/frontend/src/pages/rvw/components/EditorialReport.tsx index 5040689b..21320725 100644 --- a/frontend/src/pages/rvw/components/EditorialReport.tsx +++ b/frontend/src/pages/rvw/components/EditorialReport.tsx @@ -125,6 +125,8 @@ export default function EditorialReport({ data }: EditorialReportProps) { + + diff --git a/frontend/src/pages/rvw/components/FilterChips.tsx b/frontend/src/pages/rvw/components/FilterChips.tsx index f026733e..2728c969 100644 --- a/frontend/src/pages/rvw/components/FilterChips.tsx +++ b/frontend/src/pages/rvw/components/FilterChips.tsx @@ -83,6 +83,8 @@ export default function FilterChips({ filters, counts, onFilterChange }: FilterC + + diff --git a/frontend/src/pages/rvw/components/Header.tsx b/frontend/src/pages/rvw/components/Header.tsx index 36607a81..671e0e0a 100644 --- a/frontend/src/pages/rvw/components/Header.tsx +++ b/frontend/src/pages/rvw/components/Header.tsx @@ -73,6 +73,8 @@ export default function Header({ onUpload }: HeaderProps) { + + diff --git a/frontend/src/pages/rvw/components/ReportDetail.tsx b/frontend/src/pages/rvw/components/ReportDetail.tsx index c8435e3c..dc26f506 100644 --- a/frontend/src/pages/rvw/components/ReportDetail.tsx +++ b/frontend/src/pages/rvw/components/ReportDetail.tsx @@ -127,6 +127,8 @@ export default function ReportDetail({ report, onBack }: ReportDetailProps) { + + diff --git a/frontend/src/pages/rvw/components/ScoreRing.tsx b/frontend/src/pages/rvw/components/ScoreRing.tsx index 0cfafaa0..e9157b5d 100644 --- a/frontend/src/pages/rvw/components/ScoreRing.tsx +++ b/frontend/src/pages/rvw/components/ScoreRing.tsx @@ -55,6 +55,8 @@ export default function ScoreRing({ score, size = 'medium', showLabel = true }: + + diff --git a/frontend/src/pages/rvw/components/Sidebar.tsx b/frontend/src/pages/rvw/components/Sidebar.tsx index cb678cb3..99284f55 100644 --- a/frontend/src/pages/rvw/components/Sidebar.tsx +++ b/frontend/src/pages/rvw/components/Sidebar.tsx @@ -90,6 +90,8 @@ export default function Sidebar({ currentView, onViewChange, onSettingsClick }: + + diff --git a/frontend/src/pages/rvw/index.ts b/frontend/src/pages/rvw/index.ts index 46445093..9f76f8fc 100644 --- a/frontend/src/pages/rvw/index.ts +++ b/frontend/src/pages/rvw/index.ts @@ -24,6 +24,8 @@ export * from './api'; + + diff --git a/frontend/src/pages/rvw/styles.css b/frontend/src/pages/rvw/styles.css index f4c13dc5..201e300b 100644 --- a/frontend/src/pages/rvw/styles.css +++ b/frontend/src/pages/rvw/styles.css @@ -250,6 +250,8 @@ + + diff --git a/git-cleanup-redcap.ps1 b/git-cleanup-redcap.ps1 index c817a9a2..2589b58b 100644 --- a/git-cleanup-redcap.ps1 +++ b/git-cleanup-redcap.ps1 @@ -46,6 +46,8 @@ Write-Host "Next step: Run the commit command" -ForegroundColor Cyan + + diff --git a/git-commit-day1.ps1 b/git-commit-day1.ps1 index 4d111d06..29b6e9f6 100644 --- a/git-commit-day1.ps1 +++ b/git-commit-day1.ps1 @@ -102,6 +102,8 @@ Write-Host "Git commit and push completed!" -ForegroundColor Green + + diff --git a/git-fix-lock.ps1 b/git-fix-lock.ps1 index e90b2d95..a4f4cbd6 100644 --- a/git-fix-lock.ps1 +++ b/git-fix-lock.ps1 @@ -50,6 +50,8 @@ Write-Host "Now you can run git commands again." -ForegroundColor Cyan + + diff --git a/python-microservice/operations/__init__.py b/python-microservice/operations/__init__.py index e3b70fe7..ef847321 100644 --- a/python-microservice/operations/__init__.py +++ b/python-microservice/operations/__init__.py @@ -75,6 +75,8 @@ __version__ = '1.0.0' + + diff --git a/python-microservice/operations/binning.py b/python-microservice/operations/binning.py index c405019a..a03e1ce2 100644 --- a/python-microservice/operations/binning.py +++ b/python-microservice/operations/binning.py @@ -182,6 +182,8 @@ def apply_binning( + + diff --git a/python-microservice/operations/filter.py b/python-microservice/operations/filter.py index 65b951c0..b437e2b8 100644 --- a/python-microservice/operations/filter.py +++ b/python-microservice/operations/filter.py @@ -168,6 +168,8 @@ def apply_filter( + + diff --git a/python-microservice/operations/recode.py b/python-microservice/operations/recode.py index 7aaad178..b4bb9fcc 100644 --- a/python-microservice/operations/recode.py +++ b/python-microservice/operations/recode.py @@ -138,6 +138,8 @@ def apply_recode( + + diff --git a/recover_dc_code.py b/recover_dc_code.py index bd03be32..ba978f51 100644 --- a/recover_dc_code.py +++ b/recover_dc_code.py @@ -282,6 +282,8 @@ if __name__ == "__main__": + + diff --git a/redcap-docker-dev/.gitattributes b/redcap-docker-dev/.gitattributes index 35c2dacf..55c4bd3f 100644 --- a/redcap-docker-dev/.gitattributes +++ b/redcap-docker-dev/.gitattributes @@ -62,6 +62,8 @@ + + diff --git a/redcap-docker-dev/.gitignore b/redcap-docker-dev/.gitignore index 7c0fed03..07ddd46b 100644 --- a/redcap-docker-dev/.gitignore +++ b/redcap-docker-dev/.gitignore @@ -93,6 +93,8 @@ Desktop.ini + + diff --git a/redcap-docker-dev/README.md b/redcap-docker-dev/README.md index a6add25b..22290893 100644 --- a/redcap-docker-dev/README.md +++ b/redcap-docker-dev/README.md @@ -394,6 +394,8 @@ docker-compose -f docker-compose.prod.yml up -d + + diff --git a/redcap-docker-dev/docker-compose.prod.yml b/redcap-docker-dev/docker-compose.prod.yml index 424f0e36..e9514771 100644 --- a/redcap-docker-dev/docker-compose.prod.yml +++ b/redcap-docker-dev/docker-compose.prod.yml @@ -155,6 +155,8 @@ volumes: + + diff --git a/redcap-docker-dev/docker-compose.yml b/redcap-docker-dev/docker-compose.yml index e98060d5..cdf30f5e 100644 --- a/redcap-docker-dev/docker-compose.yml +++ b/redcap-docker-dev/docker-compose.yml @@ -153,6 +153,8 @@ volumes: + + diff --git a/redcap-docker-dev/env.template b/redcap-docker-dev/env.template index 47d6c154..588605f8 100644 --- a/redcap-docker-dev/env.template +++ b/redcap-docker-dev/env.template @@ -89,6 +89,8 @@ PMA_UPLOAD_LIMIT=50M + + diff --git a/redcap-docker-dev/scripts/clean-redcap.ps1 b/redcap-docker-dev/scripts/clean-redcap.ps1 index 8c899eb7..de49db47 100644 --- a/redcap-docker-dev/scripts/clean-redcap.ps1 +++ b/redcap-docker-dev/scripts/clean-redcap.ps1 @@ -97,6 +97,8 @@ Write-Host "" + + diff --git a/redcap-docker-dev/scripts/create-redcap-password.php b/redcap-docker-dev/scripts/create-redcap-password.php index d8f464cc..f6ddce0d 100644 --- a/redcap-docker-dev/scripts/create-redcap-password.php +++ b/redcap-docker-dev/scripts/create-redcap-password.php @@ -75,6 +75,8 @@ try { + + diff --git a/redcap-docker-dev/scripts/logs-redcap.ps1 b/redcap-docker-dev/scripts/logs-redcap.ps1 index 6d206fb5..33fcf5f2 100644 --- a/redcap-docker-dev/scripts/logs-redcap.ps1 +++ b/redcap-docker-dev/scripts/logs-redcap.ps1 @@ -88,6 +88,8 @@ Write-Host "" + + diff --git a/redcap-docker-dev/scripts/reset-admin-password.php b/redcap-docker-dev/scripts/reset-admin-password.php index d0aaddea..7be0a246 100644 --- a/redcap-docker-dev/scripts/reset-admin-password.php +++ b/redcap-docker-dev/scripts/reset-admin-password.php @@ -51,6 +51,8 @@ if ($result) { + + diff --git a/redcap-docker-dev/scripts/start-redcap.ps1 b/redcap-docker-dev/scripts/start-redcap.ps1 index 528487e1..51067d2d 100644 --- a/redcap-docker-dev/scripts/start-redcap.ps1 +++ b/redcap-docker-dev/scripts/start-redcap.ps1 @@ -73,6 +73,8 @@ if ($LASTEXITCODE -eq 0) { + + diff --git a/redcap-docker-dev/scripts/stop-redcap.ps1 b/redcap-docker-dev/scripts/stop-redcap.ps1 index cdd30b92..4f5a11a7 100644 --- a/redcap-docker-dev/scripts/stop-redcap.ps1 +++ b/redcap-docker-dev/scripts/stop-redcap.ps1 @@ -59,6 +59,8 @@ if ($LASTEXITCODE -eq 0) { + + diff --git a/run_recovery.ps1 b/run_recovery.ps1 index 46066ef8..1547b5bd 100644 --- a/run_recovery.ps1 +++ b/run_recovery.ps1 @@ -106,6 +106,8 @@ Write-Host "==================================================================== + + diff --git a/tests/QUICKSTART_快速开始.md b/tests/QUICKSTART_快速开始.md index 049ecea1..d9aa506b 100644 --- a/tests/QUICKSTART_快速开始.md +++ b/tests/QUICKSTART_快速开始.md @@ -153,6 +153,8 @@ INFO: Uvicorn running on http://0.0.0.0:8001 + + diff --git a/tests/README_测试说明.md b/tests/README_测试说明.md index 6ed90668..a010c494 100644 --- a/tests/README_测试说明.md +++ b/tests/README_测试说明.md @@ -309,6 +309,8 @@ df_numeric.to_excel('test_data/numeric_test.xlsx', index=False) + + diff --git a/tests/run_tests.bat b/tests/run_tests.bat index ac11b586..ff50d7c5 100644 --- a/tests/run_tests.bat +++ b/tests/run_tests.bat @@ -104,6 +104,8 @@ pause + + diff --git a/tests/run_tests.sh b/tests/run_tests.sh index a2534cd6..66024d65 100644 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -100,6 +100,8 @@ echo "========================================" + + diff --git a/快速部署到SAE.md b/快速部署到SAE.md index a03c4cc5..53e211c0 100644 --- a/快速部署到SAE.md +++ b/快速部署到SAE.md @@ -365,6 +365,8 @@ OSS AccessKeySecret:_______________ + + diff --git a/部署检查清单.md b/部署检查清单.md index b99fd3aa..3c7c225c 100644 --- a/部署检查清单.md +++ b/部署检查清单.md @@ -401,6 +401,8 @@ OSS配置: + +