From 4088275290d1d46b634e5d7e772b8e60ea5dc914 Mon Sep 17 00:00:00 2001 From: HaHafeng Date: Tue, 13 Jan 2026 13:17:20 +0800 Subject: [PATCH] fix(pkb): fix create KB and upload issues - remove simulated upload, fix department mapping, add upload modal Fixed issues: - Remove simulateUpload function from DashboardPage Step 3 - Map department to description field when creating KB - Add upload modal in WorkspacePage knowledge assets tab - Fix DocumentUpload import path (../../stores to ../stores) Known issue: Dify API validation error during document upload (file uploaded but DB record failed, needs investigation) Testing: KB creation works, upload dialog opens correctly --- COMMIT_DAY1.txt | 3 + DC模块代码恢复指南.md | 3 + SAE_WECHAT_MP_DEPLOY_STEPS.md | 3 + backend/DEPLOY_TO_SAE_FOR_WECHAT_MP.md | 3 + backend/RESTART_SERVER_NOW.md | 3 + backend/WECHAT_MP_CONFIG_READY.md | 3 + backend/WECHAT_MP_QUICK_FIX.md | 3 + backend/check_db.ts | 3 + backend/check_db_data.ts | 3 + backend/check_iit.ts | 3 + backend/check_iit_asl_data.ts | 3 + backend/check_queue_table.ts | 3 + backend/check_rvw_issue.ts | 3 + backend/check_tables.ts | 3 + backend/compare_db.ts | 3 + backend/compare_dc_asl.ts | 3 + backend/compare_pkb_aia_rvw.ts | 3 + backend/compare_schema_db.ts | 3 + backend/create_mock_user.sql | 3 + backend/create_mock_user_platform.sql | 3 + .../add_data_stats_to_tool_c_session.sql | 3 + .../001_add_postgres_cache_and_checkpoint.sql | 3 + .../manual-migrations/run-migration-002.ts | 3 + .../20251208_add_column_mapping/migration.sql | 3 + .../migrations/create_tool_c_session.sql | 3 + backend/rebuild-and-push.ps1 | 3 + backend/recover-code-from-cursor-db.js | 3 + backend/restore_job_common.sql | 3 + backend/restore_pgboss_functions.sql | 3 + backend/scripts/check-dc-tables.mjs | 3 + backend/scripts/create-capability-schema.sql | 3 + .../create-tool-c-ai-history-table.mjs | 3 + backend/scripts/create-tool-c-table.js | 3 + backend/scripts/create-tool-c-table.mjs | 3 + backend/scripts/setup-prompt-system.ts | 3 + backend/scripts/test-pkb-apis-simple.ts | 3 + backend/scripts/test-prompt-api.ts | 3 + backend/scripts/verify-pkb-rvw-schema.ts | 3 + backend/src/common/auth/auth.controller.ts | 10 +- backend/src/common/auth/jwt.service.ts | 3 + backend/src/common/jobs/utils.ts | 3 + backend/src/common/prompt/prompt.fallbacks.ts | 1 + backend/src/common/prompt/prompt.types.ts | 1 + .../src/modules/admin/routes/tenantRoutes.ts | 1 + .../src/modules/admin/types/tenant.types.ts | 1 + .../asl/controllers/literatureController.ts | 19 +- .../asl/controllers/projectController.ts | 24 +- .../asl/controllers/screeningController.ts | 21 +- .../__tests__/api-integration-test.ts | 3 + .../__tests__/e2e-real-test-v2.ts | 3 + .../__tests__/fulltext-screening-api.http | 3 + .../FulltextScreeningController.ts | 21 +- .../services/ConflictDetectionService.ts | 1 + backend/src/modules/dc/tool-c/README.md | 1 + .../tool-c/controllers/StreamAIController.ts | 1 + .../iit-manager/agents/SessionMemory.ts | 3 + .../iit-manager/check-iit-table-structure.ts | 3 + .../iit-manager/check-project-config.ts | 3 + .../iit-manager/check-test-project-in-db.ts | 3 + .../iit-manager/docs/微信服务号接入指南.md | 3 + .../iit-manager/generate-wechat-tokens.ts | 3 + .../services/PatientWechatService.ts | 3 + .../iit-manager/test-chatservice-dify.ts | 3 + .../modules/iit-manager/test-iit-database.ts | 3 + .../iit-manager/test-patient-wechat-config.ts | 3 + .../test-patient-wechat-url-verify.ts | 3 + .../iit-manager/test-redcap-query-from-db.ts | 3 + .../iit-manager/test-wechat-mp-local.ps1 | 3 + .../src/modules/iit-manager/types/index.ts | 3 + .../pkb/controllers/batchController.ts | 16 +- .../pkb/controllers/documentController.ts | 30 +- .../controllers/knowledgeBaseController.ts | 38 +- backend/src/modules/pkb/routes/health.ts | 1 + .../pkb/services/knowledgeBaseService.ts | 9 +- backend/src/modules/rvw/__tests__/api.http | 3 + .../src/modules/rvw/__tests__/test-api.ps1 | 3 + .../rvw/controllers/reviewController.ts | 30 +- backend/src/modules/rvw/index.ts | 3 + .../modules/rvw/services/editorialService.ts | 33 +- .../rvw/services/methodologyService.ts | 33 +- backend/src/modules/rvw/services/utils.ts | 3 + .../src/modules/rvw/workers/reviewWorker.ts | 6 +- backend/src/tests/README.md | 3 + backend/src/tests/verify-test1-database.sql | 3 + backend/src/tests/verify-test1-database.ts | 3 + backend/src/types/global.d.ts | 3 + backend/sync-dc-database.ps1 | 3 + backend/temp_check.sql | 3 + backend/test-pkb-migration.http | 3 + backend/test-tool-c-advanced-scenarios.mjs | 3 + backend/test-tool-c-day2.mjs | 3 + backend/test-tool-c-day3.mjs | 3 + backend/verify_all_users.ts | 3 + backend/verify_functions.ts | 3 + backend/verify_job_common.ts | 3 + backend/verify_mock_user.ts | 3 + backend/verify_system.ts | 3 + backup_aia_migration_20260111_223208.sql | Bin 0 -> 491324 bytes deploy-to-sae.ps1 | 3 + .../Postgres-Only异步任务处理指南.md | 3 + docs/02-通用能力层/通用能力层技术债务清单.md | 3 + .../ADMIN-运营管理端/00-Phase3.5完成总结.md | 1 + docs/03-业务模块/ADMIN-运营管理端/README.md | 1 + .../ADMIN运营与INST机构管理端-文档体系建立完成.md | 3 + .../AIA-AI智能问答/01-需求分析/AIA模块PRD.md | 135 +++ .../AIA-AI智能问答/01-需求分析/AI智能问答V2.html | 665 +++++++++++++ .../01-需求分析/AI问答原型图V11.html | 514 ++++++++++ .../04-开发计划/01-AIA-V2.1开发计划.md | 540 +++++++++++ .../AIA-AI智能问答/04-开发计划/02-后端API设计.md | 568 +++++++++++ .../AIA-AI智能问答/04-开发计划/03-前端组件设计.md | 883 ++++++++++++++++++ .../04-开发计划/05-全文复筛前端开发计划.md | 3 + .../05-开发记录/2025-01-23_全文复筛前端开发完成.md | 3 + .../05-开发记录/2025-01-23_全文复筛前端逻辑调整.md | 3 + .../05-开发记录/2025-11-23_Day5_全文复筛API开发.md | 3 + .../04-开发计划/工具C_AI_Few-shot示例库.md | 3 + .../04-开发计划/工具C_Bug修复总结_2025-12-08.md | 3 + .../04-开发计划/工具C_Day3开发计划.md | 3 + .../04-开发计划/工具C_Day4-5前端开发计划.md | 3 + .../04-开发计划/工具C_Pivot列顺序优化总结.md | 3 + .../04-开发计划/工具C_方案B实施总结_2025-12-09.md | 3 + .../04-开发计划/工具C_缺失值处理_开发进度_2025-12-10.md | 3 + .../04-开发计划/工具C_缺失值处理功能_更新说明.md | 3 + .../06-开发记录/2025-12-02_工作总结.md | 3 + .../06-开发记录/2025-12-06_工具C_Day1开发完成总结.md | 3 + .../06-开发记录/2025-12-06_工具C_Day2开发完成总结.md | 3 + .../06-开发记录/2025-12-07_AI对话核心功能增强总结.md | 3 + .../2025-12-07_Bug修复_DataGrid空数据防御.md | 3 + .../06-开发记录/2025-12-07_Day5_Ant-Design-X重构完成.md | 3 + .../06-开发记录/2025-12-07_Day5最终总结.md | 3 + .../06-开发记录/2025-12-07_UI优化与Bug修复.md | 3 + .../06-开发记录/2025-12-07_后端API完整对接完成.md | 3 + .../06-开发记录/2025-12-07_完整UI优化与功能增强.md | 3 + .../06-开发记录/2025-12-07_工具C_Day4前端基础完成.md | 3 + .../06-开发记录/DC模块重建完成总结-Day1.md | 3 + .../06-开发记录/Phase1-Portal页面开发完成-2025-12-02.md | 3 + .../Phase2-ToolB-Step1-2开发完成-2025-12-03.md | 3 + .../06-开发记录/Portal页面UI优化-2025-12-02.md | 3 + .../06-开发记录/Tool-B-MVP完成总结-2025-12-03.md | 3 + .../06-开发记录/ToolB-UI优化-2025-12-03.md | 3 + .../06-开发记录/ToolB-UI优化-Round2-2025-12-03.md | 3 + .../06-开发记录/ToolB浏览器测试计划-2025-12-03.md | 3 + .../06-开发记录/后端API测试报告-2025-12-02.md | 3 + .../06-开发记录/待办事项-下一步工作.md | 3 + .../06-开发记录/数据库验证报告-2025-12-02.md | 3 + .../07-技术债务/Tool-B技术债务清单.md | 3 + .../IIT Manager Agent 技术路径与架构设计.md | 3 + .../04-开发计划/REDCap对接技术方案与实施指南.md | 3 + .../04-开发计划/企业微信注册指南.md | 3 + .../2026-01-04-Dify知识库集成开发记录.md | 3 + .../Day2-REDCap实时集成开发完成记录.md | 3 + .../Day3-企业微信集成与端到端测试完成记录.md | 3 + .../06-开发记录/Day3-企业微信集成开发完成记录.md | 3 + .../Phase1.5-AI对话集成REDCap完成记录.md | 3 + .../06-开发记录/V1.1更新完成报告.md | 3 + .../07-技术债务/IIT Manager Agent 技术债务清单.md | 3 + .../INST-机构管理端/00-模块当前状态与开发指南.md | 3 + docs/03-业务模块/INST-机构管理端/README.md | 3 + .../06-开发记录/2026-01-07-前端迁移与批处理功能完善.md | 3 + .../06-开发记录/2026-01-07_PKB模块前端V3设计实现.md | 3 + .../01-部署与配置/10-REDCap_Docker部署操作手册.md | 3 + docs/03-业务模块/Redcap/README.md | 3 + docs/04-开发规范/09-数据库开发规范.md | 3 + docs/04-开发规范/10-模块认证规范.md | 1 + .../02-SAE部署完全指南(产品经理版).md | 3 + .../07-前端Nginx-SAE部署操作手册.md | 3 + .../08-PostgreSQL数据库部署操作手册.md | 3 + .../10-Node.js后端-Docker镜像构建手册.md | 3 + .../11-Node.js后端-SAE部署配置清单.md | 3 + .../12-Node.js后端-SAE部署操作手册.md | 3 + .../13-Node.js后端-镜像修复记录.md | 3 + .../14-Node.js后端-pino-pretty问题修复.md | 3 + docs/05-部署文档/16-前端Nginx-部署成功总结.md | 3 + .../05-部署文档/17-完整部署实战手册-2025版.md | 3 + docs/05-部署文档/18-部署文档使用指南.md | 3 + docs/05-部署文档/19-日常更新快速操作手册.md | 3 + docs/05-部署文档/文档修正报告-20251214.md | 3 + docs/07-运维文档/03-SAE环境变量配置指南.md | 3 + .../05-Redis缓存与队列的区别说明.md | 3 + docs/07-运维文档/06-长时间任务可靠性分析.md | 3 + .../07-Redis使用需求分析(按模块).md | 3 + .../2025-12-13-Postgres-Only架构改造完成.md | 3 + .../05-技术债务/通用对话服务抽取计划.md | 3 + docs/08-项目管理/2026-01-11-数据库事故总结.md | 3 + docs/08-项目管理/PKB前端问题修复报告.md | 3 + docs/08-项目管理/PKB前端验证指南.md | 3 + docs/08-项目管理/PKB功能审查报告-阶段0.md | 3 + docs/08-项目管理/PKB和RVW功能迁移计划.md | 3 + docs/08-项目管理/PKB精细化优化报告.md | 3 + docs/08-项目管理/PKB迁移-超级安全执行计划.md | 3 + docs/08-项目管理/PKB迁移-阶段1完成报告.md | 3 + docs/08-项目管理/PKB迁移-阶段2完成报告.md | 3 + docs/08-项目管理/PKB迁移-阶段2进行中.md | 3 + docs/08-项目管理/PKB迁移-阶段3完成报告.md | 3 + docs/08-项目管理/PKB迁移-阶段4完成报告.md | 3 + extraction_service/.dockerignore | 3 + extraction_service/operations/__init__.py | 3 + extraction_service/operations/dropna.py | 3 + extraction_service/operations/filter.py | 3 + extraction_service/operations/unpivot.py | 3 + extraction_service/test_dc_api.py | 3 + extraction_service/test_execute_simple.py | 3 + extraction_service/test_module.py | 3 + frontend-v2/.dockerignore | 3 + frontend-v2/docker-entrypoint.sh | 3 + frontend-v2/nginx.conf | 3 + frontend-v2/src/common/api/axios.ts | 1 + .../src/framework/auth/AuthContext.tsx | 3 + frontend-v2/src/framework/auth/api.ts | 3 + frontend-v2/src/framework/auth/index.ts | 3 + frontend-v2/src/framework/auth/moduleApi.ts | 1 + frontend-v2/src/framework/auth/types.ts | 3 + frontend-v2/src/modules/asl/api/index.ts | 23 +- .../asl/components/FulltextDetailDrawer.tsx | 3 + frontend-v2/src/modules/dc/api/toolB.ts | 46 +- frontend-v2/src/modules/dc/api/toolC.ts | 24 +- frontend-v2/src/modules/dc/hooks/useAssets.ts | 3 + .../src/modules/dc/hooks/useRecentTasks.ts | 3 + .../pages/tool-c/components/DropnaDialog.tsx | 3 + .../tool-c/components/MetricTimePanel.tsx | 3 + .../dc/pages/tool-c/components/PivotPanel.tsx | 3 + .../dc/pages/tool-c/hooks/useSessionStatus.ts | 3 + .../modules/dc/pages/tool-c/types/index.ts | 3 + frontend-v2/src/modules/dc/types/portal.ts | 3 + .../src/modules/pkb/api/knowledgeBaseApi.ts | 14 + .../src/modules/pkb/pages/DashboardPage.tsx | 43 +- .../src/modules/pkb/pages/KnowledgePage.tsx | 3 + .../pkb/stores/useKnowledgeBaseStore.ts | 3 + .../src/modules/pkb/types/workspace.ts | 3 + frontend-v2/src/modules/rvw/api/index.ts | 17 +- .../src/modules/rvw/components/AgentModal.tsx | 3 + .../modules/rvw/components/BatchToolbar.tsx | 3 + .../modules/rvw/components/FilterChips.tsx | 3 + .../src/modules/rvw/components/Header.tsx | 3 + .../modules/rvw/components/ReportDetail.tsx | 3 + .../src/modules/rvw/components/ScoreRing.tsx | 3 + .../src/modules/rvw/components/Sidebar.tsx | 3 + .../src/modules/rvw/components/index.ts | 3 + .../src/modules/rvw/pages/Dashboard.tsx | 3 + frontend-v2/src/modules/rvw/styles/index.css | 3 + frontend-v2/src/pages/LoginPage.tsx | 3 + .../pages/admin/tenants/TenantListPage.tsx | 1 + .../src/pages/admin/tenants/api/tenantApi.ts | 1 + frontend-v2/src/shared/components/index.ts | 3 + frontend-v2/src/vite-env.d.ts | 3 + .../src/pages/rvw/components/BatchToolbar.tsx | 3 + .../pages/rvw/components/EditorialReport.tsx | 3 + .../src/pages/rvw/components/FilterChips.tsx | 3 + frontend/src/pages/rvw/components/Header.tsx | 3 + .../src/pages/rvw/components/ReportDetail.tsx | 3 + .../src/pages/rvw/components/ScoreRing.tsx | 3 + frontend/src/pages/rvw/components/Sidebar.tsx | 3 + frontend/src/pages/rvw/index.ts | 3 + frontend/src/pages/rvw/styles.css | 3 + git-cleanup-redcap.ps1 | 3 + git-commit-day1.ps1 | 3 + git-fix-lock.ps1 | 3 + python-microservice/operations/__init__.py | 3 + python-microservice/operations/binning.py | 3 + python-microservice/operations/filter.py | 3 + python-microservice/operations/recode.py | 3 + recover_dc_code.py | 3 + redcap-docker-dev/.gitattributes | 3 + redcap-docker-dev/.gitignore | 3 + redcap-docker-dev/README.md | 3 + redcap-docker-dev/docker-compose.prod.yml | 3 + redcap-docker-dev/docker-compose.yml | 3 + redcap-docker-dev/env.template | 3 + redcap-docker-dev/scripts/clean-redcap.ps1 | 3 + .../scripts/create-redcap-password.php | 3 + redcap-docker-dev/scripts/logs-redcap.ps1 | 3 + .../scripts/reset-admin-password.php | 3 + redcap-docker-dev/scripts/start-redcap.ps1 | 3 + redcap-docker-dev/scripts/stop-redcap.ps1 | 3 + run_recovery.ps1 | 3 + tests/QUICKSTART_快速开始.md | 3 + tests/README_测试说明.md | 3 + tests/run_tests.bat | 3 + tests/run_tests.sh | 3 + 快速部署到SAE.md | 3 + 部署检查清单.md | 3 + 280 files changed, 4344 insertions(+), 150 deletions(-) create mode 100644 backup_aia_migration_20260111_223208.sql create mode 100644 docs/03-业务模块/AIA-AI智能问答/01-需求分析/AIA模块PRD.md create mode 100644 docs/03-业务模块/AIA-AI智能问答/01-需求分析/AI智能问答V2.html create mode 100644 docs/03-业务模块/AIA-AI智能问答/01-需求分析/AI问答原型图V11.html create mode 100644 docs/03-业务模块/AIA-AI智能问答/04-开发计划/01-AIA-V2.1开发计划.md create mode 100644 docs/03-业务模块/AIA-AI智能问答/04-开发计划/02-后端API设计.md create mode 100644 docs/03-业务模块/AIA-AI智能问答/04-开发计划/03-前端组件设计.md diff --git a/COMMIT_DAY1.txt b/COMMIT_DAY1.txt index 6e866ebd..ad7094ab 100644 --- a/COMMIT_DAY1.txt +++ b/COMMIT_DAY1.txt @@ -42,3 +42,6 @@ Status: Day 1 complete (11/11 tasks), ready for Day 2 + + + diff --git a/DC模块代码恢复指南.md b/DC模块代码恢复指南.md index 6ede6c36..850602bb 100644 --- a/DC模块代码恢复指南.md +++ b/DC模块代码恢复指南.md @@ -268,6 +268,9 @@ + + + diff --git a/SAE_WECHAT_MP_DEPLOY_STEPS.md b/SAE_WECHAT_MP_DEPLOY_STEPS.md index 575b961f..146c1373 100644 --- a/SAE_WECHAT_MP_DEPLOY_STEPS.md +++ b/SAE_WECHAT_MP_DEPLOY_STEPS.md @@ -218,3 +218,6 @@ 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 baac104e..24ac1790 100644 --- a/backend/DEPLOY_TO_SAE_FOR_WECHAT_MP.md +++ b/backend/DEPLOY_TO_SAE_FOR_WECHAT_MP.md @@ -147,3 +147,6 @@ https://iit.xunzhengyixue.com/api/v1/iit/health + + + diff --git a/backend/RESTART_SERVER_NOW.md b/backend/RESTART_SERVER_NOW.md index d2b18832..2f666841 100644 --- a/backend/RESTART_SERVER_NOW.md +++ b/backend/RESTART_SERVER_NOW.md @@ -48,3 +48,6 @@ + + + diff --git a/backend/WECHAT_MP_CONFIG_READY.md b/backend/WECHAT_MP_CONFIG_READY.md index 54d31b3e..11eed4dc 100644 --- a/backend/WECHAT_MP_CONFIG_READY.md +++ b/backend/WECHAT_MP_CONFIG_READY.md @@ -308,3 +308,6 @@ 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 09e5d637..80e4531f 100644 --- a/backend/WECHAT_MP_QUICK_FIX.md +++ b/backend/WECHAT_MP_QUICK_FIX.md @@ -170,3 +170,6 @@ npm run dev + + + diff --git a/backend/check_db.ts b/backend/check_db.ts index eb6d6955..a00702d6 100644 --- a/backend/check_db.ts +++ b/backend/check_db.ts @@ -47,3 +47,6 @@ main() .catch(console.error) .finally(() => prisma.$disconnect()); + + + diff --git a/backend/check_db_data.ts b/backend/check_db_data.ts index 89f8c3cf..5a2b125c 100644 --- a/backend/check_db_data.ts +++ b/backend/check_db_data.ts @@ -41,3 +41,6 @@ main() .catch(console.error) .finally(() => prisma.$disconnect()); + + + diff --git a/backend/check_iit.ts b/backend/check_iit.ts index 461a0b68..2748a44e 100644 --- a/backend/check_iit.ts +++ b/backend/check_iit.ts @@ -36,3 +36,6 @@ main() .catch(console.error) .finally(() => prisma.$disconnect()); + + + diff --git a/backend/check_iit_asl_data.ts b/backend/check_iit_asl_data.ts index d09b59ef..195a480b 100644 --- a/backend/check_iit_asl_data.ts +++ b/backend/check_iit_asl_data.ts @@ -68,3 +68,6 @@ main() .catch(console.error) .finally(() => prisma.$disconnect()); + + + diff --git a/backend/check_queue_table.ts b/backend/check_queue_table.ts index 22f7f99b..e1d3364d 100644 --- a/backend/check_queue_table.ts +++ b/backend/check_queue_table.ts @@ -31,3 +31,6 @@ main() .catch(console.error) .finally(() => prisma.$disconnect()); + + + diff --git a/backend/check_rvw_issue.ts b/backend/check_rvw_issue.ts index 379b3ad8..2318019f 100644 --- a/backend/check_rvw_issue.ts +++ b/backend/check_rvw_issue.ts @@ -72,3 +72,6 @@ main() .catch(console.error) .finally(() => prisma.$disconnect()); + + + diff --git a/backend/check_tables.ts b/backend/check_tables.ts index 915f0be1..26989c38 100644 --- a/backend/check_tables.ts +++ b/backend/check_tables.ts @@ -19,3 +19,6 @@ main() .catch(console.error) .finally(() => prisma.$disconnect()); + + + diff --git a/backend/compare_db.ts b/backend/compare_db.ts index aacfe763..3cd1eea3 100644 --- a/backend/compare_db.ts +++ b/backend/compare_db.ts @@ -107,3 +107,6 @@ main() .catch(console.error) .finally(() => prisma.$disconnect()); + + + diff --git a/backend/compare_dc_asl.ts b/backend/compare_dc_asl.ts index 1160396b..404eb0dc 100644 --- a/backend/compare_dc_asl.ts +++ b/backend/compare_dc_asl.ts @@ -78,3 +78,6 @@ main() .catch(console.error) .finally(() => prisma.$disconnect()); + + + diff --git a/backend/compare_pkb_aia_rvw.ts b/backend/compare_pkb_aia_rvw.ts index 9747075b..ac637688 100644 --- a/backend/compare_pkb_aia_rvw.ts +++ b/backend/compare_pkb_aia_rvw.ts @@ -64,3 +64,6 @@ main() .catch(console.error) .finally(() => prisma.$disconnect()); + + + diff --git a/backend/compare_schema_db.ts b/backend/compare_schema_db.ts index 29b32e1a..b8e16b54 100644 --- a/backend/compare_schema_db.ts +++ b/backend/compare_schema_db.ts @@ -106,3 +106,6 @@ main() .catch(console.error) .finally(() => prisma.$disconnect()); + + + diff --git a/backend/create_mock_user.sql b/backend/create_mock_user.sql index 57ec0946..08e92690 100644 --- a/backend/create_mock_user.sql +++ b/backend/create_mock_user.sql @@ -17,3 +17,6 @@ VALUES ( ) ON CONFLICT (id) DO NOTHING; + + + diff --git a/backend/create_mock_user_platform.sql b/backend/create_mock_user_platform.sql index 66f112b5..1ef10257 100644 --- a/backend/create_mock_user_platform.sql +++ b/backend/create_mock_user_platform.sql @@ -49,3 +49,6 @@ VALUES ( ) ON CONFLICT (id) DO NOTHING; + + + 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 087c9baf..9ea35df3 100644 --- a/backend/migrations/add_data_stats_to_tool_c_session.sql +++ b/backend/migrations/add_data_stats_to_tool_c_session.sql @@ -63,6 +63,9 @@ 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 a3c65c4e..720b1024 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 @@ -101,6 +101,9 @@ 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 55eaf262..ecb5f34a 100644 --- a/backend/prisma/manual-migrations/run-migration-002.ts +++ b/backend/prisma/manual-migrations/run-migration-002.ts @@ -114,6 +114,9 @@ runMigration() + + + diff --git a/backend/prisma/migrations/20251208_add_column_mapping/migration.sql b/backend/prisma/migrations/20251208_add_column_mapping/migration.sql index b26dac9e..972ae4ea 100644 --- a/backend/prisma/migrations/20251208_add_column_mapping/migration.sql +++ b/backend/prisma/migrations/20251208_add_column_mapping/migration.sql @@ -48,6 +48,9 @@ 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 39276fd0..fd6b5004 100644 --- a/backend/prisma/migrations/create_tool_c_session.sql +++ b/backend/prisma/migrations/create_tool_c_session.sql @@ -75,6 +75,9 @@ COMMENT ON COLUMN dc_schema.dc_tool_c_sessions.expires_at IS '过期时间(创 + + + diff --git a/backend/rebuild-and-push.ps1 b/backend/rebuild-and-push.ps1 index c5d79987..2f50c2da 100644 --- a/backend/rebuild-and-push.ps1 +++ b/backend/rebuild-and-push.ps1 @@ -115,6 +115,9 @@ Write-Host "" + + + diff --git a/backend/recover-code-from-cursor-db.js b/backend/recover-code-from-cursor-db.js index e62abeec..3b6f9d63 100644 --- a/backend/recover-code-from-cursor-db.js +++ b/backend/recover-code-from-cursor-db.js @@ -225,6 +225,9 @@ function extractCodeBlocks(obj, blocks = []) { + + + diff --git a/backend/restore_job_common.sql b/backend/restore_job_common.sql index 9adfda8d..13cf99c5 100644 --- a/backend/restore_job_common.sql +++ b/backend/restore_job_common.sql @@ -26,3 +26,6 @@ CREATE TABLE IF NOT EXISTS platform_schema.job_common ( policy text ); + + + diff --git a/backend/restore_pgboss_functions.sql b/backend/restore_pgboss_functions.sql index 2ef31156..b5bc40e2 100644 --- a/backend/restore_pgboss_functions.sql +++ b/backend/restore_pgboss_functions.sql @@ -100,3 +100,6 @@ CREATE OR REPLACE FUNCTION platform_schema.delete_queue(queue_name text) RETURNS END; $$; + + + diff --git a/backend/scripts/check-dc-tables.mjs b/backend/scripts/check-dc-tables.mjs index f5bcdb04..ce0d97ec 100644 --- a/backend/scripts/check-dc-tables.mjs +++ b/backend/scripts/check-dc-tables.mjs @@ -244,6 +244,9 @@ checkDCTables(); + + + diff --git a/backend/scripts/create-capability-schema.sql b/backend/scripts/create-capability-schema.sql index e44bc9e1..7379934b 100644 --- a/backend/scripts/create-capability-schema.sql +++ b/backend/scripts/create-capability-schema.sql @@ -1,3 +1,6 @@ -- Create capability_schema for Prompt Management System 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 5f788746..6459300f 100644 --- a/backend/scripts/create-tool-c-ai-history-table.mjs +++ b/backend/scripts/create-tool-c-ai-history-table.mjs @@ -196,6 +196,9 @@ createAiHistoryTable() + + + diff --git a/backend/scripts/create-tool-c-table.js b/backend/scripts/create-tool-c-table.js index c1401538..31b157a8 100644 --- a/backend/scripts/create-tool-c-table.js +++ b/backend/scripts/create-tool-c-table.js @@ -183,6 +183,9 @@ createToolCTable() + + + diff --git a/backend/scripts/create-tool-c-table.mjs b/backend/scripts/create-tool-c-table.mjs index 9ab92643..430111ee 100644 --- a/backend/scripts/create-tool-c-table.mjs +++ b/backend/scripts/create-tool-c-table.mjs @@ -180,6 +180,9 @@ createToolCTable() + + + diff --git a/backend/scripts/setup-prompt-system.ts b/backend/scripts/setup-prompt-system.ts index c6c1ccbf..4995f205 100644 --- a/backend/scripts/setup-prompt-system.ts +++ b/backend/scripts/setup-prompt-system.ts @@ -111,3 +111,6 @@ main() .catch(console.error) .finally(() => prisma.$disconnect()); + + + diff --git a/backend/scripts/test-pkb-apis-simple.ts b/backend/scripts/test-pkb-apis-simple.ts index d4a4b818..ecee54f9 100644 --- a/backend/scripts/test-pkb-apis-simple.ts +++ b/backend/scripts/test-pkb-apis-simple.ts @@ -331,3 +331,6 @@ runTests().catch(error => { + + + diff --git a/backend/scripts/test-prompt-api.ts b/backend/scripts/test-prompt-api.ts index 970176a2..b2a46ac0 100644 --- a/backend/scripts/test-prompt-api.ts +++ b/backend/scripts/test-prompt-api.ts @@ -77,3 +77,6 @@ async function testAPI() { testAPI().catch(console.error); + + + diff --git a/backend/scripts/verify-pkb-rvw-schema.ts b/backend/scripts/verify-pkb-rvw-schema.ts index 3d0e08b3..7e8635b3 100644 --- a/backend/scripts/verify-pkb-rvw-schema.ts +++ b/backend/scripts/verify-pkb-rvw-schema.ts @@ -296,3 +296,6 @@ verifySchemas() + + + diff --git a/backend/src/common/auth/auth.controller.ts b/backend/src/common/auth/auth.controller.ts index 2165390f..93a81250 100644 --- a/backend/src/common/auth/auth.controller.ts +++ b/backend/src/common/auth/auth.controller.ts @@ -235,7 +235,7 @@ export async function logout( /** - * ȡǰûɷʵģ + * ��ȡ��ǰ�û��ɷ��ʵ�ģ�� * * GET /api/v1/auth/me/modules */ @@ -248,11 +248,11 @@ export async function getUserModules( return reply.status(401).send({ success: false, error: 'Unauthorized', - message: 'δ֤', + message: 'δ��֤', }); } - // SUPER_ADMIN PROMPT_ENGINEER Էģ + // SUPER_ADMIN �� PROMPT_ENGINEER ���Է�������ģ�� if (request.user.role === 'SUPER_ADMIN' || request.user.role === 'PROMPT_ENGINEER') { const { moduleService } = await import('./module.service.js'); const allModules = await moduleService.getAllModules(); @@ -270,8 +270,8 @@ export async function getUserModules( data: result.modules, }); } catch (error) { - const message = error instanceof Error ? error.message : 'ȡûģʧ'; - logger.error('ȡûģʧ', { error: message, userId: request.user?.userId }); + const message = error instanceof Error ? error.message : '��ȡ�û�ģ��ʧ��'; + logger.error('��ȡ�û�ģ��ʧ��', { error: message, userId: request.user?.userId }); return reply.status(500).send({ success: false, diff --git a/backend/src/common/auth/jwt.service.ts b/backend/src/common/auth/jwt.service.ts index 9ee3df3e..6b9a3462 100644 --- a/backend/src/common/auth/jwt.service.ts +++ b/backend/src/common/auth/jwt.service.ts @@ -184,3 +184,6 @@ export class JWTService { // 导出单例 export const jwtService = new JWTService(); + + + diff --git a/backend/src/common/jobs/utils.ts b/backend/src/common/jobs/utils.ts index edcaf424..d292e392 100644 --- a/backend/src/common/jobs/utils.ts +++ b/backend/src/common/jobs/utils.ts @@ -312,6 +312,9 @@ export function getBatchItems( + + + diff --git a/backend/src/common/prompt/prompt.fallbacks.ts b/backend/src/common/prompt/prompt.fallbacks.ts index 25a11fe0..db859608 100644 --- a/backend/src/common/prompt/prompt.fallbacks.ts +++ b/backend/src/common/prompt/prompt.fallbacks.ts @@ -100,3 +100,4 @@ export function getAllFallbackCodes(): string[] { + diff --git a/backend/src/common/prompt/prompt.types.ts b/backend/src/common/prompt/prompt.types.ts index 61231c85..50c79b0d 100644 --- a/backend/src/common/prompt/prompt.types.ts +++ b/backend/src/common/prompt/prompt.types.ts @@ -69,3 +69,4 @@ export interface VariableValidation { + diff --git a/backend/src/modules/admin/routes/tenantRoutes.ts b/backend/src/modules/admin/routes/tenantRoutes.ts index 774086d9..5498b58f 100644 --- a/backend/src/modules/admin/routes/tenantRoutes.ts +++ b/backend/src/modules/admin/routes/tenantRoutes.ts @@ -75,3 +75,4 @@ 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 466e8f9c..517d5a08 100644 --- a/backend/src/modules/admin/types/tenant.types.ts +++ b/backend/src/modules/admin/types/tenant.types.ts @@ -105,3 +105,4 @@ export interface PaginatedResponse { totalPages: number; } + diff --git a/backend/src/modules/asl/controllers/literatureController.ts b/backend/src/modules/asl/controllers/literatureController.ts index ce6d6066..d7587721 100644 --- a/backend/src/modules/asl/controllers/literatureController.ts +++ b/backend/src/modules/asl/controllers/literatureController.ts @@ -9,6 +9,17 @@ import { logger } from '../../../common/logging/index.js'; import * as XLSX from 'xlsx'; import { startScreeningTask } from '../services/screeningService.js'; +/** + * 获取用户ID(从JWT Token中获取) + */ +function getUserId(request: FastifyRequest): string { + const userId = (request as any).user?.userId; + if (!userId) { + throw new Error('User not authenticated'); + } + return userId; +} + /** * 导入文献(从Excel或JSON) */ @@ -17,7 +28,7 @@ export async function importLiteratures( reply: FastifyReply ) { try { - const userId = (request as any).userId || 'asl-test-user-001'; + const userId = getUserId(request); const { projectId, literatures } = request.body; // 验证项目归属 @@ -90,7 +101,7 @@ export async function importLiteraturesFromExcel( reply: FastifyReply ) { try { - const userId = (request as any).userId || 'asl-test-user-001'; + const userId = getUserId(request); // 获取上传的文件 const data = await request.file(); @@ -176,7 +187,7 @@ export async function getLiteratures( reply: FastifyReply ) { try { - const userId = (request as any).userId || 'asl-test-user-001'; + const userId = getUserId(request); const { projectId } = request.params; const { page = 1, limit = 50 } = request.query; @@ -239,7 +250,7 @@ export async function deleteLiterature( reply: FastifyReply ) { try { - const userId = (request as any).userId || 'asl-test-user-001'; + const userId = getUserId(request); const { literatureId } = request.params; // 验证文献归属 diff --git a/backend/src/modules/asl/controllers/projectController.ts b/backend/src/modules/asl/controllers/projectController.ts index f7321b27..293113b3 100644 --- a/backend/src/modules/asl/controllers/projectController.ts +++ b/backend/src/modules/asl/controllers/projectController.ts @@ -7,16 +7,26 @@ import { CreateScreeningProjectDto } from '../types/index.js'; import { prisma } from '../../../config/database.js'; import { logger } from '../../../common/logging/index.js'; +/** + * 获取用户ID(从JWT Token中获取) + */ +function getUserId(request: FastifyRequest): string { + const userId = (request as any).user?.userId; + if (!userId) { + throw new Error('User not authenticated'); + } + return userId; +} + /** * 创建筛选项目 */ export async function createProject( - request: FastifyRequest<{ Body: CreateScreeningProjectDto & { userId?: string } }>, + request: FastifyRequest<{ Body: CreateScreeningProjectDto }>, reply: FastifyReply ) { try { - // 临时测试模式:优先从JWT获取,否则从请求体获取 - const userId = (request as any).userId || (request.body as any).userId || 'asl-test-user-001'; + const userId = getUserId(request); const { projectName, picoCriteria, inclusionCriteria, exclusionCriteria, screeningConfig } = request.body; // 验证必填字段 @@ -65,7 +75,7 @@ export async function createProject( */ export async function getProjects(request: FastifyRequest, reply: FastifyReply) { try { - const userId = (request as any).userId || 'asl-test-user-001'; + const userId = getUserId(request); const projects = await prisma.aslScreeningProject.findMany({ where: { userId }, @@ -100,7 +110,7 @@ export async function getProjectById( reply: FastifyReply ) { try { - const userId = (request as any).userId || 'asl-test-user-001'; + const userId = getUserId(request); const { projectId } = request.params; const project = await prisma.aslScreeningProject.findFirst({ @@ -148,7 +158,7 @@ export async function updateProject( reply: FastifyReply ) { try { - const userId = (request as any).userId || 'asl-test-user-001'; + const userId = getUserId(request); const { projectId } = request.params; const updateData = request.body; @@ -190,7 +200,7 @@ export async function deleteProject( reply: FastifyReply ) { try { - const userId = (request as any).userId || 'asl-test-user-001'; + const userId = getUserId(request); const { projectId } = request.params; // 验证项目归属 diff --git a/backend/src/modules/asl/controllers/screeningController.ts b/backend/src/modules/asl/controllers/screeningController.ts index 1ace19d2..9a269c7b 100644 --- a/backend/src/modules/asl/controllers/screeningController.ts +++ b/backend/src/modules/asl/controllers/screeningController.ts @@ -6,6 +6,17 @@ import { FastifyRequest, FastifyReply } from 'fastify'; import { prisma } from '../../../config/database.js'; import { logger } from '../../../common/logging/index.js'; +/** + * 获取用户ID(从JWT Token中获取) + */ +function getUserId(request: FastifyRequest): string { + const userId = (request as any).user?.userId; + if (!userId) { + throw new Error('User not authenticated'); + } + return userId; +} + /** * 获取筛选任务进度 * GET /api/v1/asl/projects/:projectId/screening-task @@ -15,7 +26,7 @@ export async function getScreeningTask( reply: FastifyReply ) { try { - const userId = (request as any).userId || 'asl-test-user-001'; + const userId = getUserId(request); const { projectId } = request.params; // 验证项目归属 @@ -74,7 +85,7 @@ export async function getScreeningResults( reply: FastifyReply ) { try { - const userId = (request as any).userId || 'asl-test-user-001'; + const userId = getUserId(request); const { projectId } = request.params; const page = parseInt(request.query.page || '1', 10); const pageSize = parseInt(request.query.pageSize || '50', 10); @@ -175,7 +186,7 @@ export async function getScreeningResultDetail( reply: FastifyReply ) { try { - const userId = (request as any).userId || 'asl-test-user-001'; + const userId = getUserId(request); const { resultId } = request.params; const result = await prisma.aslScreeningResult.findUnique({ @@ -241,7 +252,7 @@ export async function reviewScreeningResult( reply: FastifyReply ) { try { - const userId = (request as any).userId || 'asl-test-user-001'; + const userId = getUserId(request); const { resultId } = request.params; const { decision, note } = request.body; @@ -327,7 +338,7 @@ export async function getProjectStatistics( reply: FastifyReply ) { try { - const userId = (request as any).userId || 'asl-test-user-001'; + const userId = getUserId(request); const { projectId } = request.params; // 1. 验证项目归属 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 f044e34c..3b19b01f 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 @@ -348,6 +348,9 @@ 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 eddb5bed..8d356053 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 @@ -289,6 +289,9 @@ 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 042f74aa..d2e2f481 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 @@ -327,6 +327,9 @@ Content-Type: application/json + + + diff --git a/backend/src/modules/asl/fulltext-screening/controllers/FulltextScreeningController.ts b/backend/src/modules/asl/fulltext-screening/controllers/FulltextScreeningController.ts index 0a87338a..40f1a8f7 100644 --- a/backend/src/modules/asl/fulltext-screening/controllers/FulltextScreeningController.ts +++ b/backend/src/modules/asl/fulltext-screening/controllers/FulltextScreeningController.ts @@ -18,6 +18,17 @@ import { FulltextScreeningService } from '../services/FulltextScreeningService.j // 初始化服务 const screeningService = new FulltextScreeningService(); +/** + * 获取用户ID(从JWT Token中获取) + */ +function getUserId(request: FastifyRequest): string { + const userId = (request as any).user?.userId; + if (!userId) { + throw new Error('User not authenticated'); + } + return userId; +} + // ==================== Zod验证Schema ==================== /** @@ -79,7 +90,7 @@ export async function createTask( const { projectId, literatureIds, modelA, modelB, promptVersion } = validated; // 获取当前用户ID(测试模式) - const userId = (request as any).userId || 'asl-test-user-001'; + const userId = getUserId(request); logger.info('Creating fulltext screening task', { projectId, @@ -204,7 +215,7 @@ export async function getTaskProgress( ) { try { const { taskId } = request.params; - const userId = (request as any).userId || 'asl-test-user-001'; + const userId = getUserId(request); // 获取任务详情 const task = await prisma.aslFulltextScreeningTask.findFirst({ @@ -318,7 +329,7 @@ export async function getTaskResults( ) { try { const { taskId } = request.params; - const userId = (request as any).userId || 'asl-test-user-001'; + const userId = getUserId(request); // 参数验证 const validated = GetResultsQuerySchema.parse(request.query); @@ -507,7 +518,7 @@ export async function updateDecision( ) { try { const { resultId } = request.params; - const userId = (request as any).userId || 'asl-test-user-001'; + const userId = getUserId(request); // 参数验证 const validated = UpdateDecisionSchema.parse(request.body); @@ -589,7 +600,7 @@ export async function exportExcel( ) { try { const { taskId } = request.params; - const userId = (request as any).userId || 'asl-test-user-001'; + const userId = getUserId(request); // 验证任务权限 const task = await prisma.aslFulltextScreeningTask.findFirst({ diff --git a/backend/src/modules/dc/tool-b/services/ConflictDetectionService.ts b/backend/src/modules/dc/tool-b/services/ConflictDetectionService.ts index dd7fde3a..643032fa 100644 --- a/backend/src/modules/dc/tool-b/services/ConflictDetectionService.ts +++ b/backend/src/modules/dc/tool-b/services/ConflictDetectionService.ts @@ -267,5 +267,6 @@ 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 4e5a9812..530c584b 100644 --- a/backend/src/modules/dc/tool-c/README.md +++ b/backend/src/modules/dc/tool-c/README.md @@ -217,5 +217,6 @@ 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 9859cb7c..7108757b 100644 --- a/backend/src/modules/dc/tool-c/controllers/StreamAIController.ts +++ b/backend/src/modules/dc/tool-c/controllers/StreamAIController.ts @@ -271,5 +271,6 @@ 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 4dbc431c..fc41b8d5 100644 --- a/backend/src/modules/iit-manager/agents/SessionMemory.ts +++ b/backend/src/modules/iit-manager/agents/SessionMemory.ts @@ -180,3 +180,6 @@ 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 e5c7b00a..88959889 100644 --- a/backend/src/modules/iit-manager/check-iit-table-structure.ts +++ b/backend/src/modules/iit-manager/check-iit-table-structure.ts @@ -114,3 +114,6 @@ checkTableStructure(); + + + diff --git a/backend/src/modules/iit-manager/check-project-config.ts b/backend/src/modules/iit-manager/check-project-config.ts index 37ff9b7f..2ad18bed 100644 --- a/backend/src/modules/iit-manager/check-project-config.ts +++ b/backend/src/modules/iit-manager/check-project-config.ts @@ -101,3 +101,6 @@ 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 0568720e..86523b99 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 @@ -83,3 +83,6 @@ main(); + + + diff --git a/backend/src/modules/iit-manager/docs/微信服务号接入指南.md b/backend/src/modules/iit-manager/docs/微信服务号接入指南.md index a70704d6..e7fd566f 100644 --- a/backend/src/modules/iit-manager/docs/微信服务号接入指南.md +++ b/backend/src/modules/iit-manager/docs/微信服务号接入指南.md @@ -540,3 +540,6 @@ 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 e6d9dcd8..c6afa2a1 100644 --- a/backend/src/modules/iit-manager/generate-wechat-tokens.ts +++ b/backend/src/modules/iit-manager/generate-wechat-tokens.ts @@ -175,3 +175,6 @@ console.log(''); + + + diff --git a/backend/src/modules/iit-manager/services/PatientWechatService.ts b/backend/src/modules/iit-manager/services/PatientWechatService.ts index 70d04e9f..111a5137 100644 --- a/backend/src/modules/iit-manager/services/PatientWechatService.ts +++ b/backend/src/modules/iit-manager/services/PatientWechatService.ts @@ -492,3 +492,6 @@ 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 5c353f52..32089739 100644 --- a/backend/src/modules/iit-manager/test-chatservice-dify.ts +++ b/backend/src/modules/iit-manager/test-chatservice-dify.ts @@ -137,3 +137,6 @@ 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 f7a91cca..826ebf21 100644 --- a/backend/src/modules/iit-manager/test-iit-database.ts +++ b/backend/src/modules/iit-manager/test-iit-database.ts @@ -166,3 +166,6 @@ 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 2d9973b1..2fce0610 100644 --- a/backend/src/modules/iit-manager/test-patient-wechat-config.ts +++ b/backend/src/modules/iit-manager/test-patient-wechat-config.ts @@ -152,3 +152,6 @@ 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 78b7cdd6..f066fb23 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 @@ -178,3 +178,6 @@ 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 e68b84b8..bfd61ea5 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 @@ -259,3 +259,6 @@ 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 563109de..3b884410 100644 --- a/backend/src/modules/iit-manager/test-wechat-mp-local.ps1 +++ b/backend/src/modules/iit-manager/test-wechat-mp-local.ps1 @@ -143,3 +143,6 @@ Write-Host "" + + + diff --git a/backend/src/modules/iit-manager/types/index.ts b/backend/src/modules/iit-manager/types/index.ts index a9e6ae4c..2709e771 100644 --- a/backend/src/modules/iit-manager/types/index.ts +++ b/backend/src/modules/iit-manager/types/index.ts @@ -236,3 +236,6 @@ export interface CachedProtocolRules { + + + diff --git a/backend/src/modules/pkb/controllers/batchController.ts b/backend/src/modules/pkb/controllers/batchController.ts index 9014c527..e1cacfb6 100644 --- a/backend/src/modules/pkb/controllers/batchController.ts +++ b/backend/src/modules/pkb/controllers/batchController.ts @@ -13,6 +13,17 @@ import { executeBatchTask, retryFailedDocuments, BatchProgress } from '../servic import { prisma } from '../../../config/database.js'; import { ModelType } from '../../../common/llm/adapters/types.js'; +/** + * 获取用户ID(从JWT Token中获取) + */ +function getUserId(request: FastifyRequest): string { + const userId = (request as any).user?.userId; + if (!userId) { + throw new Error('User not authenticated'); + } + return userId; +} + // ==================== 类型定义 ==================== interface ExecuteBatchBody { @@ -40,8 +51,7 @@ export async function executeBatch( reply: FastifyReply ) { try { - // TODO: 从JWT获取userId - const userId = 'user-mock-001'; + const userId = getUserId(request); const { kb_id, @@ -365,7 +375,7 @@ export async function retryFailed( ) { try { const { taskId } = request.params; - const userId = 'user-mock-001'; // TODO: 从JWT获取 + const userId = getUserId(request); // 获取WebSocket实例 const io = (request.server as any).io; diff --git a/backend/src/modules/pkb/controllers/documentController.ts b/backend/src/modules/pkb/controllers/documentController.ts index 1e0514d1..085cd3e6 100644 --- a/backend/src/modules/pkb/controllers/documentController.ts +++ b/backend/src/modules/pkb/controllers/documentController.ts @@ -1,8 +1,16 @@ import type { FastifyRequest, FastifyReply } from 'fastify'; import * as documentService from '../services/documentService.js'; -// Mock用户ID(实际应从JWT token中获取) -const MOCK_USER_ID = 'user-mock-001'; +/** + * 获取用户ID(从JWT Token中获取) + */ +function getUserId(request: FastifyRequest): string { + const userId = (request as any).user?.userId; + if (!userId) { + throw new Error('User not authenticated'); + } + return userId; +} /** * 上传文档 @@ -69,8 +77,9 @@ export async function uploadDocument( // 上传文档(这里fileUrl暂时为空,实际应该上传到对象存储) console.log(`⚙️ 调用文档服务上传文件...`); + const userId = getUserId(request); const document = await documentService.uploadDocument( - MOCK_USER_ID, + userId, kbId, file, filename, @@ -123,7 +132,8 @@ export async function getDocuments( try { const { kbId } = request.params; - const documents = await documentService.getDocuments(MOCK_USER_ID, kbId); + const userId = getUserId(request); + const documents = await documentService.getDocuments(userId, kbId); return reply.send({ success: true, @@ -160,7 +170,8 @@ export async function getDocumentById( try { const { id } = request.params; - const document = await documentService.getDocumentById(MOCK_USER_ID, id); + const userId = getUserId(request); + const document = await documentService.getDocumentById(userId, id); return reply.send({ success: true, @@ -197,7 +208,8 @@ export async function deleteDocument( try { const { id } = request.params; - await documentService.deleteDocument(MOCK_USER_ID, id); + const userId = getUserId(request); + await documentService.deleteDocument(userId, id); return reply.send({ success: true, @@ -234,7 +246,8 @@ export async function reprocessDocument( try { const { id } = request.params; - await documentService.reprocessDocument(MOCK_USER_ID, id); + const userId = getUserId(request); + await documentService.reprocessDocument(userId, id); return reply.send({ success: true, @@ -271,7 +284,8 @@ export async function getDocumentFullText( try { const { id } = request.params; - const document = await documentService.getDocumentById(MOCK_USER_ID, id); + const userId = getUserId(request); + const document = await documentService.getDocumentById(userId, id); // 返回完整的文档信息 return reply.send({ diff --git a/backend/src/modules/pkb/controllers/knowledgeBaseController.ts b/backend/src/modules/pkb/controllers/knowledgeBaseController.ts index 46a220aa..da05905e 100644 --- a/backend/src/modules/pkb/controllers/knowledgeBaseController.ts +++ b/backend/src/modules/pkb/controllers/knowledgeBaseController.ts @@ -1,8 +1,16 @@ import type { FastifyRequest, FastifyReply } from 'fastify'; import * as knowledgeBaseService from '../services/knowledgeBaseService.js'; -// Mock用户ID(实际应从JWT token中获取) -const MOCK_USER_ID = 'user-mock-001'; +/** + * 获取用户ID(从JWT Token中获取) + */ +function getUserId(request: FastifyRequest): string { + const userId = (request as any).user?.userId; + if (!userId) { + throw new Error('User not authenticated'); + } + return userId; +} /** * 创建知识库 @@ -26,8 +34,9 @@ export async function createKnowledgeBase( }); } + const userId = getUserId(request); const knowledgeBase = await knowledgeBaseService.createKnowledgeBase( - MOCK_USER_ID, + userId, name, description ); @@ -49,12 +58,13 @@ export async function createKnowledgeBase( * 获取知识库列表 */ export async function getKnowledgeBases( - _request: FastifyRequest, + request: FastifyRequest, reply: FastifyReply ) { try { + const userId = getUserId(request); const knowledgeBases = await knowledgeBaseService.getKnowledgeBases( - MOCK_USER_ID + userId ); return reply.send({ @@ -84,8 +94,9 @@ export async function getKnowledgeBaseById( try { const { id } = request.params; + const userId = getUserId(request); const knowledgeBase = await knowledgeBaseService.getKnowledgeBaseById( - MOCK_USER_ID, + userId, id ); @@ -129,8 +140,9 @@ export async function updateKnowledgeBase( const { id } = request.params; const updateData = request.body; + const userId = getUserId(request); const knowledgeBase = await knowledgeBaseService.updateKnowledgeBase( - MOCK_USER_ID, + userId, id, updateData ); @@ -170,7 +182,8 @@ export async function deleteKnowledgeBase( try { const { id } = request.params; - await knowledgeBaseService.deleteKnowledgeBase(MOCK_USER_ID, id); + const userId = getUserId(request); + await knowledgeBaseService.deleteKnowledgeBase(userId, id); return reply.send({ success: true, @@ -221,8 +234,9 @@ export async function searchKnowledgeBase( const topK = top_k ? parseInt(top_k, 10) : 15; // Phase 1优化:默认从3增加到15 + const userId = getUserId(request); const results = await knowledgeBaseService.searchKnowledgeBase( - MOCK_USER_ID, + userId, id, query, topK @@ -263,8 +277,9 @@ export async function getKnowledgeBaseStats( try { const { id } = request.params; + const userId = getUserId(request); const stats = await knowledgeBaseService.getKnowledgeBaseStats( - MOCK_USER_ID, + userId, id ); @@ -311,8 +326,9 @@ export async function getDocumentSelection( const maxFiles = max_files ? parseInt(max_files, 10) : undefined; const maxTokens = max_tokens ? parseInt(max_tokens, 10) : undefined; + const userId = getUserId(request); const selection = await knowledgeBaseService.getDocumentSelection( - MOCK_USER_ID, + userId, id, maxFiles, maxTokens diff --git a/backend/src/modules/pkb/routes/health.ts b/backend/src/modules/pkb/routes/health.ts index 2febca73..b136dc6c 100644 --- a/backend/src/modules/pkb/routes/health.ts +++ b/backend/src/modules/pkb/routes/health.ts @@ -51,3 +51,4 @@ export default async function healthRoutes(fastify: FastifyInstance) { + diff --git a/backend/src/modules/pkb/services/knowledgeBaseService.ts b/backend/src/modules/pkb/services/knowledgeBaseService.ts index 7f877bf7..cbb75216 100644 --- a/backend/src/modules/pkb/services/knowledgeBaseService.ts +++ b/backend/src/modules/pkb/services/knowledgeBaseService.ts @@ -29,9 +29,14 @@ export async function createKnowledgeBase( } // 2. 在Dify中创建Dataset + // Dify API name字段限制:避免特殊字符,保持简洁 + const sanitizedName = name + .replace(/[^\u4e00-\u9fa5a-zA-Z0-9_-]/g, '_') // 移除特殊字符 + .substring(0, 50); // 限制长度 + const difyDataset = await difyClient.createDataset({ - name: `${userId}_${name}_${Date.now()}`, - description: description || `Knowledge base for user ${userId}`, + name: `kb_${sanitizedName}_${Date.now()}`, // 简化格式 + description: description?.substring(0, 200) || '', // 限制描述长度 indexing_technique: 'high_quality', }); diff --git a/backend/src/modules/rvw/__tests__/api.http b/backend/src/modules/rvw/__tests__/api.http index 4526b2ca..baaf05f9 100644 --- a/backend/src/modules/rvw/__tests__/api.http +++ b/backend/src/modules/rvw/__tests__/api.http @@ -127,3 +127,6 @@ 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 528ee649..d06ecb8d 100644 --- a/backend/src/modules/rvw/__tests__/test-api.ps1 +++ b/backend/src/modules/rvw/__tests__/test-api.ps1 @@ -112,3 +112,6 @@ Write-Host " - 删除任务: DELETE $BaseUrl/api/v2/rvw/tasks/{taskId}" -Foregr + + + diff --git a/backend/src/modules/rvw/controllers/reviewController.ts b/backend/src/modules/rvw/controllers/reviewController.ts index 8c8aeae9..2a961b8b 100644 --- a/backend/src/modules/rvw/controllers/reviewController.ts +++ b/backend/src/modules/rvw/controllers/reviewController.ts @@ -15,16 +15,16 @@ import { ModelType } from '../../../common/llm/adapters/types.js'; import * as reviewService from '../services/reviewService.js'; import { AgentType } from '../types/index.js'; -// TODO: 集成JWT认证后移除 -const MOCK_USER_ID = 'user-mock-001'; - /** - * 获取用户ID(暂时使用Mock,待JWT集成) + * 获取用户ID(从JWT Token中获取) + * 如果未认证则抛出错误 */ function getUserId(request: FastifyRequest): string { - // TODO: 从JWT token中获取 - // return request.user?.id; - return MOCK_USER_ID; + const userId = (request as any).user?.userId; + if (!userId) { + throw new Error('User not authenticated'); + } + return userId; } // ==================== 任务创建 ==================== @@ -121,12 +121,20 @@ export async function createTask( }, }); } catch (error) { - logger.error('[RVW:Controller] 上传失败', { - error: error instanceof Error ? error.message : 'Unknown error' - }); + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + logger.error('[RVW:Controller] 上传失败', { error: errorMessage }); + + // 区分认证错误和其他错误 + if (errorMessage === 'User not authenticated') { + return reply.status(401).send({ + success: false, + message: '用户未认证,请重新登录', + }); + } + return reply.status(500).send({ success: false, - message: error instanceof Error ? error.message : '上传失败', + message: errorMessage || '上传失败', }); } } diff --git a/backend/src/modules/rvw/index.ts b/backend/src/modules/rvw/index.ts index 410d8eee..2d4e48d8 100644 --- a/backend/src/modules/rvw/index.ts +++ b/backend/src/modules/rvw/index.ts @@ -26,3 +26,6 @@ export * from './services/utils.js'; + + + diff --git a/backend/src/modules/rvw/services/editorialService.ts b/backend/src/modules/rvw/services/editorialService.ts index 5ccbd17c..b93f283b 100644 --- a/backend/src/modules/rvw/services/editorialService.ts +++ b/backend/src/modules/rvw/services/editorialService.ts @@ -1,36 +1,44 @@ /** * RVW稿件审查模块 - 稿约规范性评估服务 * @module rvw/services/editorialService + * + * Phase 3.5.5 改造:使用 PromptService 替代文件读取 + * - 支持灰度预览(调试者看 DRAFT,普通用户看 ACTIVE) + * - 三级容灾(数据库→缓存→兜底) */ -import fs from 'fs/promises'; -import path from 'path'; -import { fileURLToPath } from 'url'; import { LLMFactory } from '../../../common/llm/adapters/LLMFactory.js'; import { ModelType } from '../../../common/llm/adapters/types.js'; import { logger } from '../../../common/logging/index.js'; +import { prisma } from '../../../config/database.js'; +import { getPromptService } from '../../../common/prompt/index.js'; import { EditorialReview } from '../types/index.js'; import { parseJSONFromLLMResponse } from './utils.js'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -// Prompt文件路径 -const PROMPT_PATH = path.join(__dirname, '../../../../prompts/review_editorial_system.txt'); - /** * 稿约规范性评估 * @param text 稿件文本 * @param modelType 模型类型 + * @param userId 用户ID(用于灰度预览判断) * @returns 评估结果 */ export async function reviewEditorialStandards( text: string, - modelType: ModelType = 'deepseek-v3' + modelType: ModelType = 'deepseek-v3', + userId?: string ): Promise { try { - // 1. 读取系统Prompt - const systemPrompt = await fs.readFile(PROMPT_PATH, 'utf-8'); + // 1. 从 PromptService 获取系统Prompt(支持灰度预览) + const promptService = getPromptService(prisma); + const { content: systemPrompt, isDraft } = await promptService.get( + 'RVW_EDITORIAL', + {}, + { userId } + ); + + if (isDraft) { + logger.info('[RVW:Editorial] 使用 DRAFT 版本 Prompt(调试模式)', { userId }); + } // 2. 构建消息 const messages = [ @@ -71,3 +79,4 @@ export async function reviewEditorialStandards( + diff --git a/backend/src/modules/rvw/services/methodologyService.ts b/backend/src/modules/rvw/services/methodologyService.ts index d4ea8297..65d632e8 100644 --- a/backend/src/modules/rvw/services/methodologyService.ts +++ b/backend/src/modules/rvw/services/methodologyService.ts @@ -1,36 +1,44 @@ /** * RVW稿件审查模块 - 方法学评估服务 * @module rvw/services/methodologyService + * + * Phase 3.5.5 改造:使用 PromptService 替代文件读取 + * - 支持灰度预览(调试者看 DRAFT,普通用户看 ACTIVE) + * - 三级容灾(数据库→缓存→兜底) */ -import fs from 'fs/promises'; -import path from 'path'; -import { fileURLToPath } from 'url'; import { LLMFactory } from '../../../common/llm/adapters/LLMFactory.js'; import { ModelType } from '../../../common/llm/adapters/types.js'; import { logger } from '../../../common/logging/index.js'; +import { prisma } from '../../../config/database.js'; +import { getPromptService } from '../../../common/prompt/index.js'; import { MethodologyReview } from '../types/index.js'; import { parseJSONFromLLMResponse } from './utils.js'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -// Prompt文件路径 -const PROMPT_PATH = path.join(__dirname, '../../../../prompts/review_methodology_system.txt'); - /** * 方法学评估 * @param text 稿件文本 * @param modelType 模型类型 + * @param userId 用户ID(用于灰度预览判断) * @returns 评估结果 */ export async function reviewMethodology( text: string, - modelType: ModelType = 'deepseek-v3' + modelType: ModelType = 'deepseek-v3', + userId?: string ): Promise { try { - // 1. 读取系统Prompt - const systemPrompt = await fs.readFile(PROMPT_PATH, 'utf-8'); + // 1. 从 PromptService 获取系统Prompt(支持灰度预览) + const promptService = getPromptService(prisma); + const { content: systemPrompt, isDraft } = await promptService.get( + 'RVW_METHODOLOGY', + {}, + { userId } + ); + + if (isDraft) { + logger.info('[RVW:Methodology] 使用 DRAFT 版本 Prompt(调试模式)', { userId }); + } // 2. 构建消息 const messages = [ @@ -71,3 +79,4 @@ export async function reviewMethodology( + diff --git a/backend/src/modules/rvw/services/utils.ts b/backend/src/modules/rvw/services/utils.ts index ec76f256..45eb9fb1 100644 --- a/backend/src/modules/rvw/services/utils.ts +++ b/backend/src/modules/rvw/services/utils.ts @@ -117,3 +117,6 @@ export function validateAgentSelection(agents: string[]): void { + + + diff --git a/backend/src/modules/rvw/workers/reviewWorker.ts b/backend/src/modules/rvw/workers/reviewWorker.ts index fe21a9c0..1380f5b8 100644 --- a/backend/src/modules/rvw/workers/reviewWorker.ts +++ b/backend/src/modules/rvw/workers/reviewWorker.ts @@ -78,7 +78,8 @@ export function registerReviewWorker() { logger.info('[reviewWorker] Running editorial review', { taskId }); console.log(' 🔍 运行稿约规范性智能体...'); - editorialResult = await reviewEditorialStandards(extractedText, modelType); + // ✅ Phase 3.5.5: 传递 userId 支持灰度预览 + editorialResult = await reviewEditorialStandards(extractedText, modelType, userId); logger.info('[reviewWorker] Editorial review completed', { taskId, @@ -97,7 +98,8 @@ export function registerReviewWorker() { logger.info('[reviewWorker] Running methodology review', { taskId }); console.log(' 🔬 运行方法学智能体...'); - methodologyResult = await reviewMethodology(extractedText, modelType); + // ✅ Phase 3.5.5: 传递 userId 支持灰度预览 + methodologyResult = await reviewMethodology(extractedText, modelType, userId); logger.info('[reviewWorker] Methodology review completed', { taskId, diff --git a/backend/src/tests/README.md b/backend/src/tests/README.md index 0187a55a..b269343f 100644 --- a/backend/src/tests/README.md +++ b/backend/src/tests/README.md @@ -413,6 +413,9 @@ SET session_replication_role = 'origin'; + + + diff --git a/backend/src/tests/verify-test1-database.sql b/backend/src/tests/verify-test1-database.sql index 79e00e66..3f6827ae 100644 --- a/backend/src/tests/verify-test1-database.sql +++ b/backend/src/tests/verify-test1-database.sql @@ -115,6 +115,9 @@ WHERE key = 'verify_test'; + + + diff --git a/backend/src/tests/verify-test1-database.ts b/backend/src/tests/verify-test1-database.ts index ee567e0f..0b9d3a32 100644 --- a/backend/src/tests/verify-test1-database.ts +++ b/backend/src/tests/verify-test1-database.ts @@ -258,6 +258,9 @@ verifyDatabase() + + + diff --git a/backend/src/types/global.d.ts b/backend/src/types/global.d.ts index 3246bcdc..a6f07d20 100644 --- a/backend/src/types/global.d.ts +++ b/backend/src/types/global.d.ts @@ -48,6 +48,9 @@ export {} + + + diff --git a/backend/sync-dc-database.ps1 b/backend/sync-dc-database.ps1 index 21860e7f..4f17a48e 100644 --- a/backend/sync-dc-database.ps1 +++ b/backend/sync-dc-database.ps1 @@ -71,6 +71,9 @@ Write-Host "✅ 完成!" -ForegroundColor Green + + + diff --git a/backend/temp_check.sql b/backend/temp_check.sql index 27fd5db8..601ce2ed 100644 --- a/backend/temp_check.sql +++ b/backend/temp_check.sql @@ -1,2 +1,5 @@ SELECT schema_name FROM information_schema.schemata WHERE schema_name NOT IN ('pg_catalog', 'information_schema', 'pg_toast') ORDER BY schema_name; + + + diff --git a/backend/test-pkb-migration.http b/backend/test-pkb-migration.http index dc0760e2..bba261ae 100644 --- a/backend/test-pkb-migration.http +++ b/backend/test-pkb-migration.http @@ -163,3 +163,6 @@ DELETE {{baseUrl}}/api/v2/pkb/knowledge/knowledge-bases/{{testKbId}} + + + diff --git a/backend/test-tool-c-advanced-scenarios.mjs b/backend/test-tool-c-advanced-scenarios.mjs index 03d69e96..5f22b4ea 100644 --- a/backend/test-tool-c-advanced-scenarios.mjs +++ b/backend/test-tool-c-advanced-scenarios.mjs @@ -358,6 +358,9 @@ runAdvancedTests().catch(error => { + + + diff --git a/backend/test-tool-c-day2.mjs b/backend/test-tool-c-day2.mjs index 9e652c21..1e22cb8e 100644 --- a/backend/test-tool-c-day2.mjs +++ b/backend/test-tool-c-day2.mjs @@ -424,6 +424,9 @@ runAllTests() + + + diff --git a/backend/test-tool-c-day3.mjs b/backend/test-tool-c-day3.mjs index bfc3a367..4f593e81 100644 --- a/backend/test-tool-c-day3.mjs +++ b/backend/test-tool-c-day3.mjs @@ -382,6 +382,9 @@ runAllTests() + + + diff --git a/backend/verify_all_users.ts b/backend/verify_all_users.ts index 8abccfa8..fb56423d 100644 --- a/backend/verify_all_users.ts +++ b/backend/verify_all_users.ts @@ -20,3 +20,6 @@ main() .catch(console.error) .finally(() => prisma.$disconnect()); + + + diff --git a/backend/verify_functions.ts b/backend/verify_functions.ts index 69f810e6..d0b74b5a 100644 --- a/backend/verify_functions.ts +++ b/backend/verify_functions.ts @@ -18,3 +18,6 @@ main() .catch(console.error) .finally(() => prisma.$disconnect()); + + + diff --git a/backend/verify_job_common.ts b/backend/verify_job_common.ts index 75bb3506..374cd4f5 100644 --- a/backend/verify_job_common.ts +++ b/backend/verify_job_common.ts @@ -30,3 +30,6 @@ main() .catch(console.error) .finally(() => prisma.$disconnect()); + + + diff --git a/backend/verify_mock_user.ts b/backend/verify_mock_user.ts index 21bebe04..6e7b616e 100644 --- a/backend/verify_mock_user.ts +++ b/backend/verify_mock_user.ts @@ -19,3 +19,6 @@ main() .catch(console.error) .finally(() => prisma.$disconnect()); + + + diff --git a/backend/verify_system.ts b/backend/verify_system.ts index efb64c40..7c464285 100644 --- a/backend/verify_system.ts +++ b/backend/verify_system.ts @@ -159,3 +159,6 @@ main() .catch(console.error) .finally(() => prisma.$disconnect()); + + + diff --git a/backup_aia_migration_20260111_223208.sql b/backup_aia_migration_20260111_223208.sql new file mode 100644 index 0000000000000000000000000000000000000000..62469de9e20c993dfdd85c239e5a6b5f70617e7a GIT binary patch literal 491324 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

h?Qgf7?K>=W53l((YlXmnXtwhJkLU=6RF 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

SC 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 literal 0 HcmV?d00001 diff --git a/deploy-to-sae.ps1 b/deploy-to-sae.ps1 index 046c61e5..bc6984a7 100644 --- a/deploy-to-sae.ps1 +++ b/deploy-to-sae.ps1 @@ -166,6 +166,9 @@ Set-Location .. + + + diff --git a/docs/02-通用能力层/Postgres-Only异步任务处理指南.md b/docs/02-通用能力层/Postgres-Only异步任务处理指南.md index aed26ad9..e02e6554 100644 --- a/docs/02-通用能力层/Postgres-Only异步任务处理指南.md +++ b/docs/02-通用能力层/Postgres-Only异步任务处理指南.md @@ -608,6 +608,9 @@ async saveProcessedData(recordId, newData) { + + + diff --git a/docs/02-通用能力层/通用能力层技术债务清单.md b/docs/02-通用能力层/通用能力层技术债务清单.md index 80ac911e..eb3cf007 100644 --- a/docs/02-通用能力层/通用能力层技术债务清单.md +++ b/docs/02-通用能力层/通用能力层技术债务清单.md @@ -795,6 +795,9 @@ export const AsyncProgressBar: React.FC = ({ + + + diff --git a/docs/03-业务模块/ADMIN-运营管理端/00-Phase3.5完成总结.md b/docs/03-业务模块/ADMIN-运营管理端/00-Phase3.5完成总结.md index 42b50e24..c8faae4a 100644 --- a/docs/03-业务模块/ADMIN-运营管理端/00-Phase3.5完成总结.md +++ b/docs/03-业务模块/ADMIN-运营管理端/00-Phase3.5完成总结.md @@ -294,3 +294,4 @@ Level 3: 兜底Prompt(缓存也失效) + diff --git a/docs/03-业务模块/ADMIN-运营管理端/README.md b/docs/03-业务模块/ADMIN-运营管理端/README.md index 5b1578c2..595b56f3 100644 --- a/docs/03-业务模块/ADMIN-运营管理端/README.md +++ b/docs/03-业务模块/ADMIN-运营管理端/README.md @@ -214,3 +214,4 @@ ADMIN-运营管理端/ + diff --git a/docs/03-业务模块/ADMIN运营与INST机构管理端-文档体系建立完成.md b/docs/03-业务模块/ADMIN运营与INST机构管理端-文档体系建立完成.md index fa6a169f..a15250be 100644 --- a/docs/03-业务模块/ADMIN运营与INST机构管理端-文档体系建立完成.md +++ b/docs/03-业务模块/ADMIN运营与INST机构管理端-文档体系建立完成.md @@ -311,3 +311,6 @@ INST-机构管理端/ *报告完毕 - 2026-01-11* + + + diff --git a/docs/03-业务模块/AIA-AI智能问答/01-需求分析/AIA模块PRD.md b/docs/03-业务模块/AIA-AI智能问答/01-需求分析/AIA模块PRD.md new file mode 100644 index 00000000..aca68fcc --- /dev/null +++ b/docs/03-业务模块/AIA-AI智能问答/01-需求分析/AIA模块PRD.md @@ -0,0 +1,135 @@ +# **壹证循科技 \- AI智能问答 (AIA) V2.1 产品需求文档** + +**文档状态:** Draft (V2.1) + +**关联原型:** AI问答原型图V11.html + +**架构标准:** Postgres-Only | Ant Design X | Schema Isolation + +**最后更新:** 2026-01-11 + +**本次更新:** 新增移动端适配、深度思考模式、UI/UX 细节规范 + +## **1\. 概述与目标** + +### **1.1 背景** + +原 AIA 模块功能较为单一。V2.0+ 版本旨在构建一个**全流程驱动的科研智能体大厅 (Agent Hub)**,对标 ChatGPT/Gemini 的流畅体验,并深度结合医学科研场景。 + +### **1.2 核心价值** + +1. **流程化引导**:打破“只有一个对话框”的迷茫感,通过 5 个阶段引导用户逐步推进研究。 +2. **专业分工**:不同的智能体加载不同的 System Prompt 和知识库。 +3. **全端体验**:提供桌面端到移动端的无缝衔接,随时随地记录科研灵感。 + +## **2\. 核心业务流程 (User Flow)** + +1. **进入大厅 (Dashboard)**: + * 顶部提供“全局意图搜索框”,用户输入模糊需求。 + * 系统识别意图,自动跳转到对应的智能体。 +2. **选择智能体**: + * 用户也可以直接点击具体的卡片(如“06 样本量计算”)。 +3. **沉浸式工作台 (Workspace)**: + * 进入具体的对话界面。 + * **桌面端**:三栏布局(历史记录 \+ 对话区 \+ 如果有右侧引用栏)。 + * **移动端**:单栏布局,历史记录折叠为抽屉。 +4. **结果产出**: + * AI 展示**思考过程 (Thinking Process)** 后输出最终答案。 + * 用户可一键导出 Word 报告。 + +## **3\. 功能需求详情** + +### **F1. 智能体大厅 (Dashboard) \- 基于 V11 原型** + +#### **F1.1 全局意图路由 (Intent Router)** + +* **UI**:顶部大搜索框,文案“输入研究想法...”。 +* **逻辑**:后端调用 Router Agent 识别意图 \-\> 跳转目标 Agent \-\> 自动填入 Prompt。 + +#### **F1.2 智能体流水线展示 (Agent Pipeline)** + +严格还原 V11 原型的 **5 阶段布局** 和 **3 色视觉体系**(详见原型图)。 + +* **跳转逻辑**: + * **常规卡片**:进入 Chat Workspace。 + * **工具卡片 (09, 10\)**:跳转 /dc/portal 或 /st/dashboard。 + +### **F2. 沉浸式对话工作台 (Chat Workspace)** + +#### **F2.1 界面布局与风格 (UI/UX)** + +* **设计理念**:参考 Gemini 的“大留白”风格,减少分割线,使用卡片投影区分层级。 +* **左侧侧边栏 (Sidebar)**: + * **桌面端**:固定显示,可折叠(图标化)。 + * **移动端**:默认隐藏,通过左上角“汉堡菜单”滑出(Drawer 组件)。 + * **内容**:按时间分组的历史会话(今天、昨天、7天前)。 +* **主对话区 (Main Area)**: + * **Header**:显示当前智能体名称 \+ 状态("思考中..." / "输入中...")。 + * **Input**:底部悬浮,支持多行自动增高。 + +#### **F2.2 核心对话能力增强** + +* **深度思考模式 (Thinking Process)**: + * **需求**:针对“科学问题梳理”等复杂任务,AI 需先输出推理过程。 + * **交互**:输出 \...\ 内容时,前端显示为**可折叠的灰色引用块**,默认展开,生成结束后自动收起,只显示“已深度思考 (耗时 12s)”。 +* **Markdown 渲染增强**: + * **公式**:使用 Katex 渲染 LaTeX 医学公式。 + * **表格**:支持横向滚动,防止手机端撑破布局。 + * **代码**:支持一键复制,语法高亮。 +* **快捷指令 (Slash Commands)**: + * 输入 / 弹出快捷菜单:/润色, /扩写, /翻译, /导出Word。 + +#### **F2.3 附件上传 (Attachment)** + +* **格式**:PDF, Word, TXT, Excel (限制 20MB)。 +* **处理 (Postgres-Only)**: + 1. 上传 OSS \-\> common\_schema.files。 + 2. 后端提取文本 \-\> 截取注入 LLM Context。 + 3. **UI 显示**:在气泡下方显示附件小卡片(图标+文件名)。 + +#### **F2.4 结果操作栏 (Action Bar)** + +每条 AI 回答下方提供微型工具栏(Hover 显示): + +* **复制**:复制 Markdown 源码。 +* **重新生成**:不满意当前回答,触发重试。 +* **导出 Word**:调用 RVW 模块的 docx 导出服务,将当前单轮对话导出为文档。 + +### **F3. 移动端适配 (Mobile Adaptation)** + +#### **F3.1 响应式布局** + +* **断点**:md (768px)。 +* **Dashboard**: + * 隐藏顶部复杂导航,只保留 Logo 和 用户头像。 + * 智能体卡片由 3 列变为 1 列流式布局。 + * 左侧时间轴(Timeline)在移动端需调整样式,避免占用过多水平空间(可改为顶部横向步骤条或简化左侧线条)。 + +#### **F3.2 交互优化** + +* **输入体验**: + * 输入框聚焦时,确保不被 iOS/Android 软键盘遮挡(scrollIntoView)。 + * 发送按钮在移动端需始终可见。 +* **触控反馈**:按钮和卡片增加 active 态背景色变化。 + +## **4\. 数据架构 (Postgres-Only)** + +*(保持不变,复用 V2.0 定义)* + +// schema.prisma (新增字段) + +model AiaMessage { + // ... 现有字段 + thinkingContent String? @db.Text // 存储思维链内容 (\...\) + // ... +} + +## **5\. 配置化开发策略** + +*(保持不变)* + +## **6\. 非功能性需求** + +* **流式体验**:TTFB (首字响应时间) \< 1.5s。 +* **移动端性能**:首屏加载 \< 1s (LCP)。 +* **防抖**:意图搜索框需增加 500ms 防抖,避免频繁请求 Router Agent。 \ No newline at end of file diff --git a/docs/03-业务模块/AIA-AI智能问答/01-需求分析/AI智能问答V2.html b/docs/03-业务模块/AIA-AI智能问答/01-需求分析/AI智能问答V2.html new file mode 100644 index 00000000..bc570ecd --- /dev/null +++ b/docs/03-业务模块/AIA-AI智能问答/01-需求分析/AI智能问答V2.html @@ -0,0 +1,665 @@ + + + + + + AI智能问答 V3.0 - 完整版 + + + + + + + + + +

+ + +
+ + + + +
+
+ 已接入 DeepSeek-V3 +
+

想研究什么?

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

Phase 1: 选题优化

+
+ +
+ 01 +
+ +
+

科学问题梳理

+

从科学问题的清晰度、系统性、可验证性等角度使用科学理论对您的科学问题进行全面的评价。

+
+ +
+ 02 +
+ +
+

PICO 梳理

+

基于科学问题梳理研究对象、干预(暴露)、对照和结局指标,并评价并给出合理化的建议。

+
+ +
+ 03 +
+ +
+

选题评价

+

从创新性、临床价值、科学性和可行性等方面使用科学理论对您的临床问题进行全面的评价。

+
+
+
+ + +
+
+

Phase 2: 方案设计

+
+ +
+ 04 +
+ +
+

观察指标设计

+

基于研究设计和因果关系提供可能的观察指标集。

+
+ +
+ 05 +
+ +
+

病例报告表设计

+

基于研究方案设计梳理观察指标集并给出建议的病例报告表框架。

+
+ +
+ 06 +
+ +
+

样本量计算

+

基于研究阶段和研究设计为研究提供科学合理的样本量估算结果。

+
+ +
+ 07 +
+ +
+

方案撰写 (Pro)

+

基于科学问题、PICOS等信息,给出一个初步的临床研究设计方案。

+
+
+
+ + +
+
+

Phase 3: 方案预评审

+
+ +
+ 08 +
+ +
+

方法学评审

+

从研究问题、研究方案和临床意义方面,对研究进行临床研究方法学的全面评价。

+
+
+
+ + +
+
+

Phase 4: 数据与统计 (工具)

+
+ +
+ + 09 +
+ +
+

数据评价与预处理

+

对现有数据的质量进行自动评价,并给出数据质量报告,包括缺失值、异常值、定量分析等。

+
+ + +
+ + 10 +
+ +
+

智能统计分析

+

内置3条智能研究路径(队列研究、预测模型、RCT研究),以及近百种统计分析工具。

+
+
+
+ + +
+
+

Phase 5: 写作助手

+
+ +
+ 11 +
+ +
+

论文润色

+

结合目标杂志,提供专业化的润色服务,给出合理化的修改建议。

+
+ +
+ 12 +
+ +
+

论文翻译

+

结合目标杂志,提供专业化的翻译并进行润色,给出合理化的修改建议。

+
+
+
+ +
+
+ + + + +
+ + + + + \ No newline at end of file diff --git a/docs/03-业务模块/AIA-AI智能问答/01-需求分析/AI问答原型图V11.html b/docs/03-业务模块/AIA-AI智能问答/01-需求分析/AI问答原型图V11.html new file mode 100644 index 00000000..b940890e --- /dev/null +++ b/docs/03-业务模块/AIA-AI智能问答/01-需求分析/AI问答原型图V11.html @@ -0,0 +1,514 @@ + + + + + + 临床研究平台 - AI医学科研助手 V11 + + + + + + + + + + +
+ + +
+
+
+ +
+

+ 医学研究专属大模型 已接入DeepSeek +

+
+ +
+ + +
+
+ + +
+ + +
+
+
1
+
+
+ +

+ 选题优化智能体 +

+ +
+ +
+
+
+
+ +
+

科学问题梳理

+
+ 01 +
+

+ 从科学问题的清晰度、系统性、可验证性等角度使用科学理论对您的科学问题进行全面的评价。 +

+
+ + +
+
+
+
+ +
+

PICO 梳理

+
+ 02 +
+

+ 基于科学问题梳理研究对象、干预(暴露)、对照和结局指标,并评价并给出合理化的建议。 +

+
+ + +
+
+
+
+ +
+

选题评价

+
+ 03 +
+

+ 从创新性、临床价值、科学性和可行性等方面使用科学理论对您的临床问题进行全面的评价。 +

+
+
+
+ + +
+
+
2
+
+
+ +

+ 方案设计智能体 +

+ +
+ + +
+
+
+
+ +
+

观察指标设计

+
+ 04 +
+

+ 基于研究设计和因果关系提供可能的观察指标集。 +

+
+ + +
+
+
+
+ +
+

病例报告表设计

+
+ 05 +
+

+ 基于研究方案设计梳理观察指标集并给出建议的病例报告表框架。 +

+
+ + +
+
+
+
+ +
+

样本量计算

+
+ 06 +
+

+ 基于研究阶段和研究设计为研究提供科学合理的样本量估算结果。 +

+
+ + +
+
+
+
+ +
+

临床研究方案撰写

+
+ 07 +
+

+ 基于科学问题、PICOS等信息,给出一个初步的临床研究设计方案,请尽量多给一些信息和需求。 +

+
+
+
+ + +
+
+
3
+
+
+ +

+ 方案预评审 +

+ +
+ +
+
+
+
+ +
+

方法学评审智能体

+
+ 08 +
+

+ 从研究问题、研究方案和临床意义方面,对研究进行临床研究方法学的全面评价。 +

+
+
+
+ + +
+
+ +
4
+
+
+ +
+

+ 数据处理与统计分析 +

+ + 工具 +
+ +
+ +
+ + + +
+
+
+ +
+

数据评价与预处理

+
+ 09 +
+

+ 对现有数据的质量进行自动评价,并给出数据质量报告,包括缺失值、异常值、定量分析等。 +

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

智能统计分析

+
+ 10 +
+

+ 内置3条智能研究路径(队列研究、预测模型、RCT研究),以及近百种统计分析工具。 +

+
+
+
+ + +
+
+
5
+
+ +

+ 写作助手 +

+ +
+ +
+
+
+
+ +
+

论文润色

+
+ 11 +
+

+ 结合目标杂志,提供专业化的润色服务,给出合理化的修改建议。 +

+
+ + +
+
+
+
+ +
+

论文翻译

+
+ 12 +
+

+ 结合目标杂志,提供专业化的翻译并进行润色,给出合理化的修改建议。 +

+
+
+
+
+
+ +
+

© 2025 临床研究平台 - 医学研究专属大模型

+
+ + + + \ No newline at end of file diff --git a/docs/03-业务模块/AIA-AI智能问答/04-开发计划/01-AIA-V2.1开发计划.md b/docs/03-业务模块/AIA-AI智能问答/04-开发计划/01-AIA-V2.1开发计划.md new file mode 100644 index 00000000..ccbb5829 --- /dev/null +++ b/docs/03-业务模块/AIA-AI智能问答/04-开发计划/01-AIA-V2.1开发计划.md @@ -0,0 +1,540 @@ +# AIA 模块 V2.1 开发计划 + +> **版本**:V2.1 +> **创建日期**:2026-01-11 +> **计划周期**:约 8-11 天 +> **策略**:重写后端 + 复用数据库 + 新开发前端 +> **关联PRD**:`01-需求分析/AIA模块PRD.md` +> **关联原型**:`01-需求分析/AI智能问答V2.html` + +--- + +## 📋 开发策略概述 + +### 核心决策 + +| 组件 | 策略 | 理由 | +|------|------|------| +| **后端** | 🔴 **重写** | 旧版不符合云原生规范(console.log、未使用平台能力) | +| **数据库** | ✅ **复用** | aia_schema 已完善,字段满足需求 | +| **前端** | 🔴 **新开发** | 复用 shared/components/Chat 通用组件,全新UI | + +### 技术规范 + +| 规范 | 要求 | +|------|------| +| **日志** | 使用 `logger` from `@/common/logging`(禁止 console.log) | +| **存储** | 使用 `storage` from `@/common/storage` | +| **缓存** | 使用 `cache` from `@/common/cache` | +| **队列** | 使用 `jobQueue` from `@/common/jobs` | +| **Prompt** | 使用 `promptService.get()` from `@/common/prompts` | +| **代码位置** | `backend/src/modules/aia/` | +| **API路由** | `/api/v2/aia/*` | + +--- + +## 📊 数据库状态(✅ 已完成) + +### 表结构(3个表) + +``` +aia_schema.projects ✅ 保留 +aia_schema.conversations ✅ 保留 +aia_schema.messages ✅ 保留 + 新增字段 +``` + +### 新增字段(2026-01-11 已迁移) + +| 表 | 字段 | 类型 | 说明 | +|----|------|------|------| +| messages | `thinking_content` | TEXT | 深度思考内容 `...` | +| messages | `attachments` | JSONB | 附件数组(上限5个,单个≤20MB,文本≤30K tokens) | + +### 已删除表 + +| 表 | 原因 | +|----|------| +| `general_conversations` | 功能重叠,使用 conversations.project_id = NULL | +| `general_messages` | 功能重叠 | + +--- + +## 🗓️ 开发阶段 + +### Phase 1: 后端重写(3-4天) + +#### Day 1: 模块骨架 + 核心服务 + +**目标**:搭建模块结构,迁移核心对话服务 + +**任务清单**: + +- [ ] **1.1 创建模块目录结构** + ``` + backend/src/modules/aia/ + ├── controllers/ + │ ├── conversationController.ts + │ ├── agentController.ts + │ └── projectController.ts + ├── services/ + │ ├── conversationService.ts + │ ├── agentService.ts + │ └── projectService.ts + ├── routes/ + │ └── index.ts + ├── types/ + │ └── index.ts + └── index.ts + ``` + +- [ ] **1.2 重写 conversationService** + - 从 legacy 复制核心逻辑 + - 替换 `console.log` → `logger` + - 使用 `prisma.message`(已在 aia_schema) + - 添加 `thinkingContent` 处理逻辑 + - 保持流式输出能力 + +- [ ] **1.3 重写 agentService** + - 改用 `promptService.get()` 获取 Prompt + - 缓存智能体配置(使用 `cache`) + +- [ ] **1.4 注册 v2 路由** + - 注册到 `/api/v2/aia/*` + - 保持 legacy 路由兼容(逐步迁移) + +**验收标准**: +- [ ] 基础对话功能可用 +- [ ] 流式输出正常 +- [ ] 日志输出到 logger + +--- + +#### Day 2: 深度思考 + 附件上传 + +**目标**:实现 V2.1 新增功能 + +**任务清单**: + +- [ ] **2.1 深度思考模式** + - 检测 LLM 输出中的 `...` 标签 + - 提取并存储到 `messages.thinking_content` + - 从 `content` 中移除 think 标签 + - 流式输出时分离 thinking 和 content + +- [ ] **2.2 附件上传服务** + - 使用 `storage.upload()` 上传到 OSS + - 调用 Python 微服务提取文本 + - Token 计数(使用 tiktoken) + - 截断处理(超过 30K tokens) + - 存储附件信息到 `messages.attachments` + +- [ ] **2.3 附件注入 LLM 上下文** + - 组装附件文本到 User Prompt + - 控制总 Token 长度 + +**技术规格**: + +```typescript +// 附件处理配置 +const ATTACHMENT_CONFIG = { + maxCount: 5, // 每条消息最多5个附件 + maxSizePerFile: 20 * 1024 * 1024, // 单个文件20MB + maxTokens: 30000, // 提取文本最多30K tokens + supportedTypes: ['pdf', 'docx', 'txt', 'xlsx'], +}; +``` + +**验收标准**: +- [ ] 深度思考内容正确分离存储 +- [ ] 附件上传成功 +- [ ] 附件文本正确注入 LLM + +--- + +#### Day 3: 意图路由 + 知识库集成 + +**目标**:实现全局意图路由,完善知识库引用 + +**任务清单**: + +- [ ] **3.1 意图路由服务** + - 新建 `intentRouterService.ts` + - 调用 Router Agent 识别意图 + - 返回目标 Agent ID + 预填 Prompt + - 添加 500ms 防抖(前端实现) + +- [ ] **3.2 完善知识库集成** + - 复用 PKB 模块的 RAG 检索 + - 智能引用系统([来源N]) + - 引用清单格式化 + +- [ ] **3.3 API 端点完善** + ``` + POST /api/v2/aia/intent/route # 意图路由 + POST /api/v2/aia/conversations # 创建对话 + GET /api/v2/aia/conversations # 对话列表 + GET /api/v2/aia/conversations/:id # 对话详情 + POST /api/v2/aia/conversations/:id/messages/stream # 发送消息(流式) + POST /api/v2/aia/conversations/:id/attachments # 上传附件 + GET /api/v2/aia/agents # 智能体列表 + GET /api/v2/aia/agents/:id # 智能体详情 + ``` + +**验收标准**: +- [ ] 意图路由正确识别并跳转 +- [ ] 知识库引用正确显示 +- [ ] 所有 API 端点可用 + +--- + +#### Day 4: 测试 + 文档 + +**目标**:完成后端测试和文档 + +**任务清单**: + +- [ ] **4.1 单元测试** + - conversationService 测试 + - 深度思考解析测试 + - 附件处理测试 + +- [ ] **4.2 集成测试** + - 完整对话流程 + - 附件上传流程 + - 知识库检索流程 + +- [ ] **4.3 API 文档** + - 更新 REST Client 测试文件 + - 编写 API 使用示例 + +**验收标准**: +- [ ] 测试覆盖核心功能 +- [ ] API 文档完整 + +--- + +### Phase 2: 前端开发(5-7天) + +#### Day 5-6: 智能体大厅(Dashboard) + +**目标**:实现首页智能体大厅 + +**任务清单**: + +- [ ] **5.1 创建模块目录结构** + ``` + frontend-v2/src/modules/aia/ + ├── pages/ + │ ├── Dashboard.tsx # 智能体大厅 + │ └── Workspace.tsx # 对话工作台 + ├── components/ + │ ├── AgentPipeline.tsx # 5阶段流水线 + │ ├── AgentCard.tsx # 智能体卡片 + │ ├── IntentSearch.tsx # 意图搜索框 + │ ├── ConversationList.tsx # 历史会话列表 + │ ├── ThinkingBlock.tsx # 深度思考折叠块 + │ ├── AttachmentUpload.tsx # 附件上传 + │ ├── AttachmentCard.tsx # 附件卡片 + │ └── SlashCommands.tsx # 快捷指令 + ├── hooks/ + │ ├── useConversation.ts + │ ├── useAgents.ts + │ └── useIntentRouter.ts + ├── api/ + │ └── index.ts + ├── types/ + │ └── index.ts + └── index.tsx + ``` + +- [ ] **5.2 智能体流水线(AgentPipeline)** + - 5阶段布局(严格还原 V11 原型) + - 3色视觉体系 + - 卡片点击跳转 Workspace + - 工具卡片跳转外部模块(DC/ST) + +- [ ] **5.3 意图搜索框(IntentSearch)** + - 顶部大搜索框 + - 500ms 防抖 + - 调用意图路由 API + - 自动跳转目标 Agent + +**验收标准**: +- [ ] 5阶段流水线正确展示 +- [ ] 意图搜索功能可用 +- [ ] 与原型图一致 + +--- + +#### Day 7-8: 对话工作台(Workspace) + +**目标**:实现沉浸式对话界面 + +**任务清单**: + +- [ ] **7.1 工作台布局** + - Gemini 风格(大留白、少分割线) + - 左侧侧边栏(历史会话,可折叠) + - 主对话区(Header + 消息列表 + 输入框) + +- [ ] **7.2 复用 Chat 通用组件** + ```tsx + import { ChatContainer } from '@/shared/components/Chat'; + + + ``` + +- [ ] **7.3 深度思考折叠块(ThinkingBlock)** + - 可折叠灰色引用块 + - 生成中展开,完成后自动收起 + - 显示"已深度思考 (耗时 Xs)" + +- [ ] **7.4 附件上传组件** + - 支持拖拽上传 + - 文件类型/大小校验 + - 上传进度显示 + - 消息气泡下方附件卡片 + +- [ ] **7.5 历史会话列表** + - 按时间分组(今天、昨天、7天前) + - 桌面端固定显示 + - 移动端抽屉滑出 + +**验收标准**: +- [ ] Gemini 风格 UI +- [ ] 深度思考正确展示 +- [ ] 附件上传完整流程 +- [ ] 历史会话切换 + +--- + +#### Day 9: Markdown 增强 + 快捷指令 + +**目标**:增强对话体验 + +**任务清单**: + +- [ ] **9.1 Markdown 渲染增强** + - KaTeX 公式渲染(医学公式) + - 表格横向滚动 + - 代码块语法高亮 + 一键复制 + +- [ ] **9.2 快捷指令(SlashCommands)** + - 输入 `/` 弹出菜单 + - 支持:/润色, /扩写, /翻译, /导出Word + - 键盘导航 + +- [ ] **9.3 结果操作栏** + - Hover 显示工具栏 + - 复制(Markdown 源码) + - 重新生成 + - 导出 Word(调用 RVW 导出服务) + +**验收标准**: +- [ ] 公式正确渲染 +- [ ] 快捷指令可用 +- [ ] 操作栏功能完整 + +--- + +#### Day 10: 移动端适配 + +**目标**:响应式布局适配 + +**任务清单**: + +- [ ] **10.1 Dashboard 移动端** + - 隐藏复杂导航 + - 卡片单列流式布局 + - 时间轴样式调整 + +- [ ] **10.2 Workspace 移动端** + - 侧边栏改为抽屉 + - 输入框键盘适配(scrollIntoView) + - 发送按钮始终可见 + +- [ ] **10.3 触控优化** + - 按钮 active 态 + - 触控区域优化 + +**验收标准**: +- [ ] md (768px) 断点响应正确 +- [ ] 移动端交互流畅 + +--- + +#### Day 11: 集成测试 + 优化 + +**目标**:完成整体测试和优化 + +**任务清单**: + +- [ ] **11.1 端到端测试** + - 完整对话流程 + - 附件上传流程 + - 深度思考流程 + - 知识库引用流程 + +- [ ] **11.2 性能优化** + - TTFB < 1.5s + - 移动端 LCP < 1s + - 意图搜索防抖 + +- [ ] **11.3 Bug 修复** + +- [ ] **11.4 文档更新** + - 更新模块状态文档 + - 更新系统当前状态 + +**验收标准**: +- [ ] 所有功能正常 +- [ ] 性能达标 +- [ ] 文档完整 + +--- + +## 📁 文件清单 + +### 后端新增文件 + +``` +backend/src/modules/aia/ +├── controllers/ +│ ├── conversationController.ts # ~300行 +│ ├── agentController.ts # ~150行 +│ └── projectController.ts # ~200行 +├── services/ +│ ├── conversationService.ts # ~500行(核心) +│ ├── agentService.ts # ~200行 +│ ├── projectService.ts # ~150行 +│ ├── intentRouterService.ts # ~100行(新) +│ └── attachmentService.ts # ~200行(新) +├── routes/ +│ └── index.ts # ~100行 +├── types/ +│ └── index.ts # ~100行 +└── index.ts # ~20行 + +预计总计:~2000行 +``` + +### 前端新增文件 + +``` +frontend-v2/src/modules/aia/ +├── pages/ +│ ├── Dashboard.tsx # ~400行 +│ └── Workspace.tsx # ~500行 +├── components/ +│ ├── AgentPipeline.tsx # ~300行 +│ ├── AgentCard.tsx # ~100行 +│ ├── IntentSearch.tsx # ~150行 +│ ├── ConversationList.tsx # ~200行 +│ ├── ThinkingBlock.tsx # ~100行 +│ ├── AttachmentUpload.tsx # ~200行 +│ ├── AttachmentCard.tsx # ~80行 +│ └── SlashCommands.tsx # ~150行 +├── hooks/ +│ ├── useConversation.ts # ~150行 +│ ├── useAgents.ts # ~100行 +│ └── useIntentRouter.ts # ~80行 +├── api/ +│ └── index.ts # ~200行 +├── types/ +│ └── index.ts # ~100行 +└── index.tsx # ~50行 + +预计总计:~2900行 +``` + +--- + +## 🔗 依赖关系 + +### 后端依赖 + +| 依赖 | 来源 | 说明 | +|------|------|------| +| `logger` | `@/common/logging` | 日志服务 | +| `storage` | `@/common/storage` | OSS存储 | +| `cache` | `@/common/cache` | Redis/PG缓存 | +| `jobQueue` | `@/common/jobs` | 异步任务 | +| `promptService` | `@/common/prompts` | Prompt管理 | +| `LLMFactory` | `@/common/llm` | LLM适配器 | +| `prisma` | `@/config/database` | 数据库 | +| `ExtractionClient` | `@/clients` | 文档提取 | +| `TokenService` | `@/services` | Token计数 | + +### 前端依赖 + +| 依赖 | 来源 | 说明 | +|------|------|------| +| `ChatContainer` | `@/shared/components/Chat` | 通用对话组件 | +| `Ant Design` | `antd` | UI组件库 | +| `Ant Design X` | `@ant-design/x` | AI对话组件 | +| `KaTeX` | `katex` | 公式渲染 | +| `react-markdown` | `react-markdown` | Markdown渲染 | + +--- + +## 📈 风险评估 + +| 风险 | 概率 | 影响 | 缓解措施 | +|------|------|------|---------| +| 流式输出兼容性 | 中 | 高 | 复用已验证的 SSE 代码 | +| 附件提取超时 | 中 | 中 | 使用 jobQueue 异步处理 | +| 移动端适配问题 | 低 | 中 | 提前规划断点和布局 | +| Prompt管理服务未就绪 | 低 | 中 | 可临时回退到文件读取 | + +--- + +## ✅ 验收标准 + +### 功能验收 + +- [ ] 智能体大厅完整展示 +- [ ] 意图搜索正确路由 +- [ ] 多轮对话正常 +- [ ] 流式输出流畅 +- [ ] 深度思考正确折叠 +- [ ] 附件上传完整 +- [ ] 知识库引用正确 +- [ ] 快捷指令可用 +- [ ] 移动端适配正常 + +### 性能验收 + +- [ ] TTFB < 1.5s +- [ ] 移动端 LCP < 1s +- [ ] 意图搜索响应 < 500ms + +### 质量验收 + +- [ ] 无 console.log(使用 logger) +- [ ] 代码符合云原生规范 +- [ ] API 文档完整 +- [ ] 单元测试覆盖核心功能 + +--- + +## 📝 更新日志 + +| 日期 | 版本 | 内容 | +|------|------|------| +| 2026-01-11 | V1.0 | 创建开发计划 | + +--- + +**计划制定人**:AI Assistant +**审核人**:待定 +**批准人**:待定 + + + + diff --git a/docs/03-业务模块/AIA-AI智能问答/04-开发计划/02-后端API设计.md b/docs/03-业务模块/AIA-AI智能问答/04-开发计划/02-后端API设计.md new file mode 100644 index 00000000..680f6181 --- /dev/null +++ b/docs/03-业务模块/AIA-AI智能问答/04-开发计划/02-后端API设计.md @@ -0,0 +1,568 @@ +# AIA V2.1 后端 API 设计 + +> **版本**:V2.1 +> **创建日期**:2026-01-11 +> **基础路径**:`/api/v2/aia` + +--- + +## 📋 API 概览 + +| 方法 | 路径 | 描述 | 认证 | +|------|------|------|------| +| GET | `/agents` | 获取智能体列表 | ✅ | +| GET | `/agents/:id` | 获取智能体详情 | ✅ | +| POST | `/intent/route` | 意图路由 | ✅ | +| GET | `/conversations` | 获取对话列表 | ✅ | +| POST | `/conversations` | 创建对话 | ✅ | +| GET | `/conversations/:id` | 获取对话详情 | ✅ | +| DELETE | `/conversations/:id` | 删除对话 | ✅ | +| POST | `/conversations/:id/messages/stream` | 发送消息(流式) | ✅ | +| POST | `/conversations/:id/attachments` | 上传附件 | ✅ | +| GET | `/projects` | 获取项目列表 | ✅ | +| GET | `/projects/:id` | 获取项目详情 | ✅ | + +--- + +## 🔐 认证 + +所有 API 需要在请求头中携带 JWT Token: + +``` +Authorization: Bearer +``` + +--- + +## 📖 API 详细定义 + +### 1. 智能体相关 + +#### 1.1 获取智能体列表 + +```http +GET /api/v2/aia/agents +``` + +**查询参数**: + +| 参数 | 类型 | 必填 | 描述 | +|------|------|------|------| +| stage | string | 否 | 筛选阶段:`design`, `data`, `analysis`, `write`, `publish` | + +**响应**: + +```json +{ + "code": 0, + "data": { + "agents": [ + { + "id": "research-design", + "name": "科研设计小助手", + "description": "帮助您完成研究方案设计、文献检索、方法学指导", + "icon": "🔬", + "stage": "design", + "color": "#3B82F6", + "knowledgeBaseId": "kb-001", + "isTool": false + }, + { + "id": "dc-tool", + "name": "数据采集工具", + "description": "跳转到数据采集模块", + "icon": "📊", + "stage": "data", + "color": "#8B5CF6", + "targetModule": "/data-collection", + "isTool": true + } + ] + } +} +``` + +#### 1.2 获取智能体详情 + +```http +GET /api/v2/aia/agents/:id +``` + +**响应**: + +```json +{ + "code": 0, + "data": { + "id": "research-design", + "name": "科研设计小助手", + "description": "帮助您完成研究方案设计、文献检索、方法学指导", + "icon": "🔬", + "stage": "design", + "color": "#3B82F6", + "knowledgeBaseId": "kb-001", + "systemPrompt": "你是一个专业的医学科研设计专家...", + "welcomeMessage": "您好!我是科研设计小助手,我可以帮您:\n- 设计研究方案\n- 检索相关文献\n- 指导研究方法", + "suggestedQuestions": [ + "如何设计一个RCT研究?", + "帮我检索近5年糖尿病研究文献", + "什么情况下使用倾向性评分匹配?" + ] + } +} +``` + +--- + +### 2. 意图路由 + +#### 2.1 智能意图识别 + +```http +POST /api/v2/aia/intent/route +``` + +**请求体**: + +```json +{ + "query": "帮我分析一下这份数据" +} +``` + +**响应**: + +```json +{ + "code": 0, + "data": { + "agentId": "data-analysis", + "agentName": "统计分析小助手", + "confidence": 0.92, + "prefillPrompt": "请帮我分析这份数据,包括描述性统计和相关性分析" + } +} +``` + +--- + +### 3. 对话管理 + +#### 3.1 获取对话列表 + +```http +GET /api/v2/aia/conversations +``` + +**查询参数**: + +| 参数 | 类型 | 必填 | 描述 | +|------|------|------|------| +| agentId | string | 否 | 按智能体筛选 | +| projectId | string | 否 | 按项目筛选(NULL 表示通用对话) | +| page | number | 否 | 页码,默认 1 | +| pageSize | number | 否 | 每页数量,默认 20 | + +**响应**: + +```json +{ + "code": 0, + "data": { + "conversations": [ + { + "id": "conv-001", + "title": "RCT研究设计咨询", + "agentId": "research-design", + "agentName": "科研设计小助手", + "projectId": null, + "messageCount": 12, + "lastMessage": "好的,我来帮您设计研究方案...", + "createdAt": "2026-01-11T10:00:00Z", + "updatedAt": "2026-01-11T12:30:00Z" + } + ], + "pagination": { + "total": 25, + "page": 1, + "pageSize": 20, + "totalPages": 2 + } + } +} +``` + +#### 3.2 创建对话 + +```http +POST /api/v2/aia/conversations +``` + +**请求体**: + +```json +{ + "agentId": "research-design", + "projectId": null, + "title": "新对话" +} +``` + +**响应**: + +```json +{ + "code": 0, + "data": { + "id": "conv-002", + "title": "新对话", + "agentId": "research-design", + "projectId": null, + "createdAt": "2026-01-11T14:00:00Z" + } +} +``` + +#### 3.3 获取对话详情(含历史消息) + +```http +GET /api/v2/aia/conversations/:id +``` + +**查询参数**: + +| 参数 | 类型 | 必填 | 描述 | +|------|------|------|------| +| limit | number | 否 | 获取最近N条消息,默认 50 | + +**响应**: + +```json +{ + "code": 0, + "data": { + "id": "conv-001", + "title": "RCT研究设计咨询", + "agentId": "research-design", + "agentName": "科研设计小助手", + "projectId": null, + "createdAt": "2026-01-11T10:00:00Z", + "updatedAt": "2026-01-11T12:30:00Z", + "messages": [ + { + "id": "msg-001", + "role": "user", + "content": "帮我设计一个关于糖尿病的RCT研究", + "attachments": [], + "createdAt": "2026-01-11T10:00:00Z" + }, + { + "id": "msg-002", + "role": "assistant", + "content": "好的,我来帮您设计一个糖尿病RCT研究方案...", + "thinkingContent": "用户想设计RCT研究,需要考虑:1)研究目的 2)入排标准 3)样本量 4)随机化方法 5)盲法 6)结局指标...", + "model": "deepseek-v3", + "tokens": 1250, + "createdAt": "2026-01-11T10:00:30Z" + } + ] + } +} +``` + +#### 3.4 删除对话 + +```http +DELETE /api/v2/aia/conversations/:id +``` + +**响应**: + +```json +{ + "code": 0, + "data": { + "deleted": true + } +} +``` + +--- + +### 4. 消息发送(流式) + +#### 4.1 发送消息并获取流式响应 + +```http +POST /api/v2/aia/conversations/:id/messages/stream +``` + +**请求头**: + +``` +Content-Type: application/json +Accept: text/event-stream +``` + +**请求体**: + +```json +{ + "content": "帮我分析这份数据", + "attachmentIds": ["att-001", "att-002"], + "enableDeepThinking": true +} +``` + +**响应(SSE 格式)**: + +``` +event: thinking_start +data: {} + +event: thinking_delta +data: {"content": "用户上传了数据文件,需要"} + +event: thinking_delta +data: {"content": "进行描述性统计分析..."} + +event: thinking_end +data: {"duration": 3200} + +event: message_start +data: {"id": "msg-003"} + +event: delta +data: {"content": "根据您上传的数据,"} + +event: delta +data: {"content": "我来为您进行分析..."} + +event: message_end +data: {"id": "msg-003", "tokens": 850, "model": "deepseek-v3"} + +event: done +data: {} +``` + +**SSE 事件类型**: + +| 事件 | 描述 | 数据格式 | +|------|------|---------| +| `thinking_start` | 开始深度思考 | `{}` | +| `thinking_delta` | 思考内容片段 | `{"content": "..."}` | +| `thinking_end` | 思考结束 | `{"duration": number}` | +| `message_start` | 开始生成回复 | `{"id": "..."}` | +| `delta` | 回复内容片段 | `{"content": "..."}` | +| `message_end` | 回复结束 | `{"id": "...", "tokens": number, "model": "..."}` | +| `error` | 发生错误 | `{"code": "...", "message": "..."}` | +| `done` | 流结束 | `{}` | + +--- + +### 5. 附件上传 + +#### 5.1 上传附件 + +```http +POST /api/v2/aia/conversations/:id/attachments +``` + +**请求头**: + +``` +Content-Type: multipart/form-data +``` + +**请求体(FormData)**: + +| 字段 | 类型 | 必填 | 描述 | +|------|------|------|------| +| file | File | 是 | 文件(PDF/Word/TXT/Excel,最大20MB) | + +**响应**: + +```json +{ + "code": 0, + "data": { + "id": "att-001", + "filename": "研究数据.xlsx", + "mimeType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "size": 1024000, + "ossUrl": "https://oss.example.com/attachments/att-001.xlsx", + "textExtracted": true, + "tokenCount": 15000, + "truncated": false, + "createdAt": "2026-01-11T14:00:00Z" + } +} +``` + +**错误码**: + +| 错误码 | 描述 | +|--------|------| +| `ATTACHMENT_TOO_LARGE` | 文件超过 20MB | +| `ATTACHMENT_TYPE_NOT_SUPPORTED` | 不支持的文件类型 | +| `ATTACHMENT_LIMIT_EXCEEDED` | 附件数量超过上限(5个) | +| `TEXT_EXTRACTION_FAILED` | 文本提取失败 | + +--- + +### 6. 项目管理 + +#### 6.1 获取项目列表 + +```http +GET /api/v2/aia/projects +``` + +**响应**: + +```json +{ + "code": 0, + "data": { + "projects": [ + { + "id": "proj-001", + "name": "糖尿病研究项目", + "description": "2型糖尿病患者生活方式干预研究", + "conversationCount": 5, + "createdAt": "2026-01-01T10:00:00Z", + "updatedAt": "2026-01-11T12:00:00Z" + } + ] + } +} +``` + +--- + +## 🔧 错误处理 + +### 错误响应格式 + +```json +{ + "code": -1, + "error": { + "code": "CONVERSATION_NOT_FOUND", + "message": "对话不存在" + } +} +``` + +### 通用错误码 + +| 错误码 | HTTP 状态码 | 描述 | +|--------|------------|------| +| `UNAUTHORIZED` | 401 | 未授权 | +| `FORBIDDEN` | 403 | 无权限 | +| `NOT_FOUND` | 404 | 资源不存在 | +| `VALIDATION_ERROR` | 400 | 参数验证失败 | +| `INTERNAL_ERROR` | 500 | 服务器内部错误 | +| `LLM_ERROR` | 500 | LLM 调用失败 | +| `RATE_LIMITED` | 429 | 请求过于频繁 | + +--- + +## 📊 数据模型 + +### Attachment(附件) + +```typescript +interface Attachment { + id: string; // 附件ID + filename: string; // 原始文件名 + mimeType: string; // MIME 类型 + size: number; // 文件大小(字节) + ossUrl: string; // OSS 存储地址 + textContent?: string; // 提取的文本内容(存储时截断) + tokenCount: number; // 文本 Token 数 + truncated: boolean; // 是否被截断(超过30K tokens) + createdAt: string; // 创建时间 +} +``` + +### Message(消息) + +```typescript +interface Message { + id: string; + conversationId: string; + role: 'user' | 'assistant'; + content: string; + thinkingContent?: string; // 深度思考内容 + attachments?: Attachment[]; + model?: string; + tokens?: number; + isPinned: boolean; + createdAt: string; +} +``` + +--- + +## 🧪 测试示例(REST Client) + +```http +### 获取智能体列表 +GET {{baseUrl}}/api/v2/aia/agents +Authorization: Bearer {{token}} + +### 意图路由 +POST {{baseUrl}}/api/v2/aia/intent/route +Authorization: Bearer {{token}} +Content-Type: application/json + +{ + "query": "帮我分析数据" +} + +### 创建对话 +POST {{baseUrl}}/api/v2/aia/conversations +Authorization: Bearer {{token}} +Content-Type: application/json + +{ + "agentId": "research-design", + "title": "测试对话" +} + +### 发送消息(流式) +POST {{baseUrl}}/api/v2/aia/conversations/{{conversationId}}/messages/stream +Authorization: Bearer {{token}} +Content-Type: application/json +Accept: text/event-stream + +{ + "content": "帮我设计一个RCT研究", + "enableDeepThinking": true +} + +### 上传附件 +POST {{baseUrl}}/api/v2/aia/conversations/{{conversationId}}/attachments +Authorization: Bearer {{token}} +Content-Type: multipart/form-data; boundary=----WebKitFormBoundary + +------WebKitFormBoundary +Content-Disposition: form-data; name="file"; filename="data.xlsx" +Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet + +< ./data.xlsx +------WebKitFormBoundary-- +``` + +--- + +## 📝 更新日志 + +| 日期 | 版本 | 内容 | +|------|------|------| +| 2026-01-11 | V1.0 | 创建 API 设计文档 | + + + + diff --git a/docs/03-业务模块/AIA-AI智能问答/04-开发计划/03-前端组件设计.md b/docs/03-业务模块/AIA-AI智能问答/04-开发计划/03-前端组件设计.md new file mode 100644 index 00000000..09c277a4 --- /dev/null +++ b/docs/03-业务模块/AIA-AI智能问答/04-开发计划/03-前端组件设计.md @@ -0,0 +1,883 @@ +# AIA V2.1 前端组件设计 + +> **版本**:V2.1 +> **创建日期**:2026-01-11 +> **技术栈**:React 19 + TypeScript 5 + Ant Design 6 + Ant Design X 2.1 + +--- + +## 📁 模块结构 + +``` +frontend-v2/src/modules/aia/ +├── pages/ +│ ├── Dashboard.tsx # 智能体大厅(首页) +│ └── Workspace.tsx # 对话工作台 +├── components/ +│ ├── AgentPipeline/ +│ │ ├── index.tsx # 5阶段流水线容器 +│ │ ├── StageColumn.tsx # 单阶段列 +│ │ └── AgentCard.tsx # 智能体卡片 +│ ├── IntentSearch/ +│ │ ├── index.tsx # 意图搜索框 +│ │ └── SuggestionDropdown.tsx # 建议下拉框 +│ ├── ConversationList/ +│ │ ├── index.tsx # 历史会话列表 +│ │ └── ConversationItem.tsx # 会话项 +│ ├── MessageList/ +│ │ ├── index.tsx # 消息列表 +│ │ ├── UserMessage.tsx # 用户消息 +│ │ ├── AssistantMessage.tsx # AI回复 +│ │ └── ThinkingBlock.tsx # 深度思考折叠块 +│ ├── Attachment/ +│ │ ├── AttachmentUpload.tsx # 附件上传 +│ │ ├── AttachmentCard.tsx # 附件卡片 +│ │ └── AttachmentPreview.tsx # 附件预览 +│ ├── SlashCommands/ +│ │ └── index.tsx # 快捷指令菜单 +│ └── ActionBar/ +│ └── index.tsx # 结果操作栏 +├── hooks/ +│ ├── useConversation.ts # 对话管理 +│ ├── useAgents.ts # 智能体数据 +│ ├── useIntentRouter.ts # 意图路由 +│ ├── useStreamMessage.ts # 流式消息 +│ └── useAttachment.ts # 附件上传 +├── api/ +│ └── index.ts # API 封装 +├── types/ +│ └── index.ts # TypeScript 类型 +├── styles/ +│ ├── dashboard.module.css # Dashboard 样式 +│ └── workspace.module.css # Workspace 样式 +└── index.tsx # 模块入口 + 路由 +``` + +--- + +## 🎨 设计规范 + +### 色彩系统 + +```css +:root { + /* 5阶段流水线主题色 */ + --stage-design: #3B82F6; /* 蓝色 - 研究设计 */ + --stage-data: #8B5CF6; /* 紫色 - 数据采集 */ + --stage-analysis: #10B981; /* 绿色 - 统计分析 */ + --stage-write: #F59E0B; /* 橙色 - 论文撰写 */ + --stage-publish: #EF4444; /* 红色 - 成果发布 */ + + /* 功能色 */ + --ai-assistant: #6366F1; /* AI助手主色 */ + --thinking-bg: #F3F4F6; /* 思考块背景 */ + --thinking-border: #E5E7EB; /* 思考块边框 */ + + /* Gemini 风格 */ + --bg-primary: #FFFFFF; + --bg-secondary: #F9FAFB; + --text-primary: #111827; + --text-secondary: #6B7280; + --border-light: #E5E7EB; +} +``` + +### 间距系统 + +```css +/* 遵循 8px 网格 */ +--spacing-xs: 4px; +--spacing-sm: 8px; +--spacing-md: 16px; +--spacing-lg: 24px; +--spacing-xl: 32px; +--spacing-2xl: 48px; +``` + +### 断点 + +```css +/* 移动优先 */ +--breakpoint-sm: 640px; /* 手机 */ +--breakpoint-md: 768px; /* 平板 */ +--breakpoint-lg: 1024px; /* 桌面 */ +--breakpoint-xl: 1280px; /* 大屏 */ +``` + +--- + +## 📄 页面设计 + +### 1. Dashboard(智能体大厅) + +```tsx +// pages/Dashboard.tsx + +import { IntentSearch } from '../components/IntentSearch'; +import { AgentPipeline } from '../components/AgentPipeline'; + +export const Dashboard: React.FC = () => { + return ( +
+ {/* 顶部意图搜索框 */} +
+

AI 智能助手

+

有什么可以帮助您的?

+ +
+ + {/* 5阶段智能体流水线 */} +
+ +
+
+ ); +}; +``` + +**布局特点**: +- 顶部居中大搜索框 +- 5阶段流水线横向平铺(桌面)/ 纵向滚动(移动) +- Gemini 风格大留白 + +--- + +### 2. Workspace(对话工作台) + +```tsx +// pages/Workspace.tsx + +import { ChatContainer } from '@/shared/components/Chat'; +import { ConversationList } from '../components/ConversationList'; +import { ThinkingBlock } from '../components/MessageList/ThinkingBlock'; +import { AttachmentUpload } from '../components/Attachment/AttachmentUpload'; + +export const Workspace: React.FC = () => { + const { conversationId } = useParams(); + const { conversation, messages, sendMessage } = useConversation(conversationId); + + return ( +
+ {/* 左侧边栏 - 历史会话 */} + + + {/* 主对话区 */} +
+ {/* 对话头部 */} +
+ +

{conversation?.agent?.name}

+
+ + {/* 消息列表 - 复用通用 Chat 组件 */} + ( +
+ {/* 深度思考块 */} + {msg.thinkingContent && ( + + )} + {/* 消息内容 */} + + {/* 附件卡片 */} + {msg.attachments?.map(att => ( + + ))} +
+ )} + inputFooter={} + /> +
+
+ ); +}; +``` + +--- + +## 🧩 组件详细设计 + +### 1. AgentPipeline(5阶段流水线) + +```tsx +// components/AgentPipeline/index.tsx + +interface AgentPipelineProps { + onAgentClick: (agentId: string) => void; +} + +export const AgentPipeline: React.FC = ({ onAgentClick }) => { + const { agents } = useAgents(); + + const stages = [ + { key: 'design', title: '研究设计', color: 'var(--stage-design)' }, + { key: 'data', title: '数据采集', color: 'var(--stage-data)' }, + { key: 'analysis', title: '统计分析', color: 'var(--stage-analysis)' }, + { key: 'write', title: '论文撰写', color: 'var(--stage-write)' }, + { key: 'publish', title: '成果发布', color: 'var(--stage-publish)' }, + ]; + + return ( +
+ {stages.map((stage, index) => ( + a.stage === stage.key)} + onAgentClick={onAgentClick} + showConnector={index < stages.length - 1} + /> + ))} +
+ ); +}; +``` + +**样式特点**: +```css +.pipeline { + display: flex; + gap: var(--spacing-lg); + overflow-x: auto; + padding: var(--spacing-xl) 0; +} + +/* 移动端纵向布局 */ +@media (max-width: 768px) { + .pipeline { + flex-direction: column; + overflow-x: visible; + } +} +``` + +--- + +### 2. IntentSearch(意图搜索框) + +```tsx +// components/IntentSearch/index.tsx + +export const IntentSearch: React.FC = () => { + const [query, setQuery] = useState(''); + const [suggestions, setSuggestions] = useState([]); + const { routeIntent, isLoading } = useIntentRouter(); + const navigate = useNavigate(); + + // 500ms 防抖 + const debouncedQuery = useDebouncedValue(query, 500); + + useEffect(() => { + if (debouncedQuery.length >= 2) { + routeIntent(debouncedQuery).then(setSuggestions); + } + }, [debouncedQuery]); + + const handleSelect = (suggestion: IntentSuggestion) => { + // 跳转到对应智能体 + navigate(`/aia/workspace?agent=${suggestion.agentId}&prompt=${encodeURIComponent(suggestion.prefillPrompt)}`); + }; + + return ( +
+ setQuery(e.target.value)} + loading={isLoading} + className={styles.searchInput} + /> + + {suggestions.length > 0 && ( + + )} +
+ ); +}; +``` + +--- + +### 3. ThinkingBlock(深度思考折叠块) + +```tsx +// components/MessageList/ThinkingBlock.tsx + +interface ThinkingBlockProps { + content: string; + duration?: number; // 思考耗时(秒) + isStreaming?: boolean; // 是否正在生成 +} + +export const ThinkingBlock: React.FC = ({ + content, + duration, + isStreaming = false, +}) => { + // 生成中展开,完成后自动收起 + const [expanded, setExpanded] = useState(isStreaming); + + useEffect(() => { + if (!isStreaming && expanded) { + // 完成后 1.5s 自动收起 + const timer = setTimeout(() => setExpanded(false), 1500); + return () => clearTimeout(timer); + } + }, [isStreaming]); + + return ( +
+
setExpanded(!expanded)} + > + + {isStreaming ? : } + + + {isStreaming ? '正在深度思考...' : `已深度思考 (耗时 ${duration?.toFixed(1)}s)`} + + + {expanded ? : } + +
+ + {expanded && ( +
+ + {content} + +
+ )} +
+ ); +}; +``` + +**样式**: +```css +.thinkingBlock { + background: var(--thinking-bg); + border: 1px solid var(--thinking-border); + border-radius: 8px; + margin-bottom: var(--spacing-md); +} + +.header { + display: flex; + align-items: center; + padding: var(--spacing-sm) var(--spacing-md); + cursor: pointer; + user-select: none; +} + +.content { + padding: var(--spacing-md); + padding-top: 0; + font-size: 13px; + line-height: 1.6; + color: var(--text-secondary); +} +``` + +--- + +### 4. AttachmentUpload(附件上传) + +```tsx +// components/Attachment/AttachmentUpload.tsx + +interface AttachmentUploadProps { + conversationId: string; + onUploadComplete: (attachment: Attachment) => void; + maxCount?: number; // 默认 5 +} + +export const AttachmentUpload: React.FC = ({ + conversationId, + onUploadComplete, + maxCount = 5, +}) => { + const { uploadFile, uploading, progress } = useAttachment(); + const [attachments, setAttachments] = useState([]); + + const handleUpload = async (file: File) => { + if (attachments.length >= maxCount) { + message.error(`最多上传 ${maxCount} 个附件`); + return false; + } + + // 文件类型校验 + const allowedTypes = ['application/pdf', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'text/plain', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet']; + if (!allowedTypes.includes(file.type)) { + message.error('仅支持 PDF、Word、TXT、Excel 文件'); + return false; + } + + // 文件大小校验(20MB) + if (file.size > 20 * 1024 * 1024) { + message.error('文件大小不能超过 20MB'); + return false; + } + + const attachment = await uploadFile(conversationId, file); + setAttachments([...attachments, attachment]); + onUploadComplete(attachment); + return false; // 阻止默认上传 + }; + + return ( +
+ + + + + {/* 已上传附件列表 */} +
+ {attachments.map(att => ( + { + setAttachments(attachments.filter(a => a.id !== att.id)); + }} + /> + ))} +
+ + {/* 上传进度 */} + {uploading && ( + + )} +
+ ); +}; +``` + +--- + +### 5. SlashCommands(快捷指令) + +```tsx +// components/SlashCommands/index.tsx + +interface SlashCommandsProps { + onSelect: (command: SlashCommand) => void; + onClose: () => void; +} + +const commands: SlashCommand[] = [ + { key: 'polish', icon: '✨', label: '润色', description: '优化文本表达' }, + { key: 'expand', icon: '📝', label: '扩写', description: '扩展内容细节' }, + { key: 'translate', icon: '🌐', label: '翻译', description: '中英互译' }, + { key: 'export', icon: '📄', label: '导出Word', description: '导出为 Word 文档' }, +]; + +export const SlashCommands: React.FC = ({ + onSelect, + onClose, +}) => { + const [selectedIndex, setSelectedIndex] = useState(0); + + // 键盘导航 + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + switch (e.key) { + case 'ArrowUp': + e.preventDefault(); + setSelectedIndex(i => Math.max(0, i - 1)); + break; + case 'ArrowDown': + e.preventDefault(); + setSelectedIndex(i => Math.min(commands.length - 1, i + 1)); + break; + case 'Enter': + e.preventDefault(); + onSelect(commands[selectedIndex]); + break; + case 'Escape': + onClose(); + break; + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [selectedIndex]); + + return ( +
+ {commands.map((cmd, index) => ( +
onSelect(cmd)} + onMouseEnter={() => setSelectedIndex(index)} + > + {cmd.icon} +
+
{cmd.label}
+
{cmd.description}
+
+
+ ))} +
+ ); +}; +``` + +--- + +### 6. ActionBar(结果操作栏) + +```tsx +// components/ActionBar/index.tsx + +interface ActionBarProps { + message: Message; + onCopy: () => void; + onRegenerate: () => void; + onExport: () => void; +} + +export const ActionBar: React.FC = ({ + message, + onCopy, + onRegenerate, + onExport, +}) => { + return ( +
+ +
+ ); +}; +``` + +--- + +## 🎣 Hooks 设计 + +### useConversation + +```typescript +// hooks/useConversation.ts + +interface UseConversationReturn { + conversation: Conversation | null; + messages: Message[]; + isLoading: boolean; + sendMessage: (content: string, attachmentIds?: string[]) => Promise; + regenerate: (messageId: string) => Promise; + deleteConversation: () => Promise; +} + +export function useConversation(conversationId?: string): UseConversationReturn { + const [conversation, setConversation] = useState(null); + const [messages, setMessages] = useState([]); + const [isLoading, setIsLoading] = useState(false); + + // 加载对话 + useEffect(() => { + if (conversationId) { + api.getConversation(conversationId).then(data => { + setConversation(data); + setMessages(data.messages); + }); + } + }, [conversationId]); + + // 发送消息(流式) + const sendMessage = async (content: string, attachmentIds?: string[]) => { + setIsLoading(true); + + // 添加用户消息 + const userMessage: Message = { + id: `temp-${Date.now()}`, + role: 'user', + content, + attachments: [], + createdAt: new Date().toISOString(), + }; + setMessages(prev => [...prev, userMessage]); + + // 初始化 AI 消息 + const aiMessage: Message = { + id: `temp-ai-${Date.now()}`, + role: 'assistant', + content: '', + thinkingContent: '', + createdAt: new Date().toISOString(), + }; + setMessages(prev => [...prev, aiMessage]); + + // 流式接收 + await api.sendMessageStream(conversationId!, content, attachmentIds, { + onThinkingDelta: (delta) => { + setMessages(prev => { + const last = prev[prev.length - 1]; + return [...prev.slice(0, -1), { + ...last, + thinkingContent: (last.thinkingContent || '') + delta, + }]; + }); + }, + onDelta: (delta) => { + setMessages(prev => { + const last = prev[prev.length - 1]; + return [...prev.slice(0, -1), { + ...last, + content: last.content + delta, + }]; + }); + }, + onComplete: (finalMessage) => { + setMessages(prev => { + return [...prev.slice(0, -1), finalMessage]; + }); + setIsLoading(false); + }, + onError: (error) => { + message.error(error.message); + setIsLoading(false); + }, + }); + }; + + return { + conversation, + messages, + isLoading, + sendMessage, + regenerate: async () => {}, + deleteConversation: async () => {}, + }; +} +``` + +### useStreamMessage + +```typescript +// hooks/useStreamMessage.ts + +interface StreamCallbacks { + onThinkingStart?: () => void; + onThinkingDelta?: (content: string) => void; + onThinkingEnd?: (duration: number) => void; + onMessageStart?: (id: string) => void; + onDelta?: (content: string) => void; + onMessageEnd?: (message: Message) => void; + onComplete?: (message: Message) => void; + onError?: (error: Error) => void; +} + +export function useStreamMessage() { + const streamMessage = async ( + url: string, + body: object, + callbacks: StreamCallbacks + ) => { + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'text/event-stream', + }, + body: JSON.stringify(body), + }); + + const reader = response.body?.getReader(); + const decoder = new TextDecoder(); + + while (true) { + const { done, value } = await reader!.read(); + if (done) break; + + const chunk = decoder.decode(value); + const events = parseSSE(chunk); + + for (const event of events) { + switch (event.type) { + case 'thinking_start': + callbacks.onThinkingStart?.(); + break; + case 'thinking_delta': + callbacks.onThinkingDelta?.(event.data.content); + break; + case 'thinking_end': + callbacks.onThinkingEnd?.(event.data.duration); + break; + case 'message_start': + callbacks.onMessageStart?.(event.data.id); + break; + case 'delta': + callbacks.onDelta?.(event.data.content); + break; + case 'message_end': + callbacks.onMessageEnd?.(event.data); + break; + case 'done': + callbacks.onComplete?.(event.data); + break; + case 'error': + callbacks.onError?.(new Error(event.data.message)); + break; + } + } + } + }; + + return { streamMessage }; +} +``` + +--- + +## 📱 响应式设计 + +### 断点策略 + +```typescript +// 断点定义 +const breakpoints = { + sm: 640, // 手机 + md: 768, // 平板(主要断点) + lg: 1024, // 桌面 + xl: 1280, // 大屏 +}; +``` + +### Dashboard 响应式 + +| 断点 | 布局 | +|------|------| +| `< 768px` | 流水线纵向滚动,卡片单列 | +| `≥ 768px` | 流水线横向 5 列 | + +### Workspace 响应式 + +| 断点 | 布局 | +|------|------| +| `< 768px` | 侧边栏隐藏(抽屉滑出),输入框键盘适配 | +| `≥ 768px` | 侧边栏固定显示 240px | + +--- + +## 📝 类型定义 + +```typescript +// types/index.ts + +export interface Agent { + id: string; + name: string; + description: string; + icon: string; + stage: 'design' | 'data' | 'analysis' | 'write' | 'publish'; + color: string; + knowledgeBaseId?: string; + isTool?: boolean; + targetModule?: string; + welcomeMessage?: string; + suggestedQuestions?: string[]; +} + +export interface Conversation { + id: string; + title: string; + agentId: string; + agent?: Agent; + projectId?: string; + messageCount: number; + lastMessage?: string; + createdAt: string; + updatedAt: string; +} + +export interface Message { + id: string; + conversationId?: string; + role: 'user' | 'assistant'; + content: string; + thinkingContent?: string; + attachments?: Attachment[]; + model?: string; + tokens?: number; + isPinned?: boolean; + createdAt: string; +} + +export interface Attachment { + id: string; + filename: string; + mimeType: string; + size: number; + ossUrl: string; + textExtracted: boolean; + tokenCount: number; + truncated: boolean; + createdAt: string; +} + +export interface IntentSuggestion { + agentId: string; + agentName: string; + confidence: number; + prefillPrompt: string; +} + +export interface SlashCommand { + key: string; + icon: string; + label: string; + description: string; +} +``` + +--- + +## 📝 更新日志 + +| 日期 | 版本 | 内容 | +|------|------|------| +| 2026-01-11 | V1.0 | 创建前端组件设计文档 | + + + + diff --git a/docs/03-业务模块/ASL-AI智能文献/04-开发计划/05-全文复筛前端开发计划.md b/docs/03-业务模块/ASL-AI智能文献/04-开发计划/05-全文复筛前端开发计划.md index f3bceb46..22fdf671 100644 --- a/docs/03-业务模块/ASL-AI智能文献/04-开发计划/05-全文复筛前端开发计划.md +++ b/docs/03-业务模块/ASL-AI智能文献/04-开发计划/05-全文复筛前端开发计划.md @@ -1288,6 +1288,9 @@ 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 c630ed0b..05a305be 100644 --- a/docs/03-业务模块/ASL-AI智能文献/05-开发记录/2025-01-23_全文复筛前端开发完成.md +++ b/docs/03-业务模块/ASL-AI智能文献/05-开发记录/2025-01-23_全文复筛前端开发完成.md @@ -402,6 +402,9 @@ 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 36ab8002..c5065f41 100644 --- a/docs/03-业务模块/ASL-AI智能文献/05-开发记录/2025-01-23_全文复筛前端逻辑调整.md +++ b/docs/03-业务模块/ASL-AI智能文献/05-开发记录/2025-01-23_全文复筛前端逻辑调整.md @@ -345,6 +345,9 @@ 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 a0e70f47..0d124a68 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 @@ -504,6 +504,9 @@ Failed to open file '\\tmp\\extraction_service\\temp_10000_test.pdf' + + + diff --git a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_AI_Few-shot示例库.md b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_AI_Few-shot示例库.md index 0d990cbc..fc1140df 100644 --- a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_AI_Few-shot示例库.md +++ b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_AI_Few-shot示例库.md @@ -570,6 +570,9 @@ 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 a28e3ef8..fddb27ce 100644 --- a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Bug修复总结_2025-12-08.md +++ b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Bug修复总结_2025-12-08.md @@ -408,6 +408,9 @@ npm run dev + + + diff --git a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Day3开发计划.md b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Day3开发计划.md index c2ea98f6..c73be7a4 100644 --- a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Day3开发计划.md +++ b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Day3开发计划.md @@ -985,6 +985,9 @@ 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 79f22286..f39101fd 100644 --- a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Day4-5前端开发计划.md +++ b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Day4-5前端开发计划.md @@ -1319,6 +1319,9 @@ npm install react-markdown + + + diff --git a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Pivot列顺序优化总结.md b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Pivot列顺序优化总结.md index 0cc225c4..7ebf20f0 100644 --- a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Pivot列顺序优化总结.md +++ b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Pivot列顺序优化总结.md @@ -227,6 +227,9 @@ 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 a087f3cc..10c0fe88 100644 --- a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_方案B实施总结_2025-12-09.md +++ b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_方案B实施总结_2025-12-09.md @@ -385,6 +385,9 @@ 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 ad103416..1f08e892 100644 --- a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_缺失值处理_开发进度_2025-12-10.md +++ b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_缺失值处理_开发进度_2025-12-10.md @@ -219,6 +219,9 @@ async handleFillnaMice(request, reply) { + + + diff --git a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_缺失值处理功能_更新说明.md b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_缺失值处理功能_更新说明.md index a92d9871..10966832 100644 --- a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_缺失值处理功能_更新说明.md +++ b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_缺失值处理功能_更新说明.md @@ -191,6 +191,9 @@ 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 35beb2cf..1a6941d2 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-02_工作总结.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-02_工作总结.md @@ -341,6 +341,9 @@ 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 3a50bab8..aa4669df 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-06_工具C_Day1开发完成总结.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-06_工具C_Day1开发完成总结.md @@ -413,6 +413,9 @@ 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 365a7ff4..11cf0e19 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-06_工具C_Day2开发完成总结.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-06_工具C_Day2开发完成总结.md @@ -642,6 +642,9 @@ 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 ad4aa9b4..bcf90fa7 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_AI对话核心功能增强总结.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_AI对话核心功能增强总结.md @@ -646,6 +646,9 @@ 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 6a54bfca..5cbe734b 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_Bug修复_DataGrid空数据防御.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_Bug修复_DataGrid空数据防御.md @@ -298,6 +298,9 @@ 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 ed81356f..8b9200cd 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 @@ -451,6 +451,9 @@ Response: + + + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_Day5最终总结.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_Day5最终总结.md index 039b4c0a..f0818895 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_Day5最终总结.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_Day5最终总结.md @@ -445,6 +445,9 @@ 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 558215b4..afb48700 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_UI优化与Bug修复.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_UI优化与Bug修复.md @@ -355,6 +355,9 @@ 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 5880a5dc..d43823fd 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_后端API完整对接完成.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_后端API完整对接完成.md @@ -395,6 +395,9 @@ 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 eec659d7..914a4a28 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_完整UI优化与功能增强.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_完整UI优化与功能增强.md @@ -643,6 +643,9 @@ 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 7f278bcf..922811ec 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_工具C_Day4前端基础完成.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_工具C_Day4前端基础完成.md @@ -253,6 +253,9 @@ Day 5 (6-8小时): + + + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/DC模块重建完成总结-Day1.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/DC模块重建完成总结-Day1.md index 69a31b35..556645f0 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/DC模块重建完成总结-Day1.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/DC模块重建完成总结-Day1.md @@ -431,6 +431,9 @@ 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 27471945..3c66d23c 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Phase1-Portal页面开发完成-2025-12-02.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Phase1-Portal页面开发完成-2025-12-02.md @@ -406,6 +406,9 @@ 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 6b575166..89b44d42 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 @@ -390,6 +390,9 @@ 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 18bec607..7aaf778f 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Portal页面UI优化-2025-12-02.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Portal页面UI优化-2025-12-02.md @@ -350,6 +350,9 @@ + + + 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 7e04292f..bcc004bf 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 @@ -304,6 +304,9 @@ 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 86e41516..7f27c27f 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/ToolB-UI优化-2025-12-03.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/ToolB-UI优化-2025-12-03.md @@ -353,6 +353,9 @@ + + + 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 ce0cf774..b21ef3dd 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 @@ -316,6 +316,9 @@ + + + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/ToolB浏览器测试计划-2025-12-03.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/ToolB浏览器测试计划-2025-12-03.md index 90779e9a..7892f0b0 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/ToolB浏览器测试计划-2025-12-03.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/ToolB浏览器测试计划-2025-12-03.md @@ -380,6 +380,9 @@ + + + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/后端API测试报告-2025-12-02.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/后端API测试报告-2025-12-02.md index e9ff029a..063bd7f3 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/后端API测试报告-2025-12-02.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/后端API测试报告-2025-12-02.md @@ -468,6 +468,9 @@ Tool B后端代码**100%复用**了平台通用能力层,无任何重复开发 + + + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/待办事项-下一步工作.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/待办事项-下一步工作.md index 28b604b4..5a704807 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/待办事项-下一步工作.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/待办事项-下一步工作.md @@ -314,6 +314,9 @@ + + + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/数据库验证报告-2025-12-02.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/数据库验证报告-2025-12-02.md index dfd75689..94e329eb 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/数据库验证报告-2025-12-02.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/数据库验证报告-2025-12-02.md @@ -245,6 +245,9 @@ $ 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 8436178c..57ad3873 100644 --- a/docs/03-业务模块/DC-数据清洗整理/07-技术债务/Tool-B技术债务清单.md +++ b/docs/03-业务模块/DC-数据清洗整理/07-技术债务/Tool-B技术债务清单.md @@ -478,6 +478,9 @@ ${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 3a85ae38..b18ead68 100644 --- a/docs/03-业务模块/IIT Manager Agent/02-技术设计/IIT Manager Agent 技术路径与架构设计.md +++ b/docs/03-业务模块/IIT Manager Agent/02-技术设计/IIT Manager Agent 技术路径与架构设计.md @@ -687,3 +687,6 @@ 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 ee1e1ddb..84a9af23 100644 --- a/docs/03-业务模块/IIT Manager Agent/04-开发计划/REDCap对接技术方案与实施指南.md +++ b/docs/03-业务模块/IIT Manager Agent/04-开发计划/REDCap对接技术方案与实施指南.md @@ -1081,3 +1081,6 @@ async function testIntegration() { + + + diff --git a/docs/03-业务模块/IIT Manager Agent/04-开发计划/企业微信注册指南.md b/docs/03-业务模块/IIT Manager Agent/04-开发计划/企业微信注册指南.md index f9bb70bf..fc4a9514 100644 --- a/docs/03-业务模块/IIT Manager Agent/04-开发计划/企业微信注册指南.md +++ b/docs/03-业务模块/IIT Manager Agent/04-开发计划/企业微信注册指南.md @@ -222,3 +222,6 @@ 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 e6e78d52..e4b0477e 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 @@ -642,3 +642,6 @@ 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 0ffa0521..c87374d1 100644 --- a/docs/03-业务模块/IIT Manager Agent/06-开发记录/Day2-REDCap实时集成开发完成记录.md +++ b/docs/03-业务模块/IIT Manager Agent/06-开发记录/Day2-REDCap实时集成开发完成记录.md @@ -648,3 +648,6 @@ 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 e0c911c8..4a2bf262 100644 --- a/docs/03-业务模块/IIT Manager Agent/06-开发记录/Day3-企业微信集成与端到端测试完成记录.md +++ b/docs/03-业务模块/IIT Manager Agent/06-开发记录/Day3-企业微信集成与端到端测试完成记录.md @@ -798,3 +798,6 @@ 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 f6085b9b..a1b18223 100644 --- a/docs/03-业务模块/IIT Manager Agent/06-开发记录/Day3-企业微信集成开发完成记录.md +++ b/docs/03-业务模块/IIT Manager Agent/06-开发记录/Day3-企业微信集成开发完成记录.md @@ -555,3 +555,6 @@ 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 45b156f9..e55e47d2 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 @@ -322,3 +322,6 @@ 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 1696f818..2df393d4 100644 --- a/docs/03-业务模块/IIT Manager Agent/06-开发记录/V1.1更新完成报告.md +++ b/docs/03-业务模块/IIT Manager Agent/06-开发记录/V1.1更新完成报告.md @@ -266,3 +266,6 @@ 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 533e4183..91f2ce05 100644 --- a/docs/03-业务模块/IIT Manager Agent/07-技术债务/IIT Manager Agent 技术债务清单.md +++ b/docs/03-业务模块/IIT Manager Agent/07-技术债务/IIT Manager Agent 技术债务清单.md @@ -680,3 +680,6 @@ const answer = `根据研究方案[1]和CRF表格[2],纳入标准包括: + + + diff --git a/docs/03-业务模块/INST-机构管理端/00-模块当前状态与开发指南.md b/docs/03-业务模块/INST-机构管理端/00-模块当前状态与开发指南.md index 20f03cf6..2a49119f 100644 --- a/docs/03-业务模块/INST-机构管理端/00-模块当前状态与开发指南.md +++ b/docs/03-业务模块/INST-机构管理端/00-模块当前状态与开发指南.md @@ -437,3 +437,6 @@ export const calculateAvailableQuota = async (tenantId: string) => { **🚀 敬请期待!** + + + diff --git a/docs/03-业务模块/INST-机构管理端/README.md b/docs/03-业务模块/INST-机构管理端/README.md index f95b2355..ad7f595d 100644 --- a/docs/03-业务模块/INST-机构管理端/README.md +++ b/docs/03-业务模块/INST-机构管理端/README.md @@ -310,3 +310,6 @@ https://platform.example.com/t/pharma-abc/login *最后更新:2026-01-11* + + + diff --git a/docs/03-业务模块/PKB-个人知识库/06-开发记录/2026-01-07-前端迁移与批处理功能完善.md b/docs/03-业务模块/PKB-个人知识库/06-开发记录/2026-01-07-前端迁移与批处理功能完善.md index 4a300249..bd1c406e 100644 --- a/docs/03-业务模块/PKB-个人知识库/06-开发记录/2026-01-07-前端迁移与批处理功能完善.md +++ b/docs/03-业务模块/PKB-个人知识库/06-开发记录/2026-01-07-前端迁移与批处理功能完善.md @@ -360,3 +360,6 @@ 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 6253053a..dacd8e21 100644 --- a/docs/03-业务模块/PKB-个人知识库/06-开发记录/2026-01-07_PKB模块前端V3设计实现.md +++ b/docs/03-业务模块/PKB-个人知识库/06-开发记录/2026-01-07_PKB模块前端V3设计实现.md @@ -233,3 +233,6 @@ 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 769db96a..0d1af142 100644 --- a/docs/03-业务模块/Redcap/01-部署与配置/10-REDCap_Docker部署操作手册.md +++ b/docs/03-业务模块/Redcap/01-部署与配置/10-REDCap_Docker部署操作手册.md @@ -770,3 +770,6 @@ 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 d6cac4c2..29e1560e 100644 --- a/docs/03-业务模块/Redcap/README.md +++ b/docs/03-业务模块/Redcap/README.md @@ -152,3 +152,6 @@ AIclinicalresearch/redcap-docker-dev/ + + + diff --git a/docs/04-开发规范/09-数据库开发规范.md b/docs/04-开发规范/09-数据库开发规范.md index 678eab66..0390b6ae 100644 --- a/docs/04-开发规范/09-数据库开发规范.md +++ b/docs/04-开发规范/09-数据库开发规范.md @@ -318,3 +318,6 @@ npx tsx check_iit_asl_data.ts 详见:`docs/08-项目管理/2026-01-11-数据库事故总结.md` + + + diff --git a/docs/04-开发规范/10-模块认证规范.md b/docs/04-开发规范/10-模块认证规范.md index b0c6c1e7..37d9680a 100644 --- a/docs/04-开发规范/10-模块认证规范.md +++ b/docs/04-开发规范/10-模块认证规范.md @@ -188,3 +188,4 @@ interface DecodedToken { - 后端 JWT 服务: `backend/src/common/auth/jwt.service.ts` + diff --git a/docs/05-部署文档/02-SAE部署完全指南(产品经理版).md b/docs/05-部署文档/02-SAE部署完全指南(产品经理版).md index 3a94a3f3..77439928 100644 --- a/docs/05-部署文档/02-SAE部署完全指南(产品经理版).md +++ b/docs/05-部署文档/02-SAE部署完全指南(产品经理版).md @@ -885,6 +885,9 @@ ACR镜像仓库: + + + diff --git a/docs/05-部署文档/07-前端Nginx-SAE部署操作手册.md b/docs/05-部署文档/07-前端Nginx-SAE部署操作手册.md index ea4705cd..756c045b 100644 --- a/docs/05-部署文档/07-前端Nginx-SAE部署操作手册.md +++ b/docs/05-部署文档/07-前端Nginx-SAE部署操作手册.md @@ -1372,6 +1372,9 @@ SAE应用配置: + + + diff --git a/docs/05-部署文档/08-PostgreSQL数据库部署操作手册.md b/docs/05-部署文档/08-PostgreSQL数据库部署操作手册.md index 570b9f2e..3797d1e9 100644 --- a/docs/05-部署文档/08-PostgreSQL数据库部署操作手册.md +++ b/docs/05-部署文档/08-PostgreSQL数据库部署操作手册.md @@ -1188,6 +1188,9 @@ 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 a7f5911f..b31afcfd 100644 --- a/docs/05-部署文档/10-Node.js后端-Docker镜像构建手册.md +++ b/docs/05-部署文档/10-Node.js后端-Docker镜像构建手册.md @@ -599,6 +599,9 @@ scripts/*.ts + + + diff --git a/docs/05-部署文档/11-Node.js后端-SAE部署配置清单.md b/docs/05-部署文档/11-Node.js后端-SAE部署配置清单.md index c159331b..9a57bbfa 100644 --- a/docs/05-部署文档/11-Node.js后端-SAE部署配置清单.md +++ b/docs/05-部署文档/11-Node.js后端-SAE部署配置清单.md @@ -287,6 +287,9 @@ Node.js后端部署成功后: + + + diff --git a/docs/05-部署文档/12-Node.js后端-SAE部署操作手册.md b/docs/05-部署文档/12-Node.js后端-SAE部署操作手册.md index d299eb2e..2572eb20 100644 --- a/docs/05-部署文档/12-Node.js后端-SAE部署操作手册.md +++ b/docs/05-部署文档/12-Node.js后端-SAE部署操作手册.md @@ -510,6 +510,9 @@ 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 5fdaa791..31e6af68 100644 --- a/docs/05-部署文档/13-Node.js后端-镜像修复记录.md +++ b/docs/05-部署文档/13-Node.js后端-镜像修复记录.md @@ -225,6 +225,9 @@ 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 1d905b5d..d55a14b7 100644 --- a/docs/05-部署文档/14-Node.js后端-pino-pretty问题修复.md +++ b/docs/05-部署文档/14-Node.js后端-pino-pretty问题修复.md @@ -263,6 +263,9 @@ npm run dev + + + diff --git a/docs/05-部署文档/16-前端Nginx-部署成功总结.md b/docs/05-部署文档/16-前端Nginx-部署成功总结.md index 594239ca..2885004e 100644 --- a/docs/05-部署文档/16-前端Nginx-部署成功总结.md +++ b/docs/05-部署文档/16-前端Nginx-部署成功总结.md @@ -487,6 +487,9 @@ pgm-2zex1m2y3r23hdn5.pg.rds.aliyuncs.com:5432 + + + diff --git a/docs/05-部署文档/17-完整部署实战手册-2025版.md b/docs/05-部署文档/17-完整部署实战手册-2025版.md index d82a3f50..8dc14ea4 100644 --- a/docs/05-部署文档/17-完整部署实战手册-2025版.md +++ b/docs/05-部署文档/17-完整部署实战手册-2025版.md @@ -1815,6 +1815,9 @@ curl http://8.140.53.236/ + + + diff --git a/docs/05-部署文档/18-部署文档使用指南.md b/docs/05-部署文档/18-部署文档使用指南.md index 0cb43123..de7b857f 100644 --- a/docs/05-部署文档/18-部署文档使用指南.md +++ b/docs/05-部署文档/18-部署文档使用指南.md @@ -363,6 +363,9 @@ 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 8252094c..569062e8 100644 --- a/docs/05-部署文档/19-日常更新快速操作手册.md +++ b/docs/05-部署文档/19-日常更新快速操作手册.md @@ -685,6 +685,9 @@ docker login --username=gofeng117@163.com \ + + + diff --git a/docs/05-部署文档/文档修正报告-20251214.md b/docs/05-部署文档/文档修正报告-20251214.md index 21d5d676..95821522 100644 --- a/docs/05-部署文档/文档修正报告-20251214.md +++ b/docs/05-部署文档/文档修正报告-20251214.md @@ -496,6 +496,9 @@ NAT网关成本¥100/月,对初创团队是一笔开销 + + + diff --git a/docs/07-运维文档/03-SAE环境变量配置指南.md b/docs/07-运维文档/03-SAE环境变量配置指南.md index 4b7f2d1f..e48aedf8 100644 --- a/docs/07-运维文档/03-SAE环境变量配置指南.md +++ b/docs/07-运维文档/03-SAE环境变量配置指南.md @@ -401,6 +401,9 @@ curl http://你的SAE地址:3001/health + + + diff --git a/docs/07-运维文档/05-Redis缓存与队列的区别说明.md b/docs/07-运维文档/05-Redis缓存与队列的区别说明.md index 462db661..8d0b04a7 100644 --- a/docs/07-运维文档/05-Redis缓存与队列的区别说明.md +++ b/docs/07-运维文档/05-Redis缓存与队列的区别说明.md @@ -733,6 +733,9 @@ const job = await queue.getJob(jobId); + + + diff --git a/docs/07-运维文档/06-长时间任务可靠性分析.md b/docs/07-运维文档/06-长时间任务可靠性分析.md index 8f2d9fcf..6821e7b4 100644 --- a/docs/07-运维文档/06-长时间任务可靠性分析.md +++ b/docs/07-运维文档/06-长时间任务可靠性分析.md @@ -500,6 +500,9 @@ processLiteraturesInBackground(task.id, projectId, testLiteratures); + + + diff --git a/docs/07-运维文档/07-Redis使用需求分析(按模块).md b/docs/07-运维文档/07-Redis使用需求分析(按模块).md index 94fb7c47..9ec25980 100644 --- a/docs/07-运维文档/07-Redis使用需求分析(按模块).md +++ b/docs/07-运维文档/07-Redis使用需求分析(按模块).md @@ -977,6 +977,9 @@ ROI = (¥22,556 - ¥144) / ¥144 × 100% = 15,564% + + + diff --git a/docs/08-项目管理/03-每周计划/2025-12-13-Postgres-Only架构改造完成.md b/docs/08-项目管理/03-每周计划/2025-12-13-Postgres-Only架构改造完成.md index 02c6d1cd..12035699 100644 --- a/docs/08-项目管理/03-每周计划/2025-12-13-Postgres-Only架构改造完成.md +++ b/docs/08-项目管理/03-每周计划/2025-12-13-Postgres-Only架构改造完成.md @@ -1034,6 +1034,9 @@ Redis 实例:¥500/月 + + + diff --git a/docs/08-项目管理/05-技术债务/通用对话服务抽取计划.md b/docs/08-项目管理/05-技术债务/通用对话服务抽取计划.md index a376a4a3..bde392f7 100644 --- a/docs/08-项目管理/05-技术债务/通用对话服务抽取计划.md +++ b/docs/08-项目管理/05-技术债务/通用对话服务抽取计划.md @@ -492,6 +492,9 @@ import { ChatContainer } from '@/shared/components/Chat'; + + + diff --git a/docs/08-项目管理/2026-01-11-数据库事故总结.md b/docs/08-项目管理/2026-01-11-数据库事故总结.md index 8784cbc9..34514672 100644 --- a/docs/08-项目管理/2026-01-11-数据库事故总结.md +++ b/docs/08-项目管理/2026-01-11-数据库事故总结.md @@ -202,3 +202,6 @@ VALUES ('user-mock-001', '13800000000', ..., 'tenant-mock-001', ...); > **声明**:本次事故发生在测试环境,未影响生产数据。但暴露的问题同样可能在生产环境发生,需要高度重视。 + + + diff --git a/docs/08-项目管理/PKB前端问题修复报告.md b/docs/08-项目管理/PKB前端问题修复报告.md index 6e05897d..2db83b5c 100644 --- a/docs/08-项目管理/PKB前端问题修复报告.md +++ b/docs/08-项目管理/PKB前端问题修复报告.md @@ -414,3 +414,6 @@ frontend-v2/src/modules/pkb/ + + + diff --git a/docs/08-项目管理/PKB前端验证指南.md b/docs/08-项目管理/PKB前端验证指南.md index 0e732374..ea27627f 100644 --- a/docs/08-项目管理/PKB前端验证指南.md +++ b/docs/08-项目管理/PKB前端验证指南.md @@ -276,3 +276,6 @@ npm run dev + + + diff --git a/docs/08-项目管理/PKB功能审查报告-阶段0.md b/docs/08-项目管理/PKB功能审查报告-阶段0.md index 1430308c..09ea821d 100644 --- a/docs/08-项目管理/PKB功能审查报告-阶段0.md +++ b/docs/08-项目管理/PKB功能审查报告-阶段0.md @@ -791,3 +791,6 @@ AIA智能问答模块 + + + diff --git a/docs/08-项目管理/PKB和RVW功能迁移计划.md b/docs/08-项目管理/PKB和RVW功能迁移计划.md index debdb9d3..9bdce0a0 100644 --- a/docs/08-项目管理/PKB和RVW功能迁移计划.md +++ b/docs/08-项目管理/PKB和RVW功能迁移计划.md @@ -933,6 +933,9 @@ 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 4ba3ac40..9f48c53f 100644 --- a/docs/08-项目管理/PKB精细化优化报告.md +++ b/docs/08-项目管理/PKB精细化优化报告.md @@ -589,3 +589,6 @@ const typography = { + + + diff --git a/docs/08-项目管理/PKB迁移-超级安全执行计划.md b/docs/08-项目管理/PKB迁移-超级安全执行计划.md index 8fe80a0f..f889aab6 100644 --- a/docs/08-项目管理/PKB迁移-超级安全执行计划.md +++ b/docs/08-项目管理/PKB迁移-超级安全执行计划.md @@ -901,3 +901,6 @@ app.use('/api/v1/knowledge', (req, res) => { + + + diff --git a/docs/08-项目管理/PKB迁移-阶段1完成报告.md b/docs/08-项目管理/PKB迁移-阶段1完成报告.md index d2efbd0f..1b807c8e 100644 --- a/docs/08-项目管理/PKB迁移-阶段1完成报告.md +++ b/docs/08-项目管理/PKB迁移-阶段1完成报告.md @@ -215,3 +215,6 @@ rm -rf src/modules/pkb + + + diff --git a/docs/08-项目管理/PKB迁移-阶段2完成报告.md b/docs/08-项目管理/PKB迁移-阶段2完成报告.md index e4988c7e..f8009fe3 100644 --- a/docs/08-项目管理/PKB迁移-阶段2完成报告.md +++ b/docs/08-项目管理/PKB迁移-阶段2完成报告.md @@ -390,3 +390,6 @@ GET /api/v2/pkb/batch-tasks/batch/templates + + + diff --git a/docs/08-项目管理/PKB迁移-阶段2进行中.md b/docs/08-项目管理/PKB迁移-阶段2进行中.md index 6a1fed6c..19c70521 100644 --- a/docs/08-项目管理/PKB迁移-阶段2进行中.md +++ b/docs/08-项目管理/PKB迁移-阶段2进行中.md @@ -34,3 +34,6 @@ import pkbRoutes from './modules/pkb/routes/index.js'; + + + diff --git a/docs/08-项目管理/PKB迁移-阶段3完成报告.md b/docs/08-项目管理/PKB迁移-阶段3完成报告.md index d2db6f45..1786f0ac 100644 --- a/docs/08-项目管理/PKB迁移-阶段3完成报告.md +++ b/docs/08-项目管理/PKB迁移-阶段3完成报告.md @@ -303,3 +303,6 @@ backend/ + + + diff --git a/docs/08-项目管理/PKB迁移-阶段4完成报告.md b/docs/08-项目管理/PKB迁移-阶段4完成报告.md index bb52d0f7..74ee6c18 100644 --- a/docs/08-项目管理/PKB迁移-阶段4完成报告.md +++ b/docs/08-项目管理/PKB迁移-阶段4完成报告.md @@ -514,3 +514,6 @@ const response = await fetch('/api/v2/pkb/batch-tasks/batch/execute', { + + + diff --git a/extraction_service/.dockerignore b/extraction_service/.dockerignore index 0ca1e34e..81127fb2 100644 --- a/extraction_service/.dockerignore +++ b/extraction_service/.dockerignore @@ -67,6 +67,9 @@ models/ + + + diff --git a/extraction_service/operations/__init__.py b/extraction_service/operations/__init__.py index b5f0d66e..0920da11 100644 --- a/extraction_service/operations/__init__.py +++ b/extraction_service/operations/__init__.py @@ -55,6 +55,9 @@ __version__ = '1.0.0' + + + diff --git a/extraction_service/operations/dropna.py b/extraction_service/operations/dropna.py index a50ddd3b..2efae6d9 100644 --- a/extraction_service/operations/dropna.py +++ b/extraction_service/operations/dropna.py @@ -188,6 +188,9 @@ def get_missing_summary(df: pd.DataFrame) -> dict: + + + diff --git a/extraction_service/operations/filter.py b/extraction_service/operations/filter.py index f2e12381..540d1428 100644 --- a/extraction_service/operations/filter.py +++ b/extraction_service/operations/filter.py @@ -148,6 +148,9 @@ def apply_filter( + + + diff --git a/extraction_service/operations/unpivot.py b/extraction_service/operations/unpivot.py index f8e5f09b..72b25b7d 100644 --- a/extraction_service/operations/unpivot.py +++ b/extraction_service/operations/unpivot.py @@ -312,6 +312,9 @@ def get_unpivot_preview( + + + diff --git a/extraction_service/test_dc_api.py b/extraction_service/test_dc_api.py index fc040b13..af1c2ec7 100644 --- a/extraction_service/test_dc_api.py +++ b/extraction_service/test_dc_api.py @@ -322,6 +322,9 @@ if __name__ == "__main__": + + + diff --git a/extraction_service/test_execute_simple.py b/extraction_service/test_execute_simple.py index 0e9a8364..5b4db083 100644 --- a/extraction_service/test_execute_simple.py +++ b/extraction_service/test_execute_simple.py @@ -88,6 +88,9 @@ except Exception as e: + + + diff --git a/extraction_service/test_module.py b/extraction_service/test_module.py index 20063bf1..2f23cc25 100644 --- a/extraction_service/test_module.py +++ b/extraction_service/test_module.py @@ -68,6 +68,9 @@ except Exception as e: + + + diff --git a/frontend-v2/.dockerignore b/frontend-v2/.dockerignore index d866dca6..93400a8d 100644 --- a/frontend-v2/.dockerignore +++ b/frontend-v2/.dockerignore @@ -87,6 +87,9 @@ vite.config.*.timestamp-* + + + diff --git a/frontend-v2/docker-entrypoint.sh b/frontend-v2/docker-entrypoint.sh index 21c78b95..1fa61542 100644 --- a/frontend-v2/docker-entrypoint.sh +++ b/frontend-v2/docker-entrypoint.sh @@ -54,6 +54,9 @@ exec nginx -g 'daemon off;' + + + diff --git a/frontend-v2/nginx.conf b/frontend-v2/nginx.conf index 6ff19b22..8b288c32 100644 --- a/frontend-v2/nginx.conf +++ b/frontend-v2/nginx.conf @@ -210,6 +210,9 @@ http { + + + diff --git a/frontend-v2/src/common/api/axios.ts b/frontend-v2/src/common/api/axios.ts index daaaf5d8..97decff0 100644 --- a/frontend-v2/src/common/api/axios.ts +++ b/frontend-v2/src/common/api/axios.ts @@ -43,3 +43,4 @@ apiClient.interceptors.response.use( export default apiClient; + diff --git a/frontend-v2/src/framework/auth/AuthContext.tsx b/frontend-v2/src/framework/auth/AuthContext.tsx index 61c2e800..5ca56e06 100644 --- a/frontend-v2/src/framework/auth/AuthContext.tsx +++ b/frontend-v2/src/framework/auth/AuthContext.tsx @@ -204,3 +204,6 @@ export function useAuth(): AuthContextType { export { AuthContext }; + + + diff --git a/frontend-v2/src/framework/auth/api.ts b/frontend-v2/src/framework/auth/api.ts index 59f696bd..e2ff1784 100644 --- a/frontend-v2/src/framework/auth/api.ts +++ b/frontend-v2/src/framework/auth/api.ts @@ -240,3 +240,6 @@ export async function logout(): Promise { } } + + + diff --git a/frontend-v2/src/framework/auth/index.ts b/frontend-v2/src/framework/auth/index.ts index 83c5bbf1..f5d81be2 100644 --- a/frontend-v2/src/framework/auth/index.ts +++ b/frontend-v2/src/framework/auth/index.ts @@ -6,3 +6,6 @@ export { AuthProvider, useAuth, AuthContext } from './AuthContext'; export * from './types'; export * from './api'; + + + diff --git a/frontend-v2/src/framework/auth/moduleApi.ts b/frontend-v2/src/framework/auth/moduleApi.ts index cc120988..c3835b59 100644 --- a/frontend-v2/src/framework/auth/moduleApi.ts +++ b/frontend-v2/src/framework/auth/moduleApi.ts @@ -32,3 +32,4 @@ export async function fetchUserModules(): Promise { return result.data || []; } + diff --git a/frontend-v2/src/framework/auth/types.ts b/frontend-v2/src/framework/auth/types.ts index 9a2fbe95..7b1cf5a1 100644 --- a/frontend-v2/src/framework/auth/types.ts +++ b/frontend-v2/src/framework/auth/types.ts @@ -99,3 +99,6 @@ export interface AuthContextType extends AuthState { hasRole: (...roles: UserRole[]) => boolean; } + + + diff --git a/frontend-v2/src/modules/asl/api/index.ts b/frontend-v2/src/modules/asl/api/index.ts index 4b0f14be..88ef9769 100644 --- a/frontend-v2/src/modules/asl/api/index.ts +++ b/frontend-v2/src/modules/asl/api/index.ts @@ -14,10 +14,25 @@ import type { PaginatedResponse, ProjectStatistics } from '../types'; +import { getAccessToken } from '../../../framework/auth/api'; // API基础URL const API_BASE_URL = '/api/v1/asl'; +/** + * 获取带认证的请求头 + */ +function getAuthHeaders(): HeadersInit { + const headers: Record = { + 'Content-Type': 'application/json', + }; + const token = getAccessToken(); + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + return headers; +} + // 通用请求函数 async function request( url: string, @@ -26,7 +41,7 @@ async function request( const response = await fetch(`${API_BASE_URL}${url}`, { ...options, headers: { - 'Content-Type': 'application/json', + ...getAuthHeaders(), ...options?.headers, }, }); @@ -239,7 +254,8 @@ export async function exportScreeningResults( ).toString(); const response = await fetch( - `${API_BASE_URL}/projects/${projectId}/screening/results/export?${queryString}` + `${API_BASE_URL}/projects/${projectId}/screening/results/export?${queryString}`, + { headers: getAuthHeaders() } ); if (!response.ok) { @@ -409,7 +425,8 @@ export async function exportFulltextResults( taskId: string ): Promise { const response = await fetch( - `${API_BASE_URL}/fulltext-screening/tasks/${taskId}/export` + `${API_BASE_URL}/fulltext-screening/tasks/${taskId}/export`, + { headers: getAuthHeaders() } ); if (!response.ok) { diff --git a/frontend-v2/src/modules/asl/components/FulltextDetailDrawer.tsx b/frontend-v2/src/modules/asl/components/FulltextDetailDrawer.tsx index 403bbf77..ec451623 100644 --- a/frontend-v2/src/modules/asl/components/FulltextDetailDrawer.tsx +++ b/frontend-v2/src/modules/asl/components/FulltextDetailDrawer.tsx @@ -557,6 +557,9 @@ export default FulltextDetailDrawer; + + + diff --git a/frontend-v2/src/modules/dc/api/toolB.ts b/frontend-v2/src/modules/dc/api/toolB.ts index 7856656e..6d2d197b 100644 --- a/frontend-v2/src/modules/dc/api/toolB.ts +++ b/frontend-v2/src/modules/dc/api/toolB.ts @@ -3,8 +3,24 @@ * 病历结构化机器人 API调用 */ +import { getAccessToken } from '../../../framework/auth/api'; + const API_BASE = '/api/v1/dc/tool-b'; +/** + * 获取带认证的请求头 + */ +function getAuthHeaders(): HeadersInit { + const headers: Record = { + 'Content-Type': 'application/json', + }; + const token = getAccessToken(); + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + return headers; +} + export interface UploadFileResponse { fileKey: string; url: string; @@ -86,8 +102,16 @@ export async function uploadFile(file: File): Promise { const formData = new FormData(); formData.append('file', file); + // 文件上传不设置 Content-Type,让浏览器自动设置 multipart/form-data + const token = getAccessToken(); + const headers: HeadersInit = {}; + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + const response = await fetch(`${API_BASE}/upload`, { method: 'POST', + headers, body: formData, }); @@ -110,7 +134,7 @@ export async function uploadFile(file: File): Promise { export async function healthCheck(request: HealthCheckRequest): Promise { const response = await fetch(`${API_BASE}/health-check`, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: getAuthHeaders(), body: JSON.stringify(request), }); @@ -136,7 +160,9 @@ export async function healthCheck(request: HealthCheckRequest): Promise { - const response = await fetch(`${API_BASE}/templates`); + const response = await fetch(`${API_BASE}/templates`, { + headers: getAuthHeaders(), + }); if (!response.ok) { throw new Error(`Get templates failed: ${response.statusText}`); @@ -152,7 +178,7 @@ export async function getTemplates(): Promise { export async function createTask(request: CreateTaskRequest): Promise { const response = await fetch(`${API_BASE}/tasks`, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: getAuthHeaders(), body: JSON.stringify(request), }); @@ -168,7 +194,9 @@ export async function createTask(request: CreateTaskRequest): Promise { - const response = await fetch(`${API_BASE}/tasks/${taskId}/progress`); + const response = await fetch(`${API_BASE}/tasks/${taskId}/progress`, { + headers: getAuthHeaders(), + }); if (!response.ok) { throw new Error(`Get task progress failed: ${response.statusText}`); @@ -182,7 +210,9 @@ export async function getTaskProgress(taskId: string): Promise { * 获取验证网格数据 */ export async function getTaskItems(taskId: string): Promise<{ items: ExtractionItem[] }> { - const response = await fetch(`${API_BASE}/tasks/${taskId}/items`); + const response = await fetch(`${API_BASE}/tasks/${taskId}/items`, { + headers: getAuthHeaders(), + }); if (!response.ok) { throw new Error(`Get task items failed: ${response.statusText}`); @@ -202,7 +232,7 @@ export async function resolveConflict( ): Promise<{ success: boolean }> { const response = await fetch(`${API_BASE}/items/${itemId}/resolve`, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: getAuthHeaders(), body: JSON.stringify({ field: fieldName, chosenValue: value }), // 🔑 后端期望field而非fieldName }); @@ -220,7 +250,9 @@ export async function resolveConflict( export async function exportResults(taskId: string): Promise { console.log('[Export] Starting export for taskId:', taskId); - const response = await fetch(`${API_BASE}/tasks/${taskId}/export`); + const response = await fetch(`${API_BASE}/tasks/${taskId}/export`, { + headers: getAuthHeaders(), + }); console.log('[Export] Response status:', response.status, response.statusText); console.log('[Export] Response headers:', { diff --git a/frontend-v2/src/modules/dc/api/toolC.ts b/frontend-v2/src/modules/dc/api/toolC.ts index f300006f..5821f2a2 100644 --- a/frontend-v2/src/modules/dc/api/toolC.ts +++ b/frontend-v2/src/modules/dc/api/toolC.ts @@ -6,7 +6,7 @@ * - AI功能:生成代码、执行代码、一步到位处理、获取历史 */ -import axios from 'axios'; +import apiClient from '../../../common/api/axios'; const BASE_URL = '/api/v1/dc/tool-c'; @@ -90,7 +90,7 @@ export const uploadFile = async (file: File): Promise => { const formData = new FormData(); formData.append('file', file); - const response = await axios.post(`${BASE_URL}/sessions/upload`, formData, { + const response = await apiClient.post(`${BASE_URL}/sessions/upload`, formData, { headers: { 'Content-Type': 'multipart/form-data' }, timeout: 30000, // 30秒超时 }); @@ -102,7 +102,7 @@ export const uploadFile = async (file: File): Promise => { * 获取Session元数据 */ export const getSession = async (sessionId: string): Promise => { - const response = await axios.get(`${BASE_URL}/sessions/${sessionId}`); + const response = await apiClient.get(`${BASE_URL}/sessions/${sessionId}`); return response.data; }; @@ -110,7 +110,7 @@ export const getSession = async (sessionId: string): Promise => { * 获取预览数据(⭐ 已改为全量加载) */ export const getPreviewData = async (sessionId: string): Promise => { - const response = await axios.get(`${BASE_URL}/sessions/${sessionId}/preview`); + const response = await apiClient.get(`${BASE_URL}/sessions/${sessionId}/preview`); return response.data; }; @@ -118,7 +118,7 @@ export const getPreviewData = async (sessionId: string): Promise => * 获取完整数据 */ export const getFullData = async (sessionId: string): Promise => { - const response = await axios.get(`${BASE_URL}/sessions/${sessionId}/full`); + const response = await apiClient.get(`${BASE_URL}/sessions/${sessionId}/full`); return response.data; }; @@ -126,7 +126,7 @@ export const getFullData = async (sessionId: string): Promise => { * 更新心跳(延长10分钟) */ export const updateHeartbeat = async (sessionId: string): Promise<{ success: boolean }> => { - const response = await axios.post(`${BASE_URL}/sessions/${sessionId}/heartbeat`); + const response = await apiClient.post(`${BASE_URL}/sessions/${sessionId}/heartbeat`); return response.data; }; @@ -134,7 +134,7 @@ export const updateHeartbeat = async (sessionId: string): Promise<{ success: boo * 删除Session */ export const deleteSession = async (sessionId: string): Promise<{ success: boolean }> => { - const response = await axios.delete(`${BASE_URL}/sessions/${sessionId}`); + const response = await apiClient.delete(`${BASE_URL}/sessions/${sessionId}`); return response.data; }; @@ -154,7 +154,7 @@ export const generateCode = async ( messageId: string; }; }> => { - const response = await axios.post(`${BASE_URL}/ai/generate`, { + const response = await apiClient.post(`${BASE_URL}/ai/generate`, { sessionId, message, }); @@ -176,7 +176,7 @@ export const executeCode = async ( } | null; error?: string; }> => { - const response = await axios.post(`${BASE_URL}/ai/execute`, { + const response = await apiClient.post(`${BASE_URL}/ai/execute`, { sessionId, code, messageId, @@ -192,7 +192,7 @@ export const processMessage = async ( message: string, maxRetries: number = 3 ): Promise => { - const response = await axios.post( + const response = await apiClient.post( `${BASE_URL}/ai/process`, { sessionId, @@ -213,7 +213,7 @@ export const getChatHistory = async ( sessionId: string, limit: number = 10 ): Promise => { - const response = await axios.get(`${BASE_URL}/ai/history/${sessionId}?limit=${limit}`); + const response = await apiClient.get(`${BASE_URL}/ai/history/${sessionId}?limit=${limit}`); return response.data; }; @@ -238,7 +238,7 @@ export const getSessionStatus = async ( }; }> => { const params = jobId ? { jobId } : {}; - const response = await axios.get(`${BASE_URL}/sessions/${sessionId}/status`, { params }); + const response = await apiClient.get(`${BASE_URL}/sessions/${sessionId}/status`, { params }); return response.data; }; diff --git a/frontend-v2/src/modules/dc/hooks/useAssets.ts b/frontend-v2/src/modules/dc/hooks/useAssets.ts index e3458afe..edd5c23e 100644 --- a/frontend-v2/src/modules/dc/hooks/useAssets.ts +++ b/frontend-v2/src/modules/dc/hooks/useAssets.ts @@ -150,6 +150,9 @@ 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 71f3ff50..8ad9e0de 100644 --- a/frontend-v2/src/modules/dc/hooks/useRecentTasks.ts +++ b/frontend-v2/src/modules/dc/hooks/useRecentTasks.ts @@ -140,6 +140,9 @@ export const useRecentTasks = () => { + + + diff --git a/frontend-v2/src/modules/dc/pages/tool-c/components/DropnaDialog.tsx b/frontend-v2/src/modules/dc/pages/tool-c/components/DropnaDialog.tsx index 727b4ef9..1596a17e 100644 --- a/frontend-v2/src/modules/dc/pages/tool-c/components/DropnaDialog.tsx +++ b/frontend-v2/src/modules/dc/pages/tool-c/components/DropnaDialog.tsx @@ -339,6 +339,9 @@ export default DropnaDialog; + + + diff --git a/frontend-v2/src/modules/dc/pages/tool-c/components/MetricTimePanel.tsx b/frontend-v2/src/modules/dc/pages/tool-c/components/MetricTimePanel.tsx index 4bf79e56..29fda690 100644 --- a/frontend-v2/src/modules/dc/pages/tool-c/components/MetricTimePanel.tsx +++ b/frontend-v2/src/modules/dc/pages/tool-c/components/MetricTimePanel.tsx @@ -424,6 +424,9 @@ export default MetricTimePanel; + + + diff --git a/frontend-v2/src/modules/dc/pages/tool-c/components/PivotPanel.tsx b/frontend-v2/src/modules/dc/pages/tool-c/components/PivotPanel.tsx index ef628685..25135ff1 100644 --- a/frontend-v2/src/modules/dc/pages/tool-c/components/PivotPanel.tsx +++ b/frontend-v2/src/modules/dc/pages/tool-c/components/PivotPanel.tsx @@ -310,6 +310,9 @@ export default PivotPanel; + + + 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 567ef38d..d0eb5fc5 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 @@ -110,6 +110,9 @@ 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 6784ccc9..05f8c161 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 @@ -102,6 +102,9 @@ export interface DataStats { + + + diff --git a/frontend-v2/src/modules/dc/types/portal.ts b/frontend-v2/src/modules/dc/types/portal.ts index 7a69e20f..d20e634d 100644 --- a/frontend-v2/src/modules/dc/types/portal.ts +++ b/frontend-v2/src/modules/dc/types/portal.ts @@ -98,6 +98,9 @@ export type AssetTabType = 'all' | 'processed' | 'raw'; + + + diff --git a/frontend-v2/src/modules/pkb/api/knowledgeBaseApi.ts b/frontend-v2/src/modules/pkb/api/knowledgeBaseApi.ts index 077e76b5..7c6ff332 100644 --- a/frontend-v2/src/modules/pkb/api/knowledgeBaseApi.ts +++ b/frontend-v2/src/modules/pkb/api/knowledgeBaseApi.ts @@ -4,6 +4,7 @@ */ import axios from 'axios'; +import { getAccessToken } from '../../../framework/auth/api'; const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000'; @@ -15,6 +16,18 @@ const api = axios.create({ }, }); +// 请求拦截器 - 自动添加 Authorization header +api.interceptors.request.use( + (config) => { + const token = getAccessToken(); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + (error) => Promise.reject(error) +); + /** * 知识库类型定义 */ @@ -221,3 +234,4 @@ export const documentSelectionApi = { + diff --git a/frontend-v2/src/modules/pkb/pages/DashboardPage.tsx b/frontend-v2/src/modules/pkb/pages/DashboardPage.tsx index 0edd84c1..2814d8ea 100644 --- a/frontend-v2/src/modules/pkb/pages/DashboardPage.tsx +++ b/frontend-v2/src/modules/pkb/pages/DashboardPage.tsx @@ -102,7 +102,9 @@ const DashboardPage: React.FC = () => { const handleCreateSubmit = async () => { try { - const kb = await createKnowledgeBase(formData.name, formData.department); + // 后端期望 description 字段,将 department 作为描述的一部分 + const description = `${formData.department || ''}科 知识库`; + const kb = await createKnowledgeBase(formData.name, description); message.success('知识库创建成功!'); setIsModalOpen(false); navigate(`/knowledge-base/workspace/${kb.id}`); @@ -111,32 +113,7 @@ const DashboardPage: React.FC = () => { } }; - // 模拟文件上传 - const simulateUpload = () => { - const newFile = { - id: Math.random().toString(), - name: `New_Clinical_Protocol_v${files.length + 1}.pdf`, - size: "3.5 MB", - status: 'uploading', - progress: 0 - }; - setFiles(prev => [...prev, newFile]); - - let progress = 0; - const interval = setInterval(() => { - progress += 2; - setFiles(prev => prev.map(f => { - if (f.id !== newFile.id) return f; - let status = 'uploading'; - if (progress > 15) status = 'analyzing_layout'; - if (progress > 50) status = 'extracting_table'; - if (progress > 85) status = 'indexing'; - if (progress >= 100) status = 'ready'; - return { ...f, progress: Math.min(progress, 100), status }; - })); - if (progress >= 100) clearInterval(interval); - }, 100); - }; + // 删除模拟上传功能,Step 3留作后续实现真实上传组件 const getStatusText = (status: string) => { switch (status) { @@ -350,14 +327,14 @@ const DashboardPage: React.FC = () => { {createStep === 3 && (
-
- +
+
-

点击上传 PDF 文件

-

支持高清 PDF 及扫描件 (MinerU 深度解析引擎已就绪)

+

上传知识资产

+

知识库创建后,可在工作台中上传文档

+

💡 提示:点击下方"完成"按钮进入工作台,在"知识资产"Tab中上传PDF文档

{files.length > 0 && ( diff --git a/frontend-v2/src/modules/pkb/pages/KnowledgePage.tsx b/frontend-v2/src/modules/pkb/pages/KnowledgePage.tsx index 61036fbb..cd5dbc13 100644 --- a/frontend-v2/src/modules/pkb/pages/KnowledgePage.tsx +++ b/frontend-v2/src/modules/pkb/pages/KnowledgePage.tsx @@ -289,3 +289,6 @@ export default KnowledgePage; + + + diff --git a/frontend-v2/src/modules/pkb/stores/useKnowledgeBaseStore.ts b/frontend-v2/src/modules/pkb/stores/useKnowledgeBaseStore.ts index a2de3d14..cbe62a7e 100644 --- a/frontend-v2/src/modules/pkb/stores/useKnowledgeBaseStore.ts +++ b/frontend-v2/src/modules/pkb/stores/useKnowledgeBaseStore.ts @@ -227,3 +227,6 @@ export const useKnowledgeBaseStore = create((set, get) => ({ + + + diff --git a/frontend-v2/src/modules/pkb/types/workspace.ts b/frontend-v2/src/modules/pkb/types/workspace.ts index b881809f..3d0ac8c8 100644 --- a/frontend-v2/src/modules/pkb/types/workspace.ts +++ b/frontend-v2/src/modules/pkb/types/workspace.ts @@ -44,3 +44,6 @@ export interface BatchTemplate { + + + diff --git a/frontend-v2/src/modules/rvw/api/index.ts b/frontend-v2/src/modules/rvw/api/index.ts index 12771477..b049b315 100644 --- a/frontend-v2/src/modules/rvw/api/index.ts +++ b/frontend-v2/src/modules/rvw/api/index.ts @@ -1,7 +1,7 @@ /** * RVW模块API */ -import axios from 'axios'; +import apiClient from '../../../common/api/axios'; import type { ReviewTask, ReviewReport, ApiResponse, AgentType } from '../types'; const API_BASE = '/api/v2/rvw'; @@ -9,7 +9,7 @@ const API_BASE = '/api/v2/rvw'; // 获取任务列表 export async function getTasks(status?: string): Promise { const params = status && status !== 'all' ? { status } : {}; - const response = await axios.get>(`${API_BASE}/tasks`, { params }); + const response = await apiClient.get>(`${API_BASE}/tasks`, { params }); return response.data.data || []; } @@ -21,7 +21,7 @@ export async function uploadManuscript(file: File, selectedAgents?: AgentType[]) formData.append('selectedAgents', JSON.stringify(selectedAgents)); } - const response = await axios.post>(`${API_BASE}/tasks`, formData, { + const response = await apiClient.post>(`${API_BASE}/tasks`, formData, { headers: { 'Content-Type': 'multipart/form-data' }, }); @@ -34,19 +34,19 @@ export async function uploadManuscript(file: File, selectedAgents?: AgentType[]) // 获取任务详情 export async function getTask(taskId: string): Promise { - const response = await axios.get>(`${API_BASE}/tasks/${taskId}`); + const response = await apiClient.get>(`${API_BASE}/tasks/${taskId}`); return response.data.data!; } // 获取任务报告 export async function getTaskReport(taskId: string): Promise { - const response = await axios.get>(`${API_BASE}/tasks/${taskId}/report`); + const response = await apiClient.get>(`${API_BASE}/tasks/${taskId}/report`); return response.data.data!; } // 运行审查任务(返回jobId供轮询) export async function runTask(taskId: string, agents: AgentType[]): Promise<{ taskId: string; jobId: string }> { - const response = await axios.post>(`${API_BASE}/tasks/${taskId}/run`, { agents }); + const response = await apiClient.post>(`${API_BASE}/tasks/${taskId}/run`, { agents }); if (!response.data.success) { throw new Error(response.data.error || '运行失败'); } @@ -55,7 +55,7 @@ export async function runTask(taskId: string, agents: AgentType[]): Promise<{ ta // 批量运行审查任务 export async function batchRunTasks(taskIds: string[], agents: AgentType[]): Promise { - const response = await axios.post>(`${API_BASE}/tasks/batch/run`, { taskIds, agents }); + const response = await apiClient.post>(`${API_BASE}/tasks/batch/run`, { taskIds, agents }); if (!response.data.success) { throw new Error(response.data.error || '批量运行失败'); } @@ -63,7 +63,7 @@ export async function batchRunTasks(taskIds: string[], agents: AgentType[]): Pro // 删除任务 export async function deleteTask(taskId: string): Promise { - await axios.delete(`${API_BASE}/tasks/${taskId}`); + await apiClient.delete(`${API_BASE}/tasks/${taskId}`); } // 轮询任务状态 @@ -129,3 +129,4 @@ export function formatTime(dateStr: string): string { } + diff --git a/frontend-v2/src/modules/rvw/components/AgentModal.tsx b/frontend-v2/src/modules/rvw/components/AgentModal.tsx index f2acd2b8..7f38b499 100644 --- a/frontend-v2/src/modules/rvw/components/AgentModal.tsx +++ b/frontend-v2/src/modules/rvw/components/AgentModal.tsx @@ -122,3 +122,6 @@ 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 26d64f28..a38d0139 100644 --- a/frontend-v2/src/modules/rvw/components/BatchToolbar.tsx +++ b/frontend-v2/src/modules/rvw/components/BatchToolbar.tsx @@ -42,3 +42,6 @@ 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 72b9ef9c..0d71e05b 100644 --- a/frontend-v2/src/modules/rvw/components/FilterChips.tsx +++ b/frontend-v2/src/modules/rvw/components/FilterChips.tsx @@ -65,3 +65,6 @@ 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 e6fa3d73..1825fa1d 100644 --- a/frontend-v2/src/modules/rvw/components/Header.tsx +++ b/frontend-v2/src/modules/rvw/components/Header.tsx @@ -55,3 +55,6 @@ 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 685218b6..4c0aaee4 100644 --- a/frontend-v2/src/modules/rvw/components/ReportDetail.tsx +++ b/frontend-v2/src/modules/rvw/components/ReportDetail.tsx @@ -109,3 +109,6 @@ 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 5b30d66a..4cb6a192 100644 --- a/frontend-v2/src/modules/rvw/components/ScoreRing.tsx +++ b/frontend-v2/src/modules/rvw/components/ScoreRing.tsx @@ -37,3 +37,6 @@ 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 94c2d930..c8737e47 100644 --- a/frontend-v2/src/modules/rvw/components/Sidebar.tsx +++ b/frontend-v2/src/modules/rvw/components/Sidebar.tsx @@ -72,3 +72,6 @@ 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 78dcb90e..88b70f30 100644 --- a/frontend-v2/src/modules/rvw/components/index.ts +++ b/frontend-v2/src/modules/rvw/components/index.ts @@ -14,3 +14,6 @@ export { default as ReportDetail } from './ReportDetail'; 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 5a23cf82..32aca330 100644 --- a/frontend-v2/src/modules/rvw/pages/Dashboard.tsx +++ b/frontend-v2/src/modules/rvw/pages/Dashboard.tsx @@ -283,3 +283,6 @@ 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 4eecca0f..74e64344 100644 --- a/frontend-v2/src/modules/rvw/styles/index.css +++ b/frontend-v2/src/modules/rvw/styles/index.css @@ -232,3 +232,6 @@ } + + + diff --git a/frontend-v2/src/pages/LoginPage.tsx b/frontend-v2/src/pages/LoginPage.tsx index 32b1d15b..a62694ef 100644 --- a/frontend-v2/src/pages/LoginPage.tsx +++ b/frontend-v2/src/pages/LoginPage.tsx @@ -365,3 +365,6 @@ export default function LoginPage() { ); } + + + diff --git a/frontend-v2/src/pages/admin/tenants/TenantListPage.tsx b/frontend-v2/src/pages/admin/tenants/TenantListPage.tsx index 41c8cecb..786bf2b0 100644 --- a/frontend-v2/src/pages/admin/tenants/TenantListPage.tsx +++ b/frontend-v2/src/pages/admin/tenants/TenantListPage.tsx @@ -335,3 +335,4 @@ const TenantListPage = () => { 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 5def64f1..6aac3abf 100644 --- a/frontend-v2/src/pages/admin/tenants/api/tenantApi.ts +++ b/frontend-v2/src/pages/admin/tenants/api/tenantApi.ts @@ -244,3 +244,4 @@ export async function fetchModuleList(): Promise { return result.data; } + diff --git a/frontend-v2/src/shared/components/index.ts b/frontend-v2/src/shared/components/index.ts index 748be852..3bf4b6c0 100644 --- a/frontend-v2/src/shared/components/index.ts +++ b/frontend-v2/src/shared/components/index.ts @@ -53,6 +53,9 @@ 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 f420505e..826b76d3 100644 --- a/frontend-v2/src/vite-env.d.ts +++ b/frontend-v2/src/vite-env.d.ts @@ -33,6 +33,9 @@ interface ImportMeta { + + + diff --git a/frontend/src/pages/rvw/components/BatchToolbar.tsx b/frontend/src/pages/rvw/components/BatchToolbar.tsx index 6e863ebf..6f231ad6 100644 --- a/frontend/src/pages/rvw/components/BatchToolbar.tsx +++ b/frontend/src/pages/rvw/components/BatchToolbar.tsx @@ -44,3 +44,6 @@ 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 2ee4c35a..54217ec8 100644 --- a/frontend/src/pages/rvw/components/EditorialReport.tsx +++ b/frontend/src/pages/rvw/components/EditorialReport.tsx @@ -109,3 +109,6 @@ 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 3b293d23..3bcf4aca 100644 --- a/frontend/src/pages/rvw/components/FilterChips.tsx +++ b/frontend/src/pages/rvw/components/FilterChips.tsx @@ -67,3 +67,6 @@ 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 a54cf370..2a7b6d4c 100644 --- a/frontend/src/pages/rvw/components/Header.tsx +++ b/frontend/src/pages/rvw/components/Header.tsx @@ -57,3 +57,6 @@ 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 96f142b0..2a3bf8e4 100644 --- a/frontend/src/pages/rvw/components/ReportDetail.tsx +++ b/frontend/src/pages/rvw/components/ReportDetail.tsx @@ -111,3 +111,6 @@ 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 556b130d..08baf6a4 100644 --- a/frontend/src/pages/rvw/components/ScoreRing.tsx +++ b/frontend/src/pages/rvw/components/ScoreRing.tsx @@ -39,3 +39,6 @@ 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 136190c0..37c3560c 100644 --- a/frontend/src/pages/rvw/components/Sidebar.tsx +++ b/frontend/src/pages/rvw/components/Sidebar.tsx @@ -74,3 +74,6 @@ export default function Sidebar({ currentView, onViewChange, onSettingsClick }: + + + diff --git a/frontend/src/pages/rvw/index.ts b/frontend/src/pages/rvw/index.ts index d1c02bd8..10c042fb 100644 --- a/frontend/src/pages/rvw/index.ts +++ b/frontend/src/pages/rvw/index.ts @@ -8,3 +8,6 @@ export * from './api'; + + + diff --git a/frontend/src/pages/rvw/styles.css b/frontend/src/pages/rvw/styles.css index ab39798d..3c5e9fe2 100644 --- a/frontend/src/pages/rvw/styles.css +++ b/frontend/src/pages/rvw/styles.css @@ -234,3 +234,6 @@ + + + diff --git a/git-cleanup-redcap.ps1 b/git-cleanup-redcap.ps1 index 4b1cc970..5b693c52 100644 --- a/git-cleanup-redcap.ps1 +++ b/git-cleanup-redcap.ps1 @@ -30,3 +30,6 @@ Write-Host "Next step: Run the commit command" -ForegroundColor Cyan + + + diff --git a/git-commit-day1.ps1 b/git-commit-day1.ps1 index 2fdb21c5..4880ba9b 100644 --- a/git-commit-day1.ps1 +++ b/git-commit-day1.ps1 @@ -86,3 +86,6 @@ Write-Host "Git commit and push completed!" -ForegroundColor Green + + + diff --git a/git-fix-lock.ps1 b/git-fix-lock.ps1 index 7d2a00f7..187db4a9 100644 --- a/git-fix-lock.ps1 +++ b/git-fix-lock.ps1 @@ -34,3 +34,6 @@ 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 b5f0d66e..0920da11 100644 --- a/python-microservice/operations/__init__.py +++ b/python-microservice/operations/__init__.py @@ -55,6 +55,9 @@ __version__ = '1.0.0' + + + diff --git a/python-microservice/operations/binning.py b/python-microservice/operations/binning.py index 37e99de9..0ed062dd 100644 --- a/python-microservice/operations/binning.py +++ b/python-microservice/operations/binning.py @@ -162,6 +162,9 @@ def apply_binning( + + + diff --git a/python-microservice/operations/filter.py b/python-microservice/operations/filter.py index f2e12381..540d1428 100644 --- a/python-microservice/operations/filter.py +++ b/python-microservice/operations/filter.py @@ -148,6 +148,9 @@ def apply_filter( + + + diff --git a/python-microservice/operations/recode.py b/python-microservice/operations/recode.py index a4125218..5c1818b4 100644 --- a/python-microservice/operations/recode.py +++ b/python-microservice/operations/recode.py @@ -118,6 +118,9 @@ def apply_recode( + + + diff --git a/recover_dc_code.py b/recover_dc_code.py index 92b3806c..76bed937 100644 --- a/recover_dc_code.py +++ b/recover_dc_code.py @@ -262,6 +262,9 @@ if __name__ == "__main__": + + + diff --git a/redcap-docker-dev/.gitattributes b/redcap-docker-dev/.gitattributes index 84cfd5e4..2aedd623 100644 --- a/redcap-docker-dev/.gitattributes +++ b/redcap-docker-dev/.gitattributes @@ -46,3 +46,6 @@ + + + diff --git a/redcap-docker-dev/.gitignore b/redcap-docker-dev/.gitignore index d2d8be21..cdc6358c 100644 --- a/redcap-docker-dev/.gitignore +++ b/redcap-docker-dev/.gitignore @@ -77,3 +77,6 @@ Desktop.ini + + + diff --git a/redcap-docker-dev/README.md b/redcap-docker-dev/README.md index 3c244b15..9e77c75b 100644 --- a/redcap-docker-dev/README.md +++ b/redcap-docker-dev/README.md @@ -378,3 +378,6 @@ 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 83541fd8..f7667446 100644 --- a/redcap-docker-dev/docker-compose.prod.yml +++ b/redcap-docker-dev/docker-compose.prod.yml @@ -139,3 +139,6 @@ volumes: + + + diff --git a/redcap-docker-dev/docker-compose.yml b/redcap-docker-dev/docker-compose.yml index 3e3caaa1..2874e5f9 100644 --- a/redcap-docker-dev/docker-compose.yml +++ b/redcap-docker-dev/docker-compose.yml @@ -137,3 +137,6 @@ volumes: + + + diff --git a/redcap-docker-dev/env.template b/redcap-docker-dev/env.template index 2b89c375..6b2238eb 100644 --- a/redcap-docker-dev/env.template +++ b/redcap-docker-dev/env.template @@ -73,3 +73,6 @@ PMA_UPLOAD_LIMIT=50M + + + diff --git a/redcap-docker-dev/scripts/clean-redcap.ps1 b/redcap-docker-dev/scripts/clean-redcap.ps1 index 6373f257..b7461012 100644 --- a/redcap-docker-dev/scripts/clean-redcap.ps1 +++ b/redcap-docker-dev/scripts/clean-redcap.ps1 @@ -81,3 +81,6 @@ Write-Host "" + + + diff --git a/redcap-docker-dev/scripts/create-redcap-password.php b/redcap-docker-dev/scripts/create-redcap-password.php index 06ea7b62..36c17f4d 100644 --- a/redcap-docker-dev/scripts/create-redcap-password.php +++ b/redcap-docker-dev/scripts/create-redcap-password.php @@ -59,3 +59,6 @@ try { + + + diff --git a/redcap-docker-dev/scripts/logs-redcap.ps1 b/redcap-docker-dev/scripts/logs-redcap.ps1 index 4cf6b3fb..c2e85e0e 100644 --- a/redcap-docker-dev/scripts/logs-redcap.ps1 +++ b/redcap-docker-dev/scripts/logs-redcap.ps1 @@ -72,3 +72,6 @@ Write-Host "" + + + diff --git a/redcap-docker-dev/scripts/reset-admin-password.php b/redcap-docker-dev/scripts/reset-admin-password.php index 0d3adc13..a7e2a8c3 100644 --- a/redcap-docker-dev/scripts/reset-admin-password.php +++ b/redcap-docker-dev/scripts/reset-admin-password.php @@ -35,3 +35,6 @@ if ($result) { + + + diff --git a/redcap-docker-dev/scripts/start-redcap.ps1 b/redcap-docker-dev/scripts/start-redcap.ps1 index 224a162f..056d982b 100644 --- a/redcap-docker-dev/scripts/start-redcap.ps1 +++ b/redcap-docker-dev/scripts/start-redcap.ps1 @@ -57,3 +57,6 @@ if ($LASTEXITCODE -eq 0) { + + + diff --git a/redcap-docker-dev/scripts/stop-redcap.ps1 b/redcap-docker-dev/scripts/stop-redcap.ps1 index 586150f7..9031afd6 100644 --- a/redcap-docker-dev/scripts/stop-redcap.ps1 +++ b/redcap-docker-dev/scripts/stop-redcap.ps1 @@ -43,3 +43,6 @@ if ($LASTEXITCODE -eq 0) { + + + diff --git a/run_recovery.ps1 b/run_recovery.ps1 index 25da6be6..b8ec9d27 100644 --- a/run_recovery.ps1 +++ b/run_recovery.ps1 @@ -86,6 +86,9 @@ Write-Host "==================================================================== + + + diff --git a/tests/QUICKSTART_快速开始.md b/tests/QUICKSTART_快速开始.md index b51dd8cf..e1872a21 100644 --- a/tests/QUICKSTART_快速开始.md +++ b/tests/QUICKSTART_快速开始.md @@ -133,6 +133,9 @@ INFO: Uvicorn running on http://0.0.0.0:8001 + + + diff --git a/tests/README_测试说明.md b/tests/README_测试说明.md index bb41fe03..d7527ca6 100644 --- a/tests/README_测试说明.md +++ b/tests/README_测试说明.md @@ -289,6 +289,9 @@ df_numeric.to_excel('test_data/numeric_test.xlsx', index=False) + + + diff --git a/tests/run_tests.bat b/tests/run_tests.bat index d4aabe80..639b4a90 100644 --- a/tests/run_tests.bat +++ b/tests/run_tests.bat @@ -84,6 +84,9 @@ pause + + + diff --git a/tests/run_tests.sh b/tests/run_tests.sh index 29d5a49e..e2f5080a 100644 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -80,6 +80,9 @@ echo "========================================" + + + diff --git a/快速部署到SAE.md b/快速部署到SAE.md index 9f7a96a4..79dca48c 100644 --- a/快速部署到SAE.md +++ b/快速部署到SAE.md @@ -345,6 +345,9 @@ OSS AccessKeySecret:_______________ + + + diff --git a/部署检查清单.md b/部署检查清单.md index f19af935..839acfd4 100644 --- a/部署检查清单.md +++ b/部署检查清单.md @@ -381,6 +381,9 @@ OSS配置: + + +