From 5e2257ab7851e2a82730bb6c6272a73ed35654a4 Mon Sep 17 00:00:00 2001 From: lipan Date: Mon, 11 May 2026 23:17:14 +0800 Subject: [PATCH] Initial commit --- .env.example | 23 + .gitignore | 151 + client/src/pages/Home.tsx | 712 ++ cp_file.sh | 27 + docker-compose.gpu-v1.yml | 15 + docker-compose.gpu-v2.yml | 25 + docker-compose.gpu.compose-v2.yml | 16 + docker-compose.gpu.yml | 12 + docker-compose.yml | 134 + docker/Dockerfile.dev | 43 + docker/Dockerfile.dev.gpu | 89 + docker/env-config.md | 41 + docker/mysql/init.sql | 166 + docker/nginx/nginx.conf | 152 + load-images.sh | 47 + src/.dockerignore | 56 + src/.gitignore | 107 + src/.gitkeep | 0 src/.prettierignore | 35 + src/.prettierrc | 15 + src/DEPLOYMENT.md | 264 + src/Dockerfile | 114 + src/client/index.html | 22 + src/client/public/.gitkeep | 0 .../public/__manus__/debug-collector.js | 821 ++ src/client/src/App.tsx | 42 + src/client/src/_core/hooks/useAuth.ts | 84 + src/client/src/components/AIChatBox.tsx | 335 + src/client/src/components/AnalysisPanel.tsx | 464 + .../src/components/CADModelSelector.tsx | 345 + src/client/src/components/ChunkVisualizer.tsx | 69 + .../src/components/CompressionResults.tsx | 114 + .../src/components/CompressionVisual.tsx | 127 + src/client/src/components/ControlPanel.tsx | 252 + src/client/src/components/DashboardLayout.tsx | 264 + .../components/DashboardLayoutSkeleton.tsx | 46 + src/client/src/components/ErrorBoundary.tsx | 62 + src/client/src/components/FileManager.tsx | 323 + src/client/src/components/FileUploader.tsx | 252 + src/client/src/components/HistoryBrowser.tsx | 219 + src/client/src/components/InfoPanel.tsx | 258 + src/client/src/components/ManusDialog.tsx | 89 + src/client/src/components/Map.tsx | 155 + .../src/components/MeasurementResults.tsx | 110 + src/client/src/components/ModelManager.tsx | 177 + .../src/components/PointCloudSelector.tsx | 397 + .../src/components/PointCloudViewer.tsx | 1785 +++ src/client/src/components/RealtimeReport.tsx | 174 + src/client/src/components/TaskManager.tsx | 259 + src/client/src/components/VehicleTree.tsx | 291 + src/client/src/components/WorkflowManager.tsx | 111 + .../src/components/WorkflowProgress.tsx | 111 + src/client/src/components/ui/accordion.tsx | 64 + src/client/src/components/ui/alert-dialog.tsx | 155 + src/client/src/components/ui/alert.tsx | 66 + src/client/src/components/ui/aspect-ratio.tsx | 9 + src/client/src/components/ui/avatar.tsx | 51 + src/client/src/components/ui/badge.tsx | 46 + src/client/src/components/ui/breadcrumb.tsx | 109 + src/client/src/components/ui/button-group.tsx | 83 + src/client/src/components/ui/button.tsx | 60 + src/client/src/components/ui/calendar.tsx | 211 + src/client/src/components/ui/card.tsx | 92 + src/client/src/components/ui/carousel.tsx | 239 + src/client/src/components/ui/chart.tsx | 355 + src/client/src/components/ui/checkbox.tsx | 30 + src/client/src/components/ui/collapsible.tsx | 31 + src/client/src/components/ui/command.tsx | 184 + src/client/src/components/ui/context-menu.tsx | 250 + src/client/src/components/ui/dialog.tsx | 209 + src/client/src/components/ui/drawer.tsx | 133 + .../src/components/ui/dropdown-menu.tsx | 255 + src/client/src/components/ui/empty.tsx | 104 + src/client/src/components/ui/field.tsx | 242 + src/client/src/components/ui/form.tsx | 168 + src/client/src/components/ui/hover-card.tsx | 42 + src/client/src/components/ui/input-group.tsx | 168 + src/client/src/components/ui/input-otp.tsx | 75 + src/client/src/components/ui/input.tsx | 70 + src/client/src/components/ui/item.tsx | 193 + src/client/src/components/ui/kbd.tsx | 28 + src/client/src/components/ui/label.tsx | 22 + src/client/src/components/ui/menubar.tsx | 274 + .../src/components/ui/navigation-menu.tsx | 168 + src/client/src/components/ui/pagination.tsx | 127 + src/client/src/components/ui/popover.tsx | 46 + src/client/src/components/ui/progress.tsx | 29 + src/client/src/components/ui/radio-group.tsx | 43 + src/client/src/components/ui/resizable.tsx | 54 + src/client/src/components/ui/scroll-area.tsx | 56 + src/client/src/components/ui/select.tsx | 185 + src/client/src/components/ui/separator.tsx | 26 + src/client/src/components/ui/sheet.tsx | 139 + src/client/src/components/ui/sidebar.tsx | 734 ++ src/client/src/components/ui/skeleton.tsx | 13 + src/client/src/components/ui/slider.tsx | 61 + src/client/src/components/ui/sonner.tsx | 23 + src/client/src/components/ui/spinner.tsx | 16 + src/client/src/components/ui/switch.tsx | 29 + src/client/src/components/ui/table.tsx | 114 + src/client/src/components/ui/tabs.tsx | 64 + src/client/src/components/ui/textarea.tsx | 67 + src/client/src/components/ui/toggle-group.tsx | 73 + src/client/src/components/ui/toggle.tsx | 45 + src/client/src/components/ui/tooltip.tsx | 59 + src/client/src/const.ts | 23 + src/client/src/contexts/ThemeContext.tsx | 64 + src/client/src/hooks/useComposition.ts | 81 + src/client/src/hooks/useMobile.tsx | 21 + src/client/src/hooks/usePersistFn.ts | 20 + src/client/src/index.css | 621 ++ src/client/src/lib/trpc.ts | 4 + src/client/src/lib/utils.ts | 6 + src/client/src/main.tsx | 99 + src/client/src/pages/ComponentShowcase.tsx | 1437 +++ src/client/src/pages/Home.tsx | 640 ++ src/client/src/pages/NotFound.tsx | 52 + src/components.json | 19 + src/docker-compose.dev.yml | 154 + src/docker-compose.gpu.compose-v2.yml | 13 + src/docker-compose.gpu.yml | 15 + src/docker-compose.yml | 140 + src/docker/Dockerfile.dev | 43 + src/docker/Dockerfile.dev.gpu | 89 + src/docker/env-config.md | 41 + src/docker/mysql/init.sql | 166 + src/docker/nginx/nginx.conf | 152 + src/drizzle.config.ts | 15 + src/drizzle/0000_groovy_crusher_hogan.sql | 13 + src/drizzle/0001_tan_jamie_braddock.sql | 76 + src/drizzle/0002_vehicle_hierarchy.sql | 68 + src/drizzle/0003_vehicle_hierarchy_repair.sql | 62 + ...0004_task_analysis_smooth_heatmap_enum.sql | 10 + ...0005_task_analysis_assembly_inspection.sql | 9 + .../0006_task_analysis_obj_deviation.sql | 10 + src/drizzle/meta/0000_snapshot.json | 110 + src/drizzle/meta/0001_snapshot.json | 619 ++ src/drizzle/meta/_journal.json | 20 + src/drizzle/migrations/.gitkeep | 0 src/drizzle/relations.ts | 1 + src/drizzle/schema.ts | 121 + src/package.json | 123 + src/patches/wouter@3.7.1.patch | 28 + src/pnpm-lock.yaml | 9685 +++++++++++++++++ src/requirements-assembly-gpu.txt | 7 + src/requirements-geometry.txt | 5 + src/scripts/alter-task-analysis-type-enum.sql | 11 + src/scripts/assembly_nearest_gpu.py | 143 + src/scripts/deviation_heatmap_gpu.py | 267 + src/scripts/snapshot-app-dev-image.sh | 51 + src/scripts/step_parse_mvp.py | 94 + src/server/_core/context.ts | 28 + src/server/_core/cookies.ts | 48 + src/server/_core/dataApi.ts | 64 + src/server/_core/env.ts | 14 + src/server/_core/imageGeneration.ts | 92 + src/server/_core/index.ts | 157 + src/server/_core/llm.ts | 332 + src/server/_core/map.ts | 319 + src/server/_core/notification.ts | 114 + src/server/_core/oauth.ts | 53 + src/server/_core/sdk.ts | 346 + src/server/_core/systemRouter.ts | 29 + src/server/_core/trpc.ts | 45 + src/server/_core/types/cookie.d.ts | 6 + src/server/_core/types/manusTypes.ts | 69 + src/server/_core/vite.ts | 86 + src/server/_core/voiceTranscription.ts | 284 + src/server/assemblyGpuNearest.ts | 115 + src/server/assemblyInspectionCompute.ts | 253 + src/server/assemblyInspectionJob.ts | 255 + src/server/assemblyInspectionJobRunner.ts | 36 + src/server/auth.logout.test.ts | 62 + src/server/cadFilePick.ts | 33 + src/server/cadHeatmapCompute.ts | 305 + src/server/db.ts | 327 + src/server/deviationGpuCompute.ts | 121 + src/server/deviationHeatmapJob.ts | 132 + src/server/deviationHeatmapJobRunner.ts | 21 + src/server/fileRoutes.ts | 324 + src/server/pointcloudSample.ts | 77 + src/server/project.test.ts | 217 + src/server/routers.ts | 388 + src/server/sampleTaskPointclouds.ts | 47 + src/server/smoothHeatmapJob.ts | 125 + src/server/smoothHeatmapJobRunner.ts | 26 + src/server/stepParse.ts | 143 + src/server/storage.ts | 102 + src/shared/_core/errors.ts | 19 + src/shared/cadHeatmapMath.ts | 290 + src/shared/const.ts | 5 + src/shared/types.ts | 7 + src/todo.md | 53 + src/tsconfig.json | 23 + src/vite.config.ts | 247 + src/vitest.config.ts | 19 + src/软件自主化率评估报告.md | 214 + 部署说明.md | 160 + 198 files changed, 37871 insertions(+) create mode 100755 .env.example create mode 100644 .gitignore create mode 100755 client/src/pages/Home.tsx create mode 100755 cp_file.sh create mode 100755 docker-compose.gpu-v1.yml create mode 100755 docker-compose.gpu-v2.yml create mode 100755 docker-compose.gpu.compose-v2.yml create mode 100755 docker-compose.gpu.yml create mode 100755 docker-compose.yml create mode 100755 docker/Dockerfile.dev create mode 100755 docker/Dockerfile.dev.gpu create mode 100755 docker/env-config.md create mode 100755 docker/mysql/init.sql create mode 100755 docker/nginx/nginx.conf create mode 100755 load-images.sh create mode 100644 src/.dockerignore create mode 100644 src/.gitignore create mode 100644 src/.gitkeep create mode 100644 src/.prettierignore create mode 100644 src/.prettierrc create mode 100644 src/DEPLOYMENT.md create mode 100644 src/Dockerfile create mode 100644 src/client/index.html create mode 100644 src/client/public/.gitkeep create mode 100644 src/client/public/__manus__/debug-collector.js create mode 100644 src/client/src/App.tsx create mode 100644 src/client/src/_core/hooks/useAuth.ts create mode 100644 src/client/src/components/AIChatBox.tsx create mode 100644 src/client/src/components/AnalysisPanel.tsx create mode 100644 src/client/src/components/CADModelSelector.tsx create mode 100644 src/client/src/components/ChunkVisualizer.tsx create mode 100644 src/client/src/components/CompressionResults.tsx create mode 100644 src/client/src/components/CompressionVisual.tsx create mode 100644 src/client/src/components/ControlPanel.tsx create mode 100644 src/client/src/components/DashboardLayout.tsx create mode 100644 src/client/src/components/DashboardLayoutSkeleton.tsx create mode 100644 src/client/src/components/ErrorBoundary.tsx create mode 100644 src/client/src/components/FileManager.tsx create mode 100644 src/client/src/components/FileUploader.tsx create mode 100644 src/client/src/components/HistoryBrowser.tsx create mode 100644 src/client/src/components/InfoPanel.tsx create mode 100644 src/client/src/components/ManusDialog.tsx create mode 100644 src/client/src/components/Map.tsx create mode 100644 src/client/src/components/MeasurementResults.tsx create mode 100644 src/client/src/components/ModelManager.tsx create mode 100644 src/client/src/components/PointCloudSelector.tsx create mode 100644 src/client/src/components/PointCloudViewer.tsx create mode 100644 src/client/src/components/RealtimeReport.tsx create mode 100644 src/client/src/components/TaskManager.tsx create mode 100644 src/client/src/components/VehicleTree.tsx create mode 100644 src/client/src/components/WorkflowManager.tsx create mode 100644 src/client/src/components/WorkflowProgress.tsx create mode 100644 src/client/src/components/ui/accordion.tsx create mode 100644 src/client/src/components/ui/alert-dialog.tsx create mode 100644 src/client/src/components/ui/alert.tsx create mode 100644 src/client/src/components/ui/aspect-ratio.tsx create mode 100644 src/client/src/components/ui/avatar.tsx create mode 100644 src/client/src/components/ui/badge.tsx create mode 100644 src/client/src/components/ui/breadcrumb.tsx create mode 100644 src/client/src/components/ui/button-group.tsx create mode 100644 src/client/src/components/ui/button.tsx create mode 100644 src/client/src/components/ui/calendar.tsx create mode 100644 src/client/src/components/ui/card.tsx create mode 100644 src/client/src/components/ui/carousel.tsx create mode 100644 src/client/src/components/ui/chart.tsx create mode 100644 src/client/src/components/ui/checkbox.tsx create mode 100644 src/client/src/components/ui/collapsible.tsx create mode 100644 src/client/src/components/ui/command.tsx create mode 100644 src/client/src/components/ui/context-menu.tsx create mode 100644 src/client/src/components/ui/dialog.tsx create mode 100644 src/client/src/components/ui/drawer.tsx create mode 100644 src/client/src/components/ui/dropdown-menu.tsx create mode 100644 src/client/src/components/ui/empty.tsx create mode 100644 src/client/src/components/ui/field.tsx create mode 100644 src/client/src/components/ui/form.tsx create mode 100644 src/client/src/components/ui/hover-card.tsx create mode 100644 src/client/src/components/ui/input-group.tsx create mode 100644 src/client/src/components/ui/input-otp.tsx create mode 100644 src/client/src/components/ui/input.tsx create mode 100644 src/client/src/components/ui/item.tsx create mode 100644 src/client/src/components/ui/kbd.tsx create mode 100644 src/client/src/components/ui/label.tsx create mode 100644 src/client/src/components/ui/menubar.tsx create mode 100644 src/client/src/components/ui/navigation-menu.tsx create mode 100644 src/client/src/components/ui/pagination.tsx create mode 100644 src/client/src/components/ui/popover.tsx create mode 100644 src/client/src/components/ui/progress.tsx create mode 100644 src/client/src/components/ui/radio-group.tsx create mode 100644 src/client/src/components/ui/resizable.tsx create mode 100644 src/client/src/components/ui/scroll-area.tsx create mode 100644 src/client/src/components/ui/select.tsx create mode 100644 src/client/src/components/ui/separator.tsx create mode 100644 src/client/src/components/ui/sheet.tsx create mode 100644 src/client/src/components/ui/sidebar.tsx create mode 100644 src/client/src/components/ui/skeleton.tsx create mode 100644 src/client/src/components/ui/slider.tsx create mode 100644 src/client/src/components/ui/sonner.tsx create mode 100644 src/client/src/components/ui/spinner.tsx create mode 100644 src/client/src/components/ui/switch.tsx create mode 100644 src/client/src/components/ui/table.tsx create mode 100644 src/client/src/components/ui/tabs.tsx create mode 100644 src/client/src/components/ui/textarea.tsx create mode 100644 src/client/src/components/ui/toggle-group.tsx create mode 100644 src/client/src/components/ui/toggle.tsx create mode 100644 src/client/src/components/ui/tooltip.tsx create mode 100644 src/client/src/const.ts create mode 100644 src/client/src/contexts/ThemeContext.tsx create mode 100644 src/client/src/hooks/useComposition.ts create mode 100644 src/client/src/hooks/useMobile.tsx create mode 100644 src/client/src/hooks/usePersistFn.ts create mode 100644 src/client/src/index.css create mode 100644 src/client/src/lib/trpc.ts create mode 100644 src/client/src/lib/utils.ts create mode 100644 src/client/src/main.tsx create mode 100644 src/client/src/pages/ComponentShowcase.tsx create mode 100644 src/client/src/pages/Home.tsx create mode 100644 src/client/src/pages/NotFound.tsx create mode 100644 src/components.json create mode 100644 src/docker-compose.dev.yml create mode 100644 src/docker-compose.gpu.compose-v2.yml create mode 100644 src/docker-compose.gpu.yml create mode 100644 src/docker-compose.yml create mode 100644 src/docker/Dockerfile.dev create mode 100644 src/docker/Dockerfile.dev.gpu create mode 100644 src/docker/env-config.md create mode 100644 src/docker/mysql/init.sql create mode 100644 src/docker/nginx/nginx.conf create mode 100644 src/drizzle.config.ts create mode 100644 src/drizzle/0000_groovy_crusher_hogan.sql create mode 100644 src/drizzle/0001_tan_jamie_braddock.sql create mode 100644 src/drizzle/0002_vehicle_hierarchy.sql create mode 100644 src/drizzle/0003_vehicle_hierarchy_repair.sql create mode 100644 src/drizzle/0004_task_analysis_smooth_heatmap_enum.sql create mode 100644 src/drizzle/0005_task_analysis_assembly_inspection.sql create mode 100644 src/drizzle/0006_task_analysis_obj_deviation.sql create mode 100644 src/drizzle/meta/0000_snapshot.json create mode 100644 src/drizzle/meta/0001_snapshot.json create mode 100644 src/drizzle/meta/_journal.json create mode 100644 src/drizzle/migrations/.gitkeep create mode 100644 src/drizzle/relations.ts create mode 100644 src/drizzle/schema.ts create mode 100644 src/package.json create mode 100644 src/patches/wouter@3.7.1.patch create mode 100644 src/pnpm-lock.yaml create mode 100644 src/requirements-assembly-gpu.txt create mode 100644 src/requirements-geometry.txt create mode 100644 src/scripts/alter-task-analysis-type-enum.sql create mode 100644 src/scripts/assembly_nearest_gpu.py create mode 100644 src/scripts/deviation_heatmap_gpu.py create mode 100755 src/scripts/snapshot-app-dev-image.sh create mode 100644 src/scripts/step_parse_mvp.py create mode 100644 src/server/_core/context.ts create mode 100644 src/server/_core/cookies.ts create mode 100644 src/server/_core/dataApi.ts create mode 100644 src/server/_core/env.ts create mode 100644 src/server/_core/imageGeneration.ts create mode 100644 src/server/_core/index.ts create mode 100644 src/server/_core/llm.ts create mode 100644 src/server/_core/map.ts create mode 100644 src/server/_core/notification.ts create mode 100644 src/server/_core/oauth.ts create mode 100644 src/server/_core/sdk.ts create mode 100644 src/server/_core/systemRouter.ts create mode 100644 src/server/_core/trpc.ts create mode 100644 src/server/_core/types/cookie.d.ts create mode 100644 src/server/_core/types/manusTypes.ts create mode 100644 src/server/_core/vite.ts create mode 100644 src/server/_core/voiceTranscription.ts create mode 100644 src/server/assemblyGpuNearest.ts create mode 100644 src/server/assemblyInspectionCompute.ts create mode 100644 src/server/assemblyInspectionJob.ts create mode 100644 src/server/assemblyInspectionJobRunner.ts create mode 100644 src/server/auth.logout.test.ts create mode 100644 src/server/cadFilePick.ts create mode 100644 src/server/cadHeatmapCompute.ts create mode 100644 src/server/db.ts create mode 100644 src/server/deviationGpuCompute.ts create mode 100644 src/server/deviationHeatmapJob.ts create mode 100644 src/server/deviationHeatmapJobRunner.ts create mode 100644 src/server/fileRoutes.ts create mode 100644 src/server/pointcloudSample.ts create mode 100644 src/server/project.test.ts create mode 100644 src/server/routers.ts create mode 100644 src/server/sampleTaskPointclouds.ts create mode 100644 src/server/smoothHeatmapJob.ts create mode 100644 src/server/smoothHeatmapJobRunner.ts create mode 100644 src/server/stepParse.ts create mode 100644 src/server/storage.ts create mode 100644 src/shared/_core/errors.ts create mode 100644 src/shared/cadHeatmapMath.ts create mode 100644 src/shared/const.ts create mode 100644 src/shared/types.ts create mode 100644 src/todo.md create mode 100644 src/tsconfig.json create mode 100644 src/vite.config.ts create mode 100644 src/vitest.config.ts create mode 100644 src/软件自主化率评估报告.md create mode 100755 部署说明.md diff --git a/.env.example b/.env.example new file mode 100755 index 0000000..9a6bc52 --- /dev/null +++ b/.env.example @@ -0,0 +1,23 @@ +# 宏量点云处理系统 - 环境变量配置 +# 使用方法: cp .env.example .env 并修改以下值 + +# ── 安全 ─────────────────────────────────────────────── +# 【必须修改】JWT 签名密钥,越长越好,生产环境请用随机字符串 +JWT_SECRET=please-change-this-to-a-long-random-string + +# ── 数据库 ───────────────────────────────────────────── +# MySQL root 密码(首次启动时设置,之后不可随意改动) +MYSQL_ROOT_PASSWORD=root_password_change_me +# 应用连接密码(与 docker-compose DATABASE_URL 中一致) +MYSQL_PASSWORD=pointcloud_password + +# ── MinIO 对象存储 ───────────────────────────────────── +MINIO_ROOT_USER=minioadmin +MINIO_ROOT_PASSWORD=minioadmin123 + +# ── 数据目录 ─────────────────────────────────────────── +# 宿主机存放 CAD 与点云文件的目录,会被挂载为容器内 /data +DATA_DIR=./data + +# ── 前端标题 ─────────────────────────────────────────── +VITE_APP_TITLE=宏量点云处理系统 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bca8acd --- /dev/null +++ b/.gitignore @@ -0,0 +1,151 @@ +# Dependencies +node_modules/ +**/node_modules/ +.pnpm-store/ +**/.pnpm-store/ +.npm/ + +# Build outputs +dist/ +build/ +.vite/ +.cache/ +.parcel-cache/ +*.tsbuildinfo + +# Logs +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* +.manus-logs/ +**/.manus-logs/ + +# Environment files +.env +.env.* +!.env.example +!.env.*.example + +# Runtime / temporary files +tmp/ +temp/ +.tmp/ +pids/ +*.pid +*.seed +*.pid.lock +*.bak + +# Test / coverage +coverage/ +*.lcov +.nyc_output/ +test-results/ +playwright-report/ + +# Local databases +*.db +*.sqlite +*.sqlite3 +*.sql.gz +*.sql.zip +dump*.sql +backup*.sql +*_dump.sql +*_backup.sql + +# Docker image exports and offline packages +images/ +*.tar +*.tar.gz +*.tgz +*.zip +*.7z +*.rar +*.image.tar +massive-cloud-deploy-*.tar.gz +release/ +releases/ +offline-package/ +deploy-package/ + +# Runtime data, uploads, processing outputs +data/ +src/data/ +uploads/ +upload/ +downloads/ +processed/ +processing/ +outputs/ +output/ +results/ +result/ +octree/ +lod/ +tiles/ +potree/ +pointclouds/ +point-clouds/ +cad-files/ +models/ +samples/ + +# Point cloud raw/intermediate data +*.pcd +*.ply +*.las +*.laz +*.e57 +*.xyz +*.pts +*.asc +*.bin + +# Large sampled point-cloud text files +points*.txt + +# CAD / mesh / model data +*.stl +*.obj +*.mtl +*.step +*.stp +*.iges +*.igs +*.fbx +*.glb +*.gltf +*.dae +*.ifc + +# Python / algorithm cache and binary outputs +__pycache__/ +*.py[cod] +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.venv/ +venv/ +env/ +*.npy +*.npz +*.pkl +*.joblib + +# Editor / OS +.DS_Store +._* +.Spotlight-V100 +.Trashes +.vscode/ +.idea/ +*.swp +*.swo +*~ +Thumbs.db +ehthumbs.db diff --git a/client/src/pages/Home.tsx b/client/src/pages/Home.tsx new file mode 100755 index 0000000..0d879d5 --- /dev/null +++ b/client/src/pages/Home.tsx @@ -0,0 +1,712 @@ +import { useState, useCallback, useEffect, useRef, useMemo } from 'react'; +import { useAuth } from '@/_core/hooks/useAuth'; +import { getLoginUrl } from '@/const'; +import PointCloudViewer from '@/components/PointCloudViewer'; +import VehicleTree, { TreeSelection } from '@/components/VehicleTree'; +import ModelManager from '@/components/ModelManager'; +import TaskManager from '@/components/TaskManager'; +import AnalysisPanel from '@/components/AnalysisPanel'; +import WorkflowProgress, { type WorkflowState, type WorkflowStepId } from '@/components/WorkflowProgress'; +import { trpc } from '@/lib/trpc'; +import { + LogIn, + LogOut, + Box, + Eye, + EyeOff, + Settings, + Maximize2, + Crosshair, + Train, + Loader2, +} from 'lucide-react'; +import { toast } from 'sonner'; + +export default function Home() { + const { user, loading, isAuthenticated, logout } = useAuth(); + const utils = trpc.useUtils(); + + // ── 树导航选中状态 ───────────────────────────────────────────────────────── + const [selection, setSelection] = useState({ + seriesId: null, + modelId: null, + taskId: null, + }); + + // ── 3D 视图数据 ──────────────────────────────────────────────────────────── + const [cadModelUrl, setCadModelUrl] = useState(); + const [cadModelFormat, setCadModelFormat] = useState(); + const [activeCadName, setActiveCadName] = useState(); + const [cadLoading, setCadLoading] = useState(false); + const [cadLoadProgress, setCadLoadProgress] = useState(0); + + const [pointCloudTaskId, setPointCloudTaskId] = useState(null); + const [pointCloudUrls, setPointCloudUrls] = useState([]); + const [pcLoading, setPcLoading] = useState(false); + const [pcProgress, setPcProgress] = useState(0); + + // ── 视图控制 ─────────────────────────────────────────────────────────────── + const [showHeatmap, setShowHeatmap] = useState(true); + const [showCADModel, setShowCADModel] = useState(true); + const [showWireframe, setShowWireframe] = useState(false); + const [pointSize, setPointSize] = useState(0.03); + /** 点云预览采样预算(非全量);调低可明显减轻卡顿 */ + const [renderPointBudget, setRenderPointBudget] = useState(400_000); + /** 点云:逐点;平滑热力:仅在分析结果就绪后由右侧「在 3D 中查看」切换 */ + const [pointCloudRenderMode, setPointCloudRenderMode] = useState<'points' | 'smoothHeatmap'>('points'); + /** 后台「平滑热力」分析结果 ID,用于拉取顶点色 */ + const [heatmapViewResultId, setHeatmapViewResultId] = useState(null); + const [errorPickEnabled, setErrorPickEnabled] = useState(false); + const [isCreatingTask, setIsCreatingTask] = useState(false); + // 触发分析时的 completed 结果基线数(key=taskId, value=触发时已有的 completed 数) + const [analysisBaselines, setAnalysisBaselines] = useState>(new Map()); + // 用 ref 在 onSuccess 闭包中读取最新 analysisResults,避免过时闭包 + const analysisResultsRef = useRef([]); + const [showLeftPanel, setShowLeftPanel] = useState(true); + const [showRightPanel, setShowRightPanel] = useState(true); + + const [selectedPoint, setSelectedPoint] = useState<{ + position: { x: number; y: number; z: number }; + error: number; + index: number; + } | null>(null); + + const cadModelUrlRef = useRef(undefined); + cadModelUrlRef.current = cadModelUrl; + + // ── 当前车型信息(用于面板标题) ─────────────────────────────────────────── + const { data: activeModel } = trpc.vehicleModel.get.useQuery( + { id: selection.modelId! }, + { enabled: selection.modelId != null } + ); + + /** 平滑热力与装配检测共用同一 mutation,避免重复订阅;勿在 batchTaskId 为空时 disabled,否则点击无任何反馈 */ + const runAnalysisJob = trpc.analysisResult.runAnalysis.useMutation({ + onSuccess: (_data, variables) => { + if (variables.type === 'smooth_heatmap') { + toast.success('已提交平滑热力后台分析,请稍后在右侧「分析结果」中查看;完成后点击「在 3D 中查看」'); + } else if (variables.type === 'assembly_inspection') { + toast.success('已提交装配检测(STEP + 最近零件),请稍后在右侧「分析结果」查看各零件统计'); + } else if (variables.type === 'obj_deviation') { + toast.success('已提交偏差热力分析(STL/OBJ + 点云距离),完成后点击「在 3D 中查看」'); + } + void utils.analysisResult.list.invalidate({ taskId: variables.taskId }); + // 记录此时已有的 completed 数作为基线,只有新增结果才算本次分析完成 + const baselineCount = analysisResultsRef.current.filter((r: any) => r.status === 'completed').length; + setAnalysisBaselines(prev => { + const next = new Map(prev); + if (!next.has(variables.taskId)) next.set(variables.taskId, baselineCount); + return next; + }); + }, + onError: (e) => toast.error(e.message || '提交失败'), + }); + + // 切换车型时清空 CAD,避免沿用上一车型的 URL + useEffect(() => { + setCadModelUrl(undefined); + setCadModelFormat(undefined); + setActiveCadName(undefined); + setCadLoading(false); + setCadLoadProgress(0); + }, [selection.modelId]); + + useEffect(() => { + setHeatmapViewResultId(null); + setPointCloudRenderMode('points'); + setIsCreatingTask(false); + }, [selection.taskId]); + + // ── 回调 ─────────────────────────────────────────────────────────────────── + /** STEP 无浏览器解析:若服务端已有 assembly manifest,则用首件 STL 作为 3D 预览 */ + const resolveCadPreview = useCallback( + async (modelId: number, file: { id: number; name: string; format: string }) => { + const lower = file.name.toLowerCase(); + if (!lower.endsWith('.stp') && !lower.endsWith('.step')) { + return { + url: `/api/files/read/cad/${modelId}/${encodeURIComponent(file.name)}`, + format: file.format, + label: file.name, + }; + } + try { + const res = await utils.cadFile.assemblyManifest.fetch({ modelId, cadFileId: file.id }); + const manifest = res.manifest as { parts?: { stlRelative: string; name: string }[] } | null; + if (res.ready && manifest?.parts && manifest.parts.length > 0) { + const p0 = manifest.parts[0]!; + return { + url: `/api/files/read/cad/${modelId}/${encodeURIComponent( + `.step_parse/${file.id}/${p0.stlRelative}`, + )}`, + format: 'stl', + label: `${file.name} → ${p0.name}`, + }; + } + } catch (e) { + console.warn('[Home] assemblyManifest', e); + } + return null; + }, + [utils.cadFile.assemblyManifest], + ); + + const handleCadProgress = useCallback((p: number) => setCadLoadProgress(p), []); + const handleCadLoaded = useCallback(() => { + setCadLoading(false); + setCadLoadProgress(100); + }, []); + const handleCadLoadError = useCallback((message: string) => { + setCadLoading(false); + setCadLoadProgress(0); + toast.error(message); + }, []); + + const handleSelectCad = useCallback( + async (url: string, format: string, name: string, cadFileId?: number) => { + setCadLoading(true); + setCadLoadProgress(3); + if (selection.modelId != null && cadFileId != null) { + const resolved = await resolveCadPreview(selection.modelId, { id: cadFileId, name, format }); + if (resolved) { + setCadModelUrl(resolved.url); + setCadModelFormat(resolved.format); + setActiveCadName(resolved.label); + return; + } + const isStep = name.toLowerCase().match(/\.(stp|step)$/); + if (isStep) { + toast.error( + 'STEP 尚未生成三角网格预览。请先点击底部「装配检测」跑完一次(需 Python cadquery),或改用 STL/OBJ 做 3D 预览。', + ); + setCadLoading(false); + setCadLoadProgress(0); + setCadModelUrl(undefined); + setCadModelFormat(undefined); + setActiveCadName(undefined); + return; + } + } + setCadModelUrl(url); + setCadModelFormat(format); + setActiveCadName(name); + }, + [resolveCadPreview, selection.modelId], + ); + + const handleLoadPointClouds = useCallback(async (taskId: number, _filePaths: string[]) => { + const modelId = selection.modelId; + // 若未在左侧点过 CAD「预览」,自动加载该车型下第一个 CAD,便于与点云同屏 + if (modelId != null && !cadModelUrlRef.current) { + try { + const files = await utils.cadFile.list.fetch({ modelId }); + const first = files[0]; + if (first) { + setCadLoading(true); + setCadLoadProgress(0); + const resolved = await resolveCadPreview(modelId, { + id: first.id, + name: first.name, + format: first.format, + }); + if (resolved) { + setCadModelUrl(resolved.url); + setCadModelFormat(resolved.format); + setActiveCadName(resolved.label); + } else if (!first.name.toLowerCase().match(/\.(stp|step)$/)) { + setCadModelUrl(`/api/files/read/cad/${modelId}/${encodeURIComponent(first.name)}`); + setCadModelFormat(first.format); + setActiveCadName(first.name); + } else { + setCadLoading(false); + } + } + } catch (e) { + console.warn('[Home] CAD list fetch failed:', e); + } + } + + setPointCloudTaskId(taskId); + setPointCloudUrls([`__task:${taskId}`]); + setPcLoading(true); + setPcProgress(0); + }, [selection.modelId, utils.cadFile.list, resolveCadPreview]); + + const handlePcProgress = useCallback((p: number) => setPcProgress(p), []); + const handlePcLoaded = useCallback(() => { + setPcLoading(false); + setPcProgress(100); + }, []); + + const handlePointSelect = useCallback((pt: any) => { + if (pt) setSelectedPoint({ position: pt.position, error: pt.error, index: pt.index }); + }, []); + + // ── 把 __task:N 格式的 URL 转成 batch 请求参数 ────────────────────────────── + const batchTaskId = pointCloudTaskId; + + // ── 工作流进度条状态推导 ─────────────────────────────────────────────────── + const { data: analysisResults = [] } = trpc.analysisResult.list.useQuery( + { taskId: selection.taskId! }, + { enabled: selection.taskId != null, refetchInterval: 5000 }, + ); + + useEffect(() => { analysisResultsRef.current = analysisResults; }, [analysisResults]); + + const workflowState = useMemo(() => { + const hasCad = !!cadModelUrl; + const hasPc = pointCloudTaskId != null; + const rendered = hasPc && !pcLoading && pcProgress >= 100; + const baseline = (!isCreatingTask && selection.taskId != null) + ? (analysisBaselines.get(selection.taskId) ?? -1) + : -1; + const triggered = baseline >= 0; + const currentCompleted = (analysisResults as any[]).filter((r: any) => r.status === 'completed').length; + const analyzed = triggered && currentCompleted > baseline; + + function step(cond: boolean, prev: boolean): 'done' | 'active' | 'pending' { + if (cond) return 'done'; + if (prev) return 'active'; + return 'pending'; + } + + // 分析步骤:只在 触发后未完成 时转圈;保存步骤:分析完成即完成,无中间态 + const analysisStatus: StepStatus = analyzed ? 'done' : triggered ? 'active' : 'pending'; + const saveStatus: StepStatus = analyzed ? 'done' : 'pending'; + + return { + cad: step(hasCad, true), + pointcloud: step(hasPc, hasCad), + render: step(rendered, hasPc), + analysis: analysisStatus, + save: saveStatus, + }; + }, [cadModelUrl, pointCloudTaskId, pcLoading, pcProgress, analysisResults, isCreatingTask, analysisBaselines, selection.taskId]); + + const rightPanelRef = useRef(null); + + const handleWorkflowStepClick = useCallback((id: WorkflowStepId) => { + if (id === 'cad' || id === 'pointcloud') { + setShowLeftPanel(true); + } else if (id === 'render') { + if (selection.taskId != null && pointCloudTaskId !== selection.taskId) { + handleLoadPointClouds(selection.taskId, []); + } + } else if (id === 'analysis' || id === 'save') { + setShowRightPanel(true); + } + }, [selection.taskId, pointCloudTaskId, handleLoadPointClouds]); + // 必须稳定数组引用:若每次 render 都 new [],PointCloudViewer 会以为点云源变了而反复全量请求 + const pointCloudUrlsStable = useMemo( + () => (pointCloudTaskId != null ? [`__task:${pointCloudTaskId}`] : undefined), + [pointCloudTaskId] + ); + + // ── 左侧面板渲染 ─────────────────────────────────────────────────────────── + const renderLeftPanel = () => { + if (selection.modelId == null) { + return ( + + ); + } + return ( + <> + +
+ +
+ setSelection(s => ({ ...s, taskId }))} + onLoadPointClouds={handleLoadPointClouds} + onStartCreate={() => setIsCreatingTask(true)} + onEndCreate={() => setIsCreatingTask(false)} + /> + + ); + }; + + const renderRightPanel = () => { + if (selection.taskId == null) { + return ( +
+ +

请从左侧选择检测任务

+

选中任务后可查看分析结果

+
+ ); + } + return ( + console.log('[Replay] resultId', resultId)} + onViewSmoothHeatmap={(resultId) => { + setHeatmapViewResultId(resultId); + setPointCloudRenderMode('smoothHeatmap'); + }} + /> + ); + }; + + return ( +
+ {/* 顶部工具栏 */} +
+
+
+
+ +
+

宏量点云处理系统

+
+ + {activeModel && ( + <> +
+
+ + {activeModel.name} + {selection.taskId && ( + + 任务 #{selection.taskId} + + )} +
+ + )} +
+ +
+
+ + +
+ +
+ + + {pcLoading ? `加载点云 ${pcProgress}%` : + cadLoading ? `加载 CAD ${cadLoadProgress}%` : + '待机'} + +
+ + {loading ? ( +
+ ) : isAuthenticated ? ( +
+ {user?.name || '用户'} + +
+ ) : ( + + + + )} +
+
+ + {/* 主内容区 */} +
+ {/* 左侧面板 */} + {showLeftPanel && ( + + )} + + {/* 中央 3D 视图 */} +
+ {/* 工作流进度条(仅在选中任务时显示) */} + {selection.taskId != null && ( + + )} +
+
+
+ {/* 视图标题栏 */} +
+ 3D Viewport +
+
+ FPS: 60 +
+
+ + {/* 3D 渲染区域 */} +
+ + + {pcLoading && ( +
+
+
正在加载点云数据
+
+
+
+
{pcProgress}%
+
+
+ )} +
+ + {/* 操作提示 */} +
+ {errorPickEnabled && pointCloudRenderMode === 'smoothHeatmap' && ( + 平滑模式不支持选点,请切回「点云」 + )} + {errorPickEnabled && pointCloudRenderMode === 'points' + ? <>左键点击选点误差右键平移滚轮缩放 + : <>左键旋转右键平移滚轮缩放 + } +
+ + {/* 选中点信息 */} + {selectedPoint && ( +
+
选中点
+
+
X
{selectedPoint.position.x.toFixed(2)}
+
Y
{selectedPoint.position.y.toFixed(2)}
+
Z
{selectedPoint.position.z.toFixed(2)}
+
+
+ 误差 + 1.5 ? 'text-red-400' : + selectedPoint.error > 0.8 ? 'text-amber-400' : 'text-green-400' + }`}>{selectedPoint.error.toFixed(4)} mm +
+
+ )} + + {/* 坐标轴 */} +
+ + + + + X + Y + Z + +
+
+
+
+ + {/* 底部视图控制 */} +
+
+ + + + + {heatmapViewResultId != null && pointCloudRenderMode === 'smoothHeatmap' && ( + + 正在显示分析结果 #{heatmapViewResultId} + + )} + + + + +
+ +
+ +
+ 点大小 + setPointSize(parseFloat(e.target.value))} + className="w-24 h-1 bg-secondary rounded appearance-none cursor-pointer" + /> + {pointSize.toFixed(3)} +
+ +
+ +
+ 预览点数 + setRenderPointBudget(Number(e.target.value))} + className="w-28 h-1 bg-secondary rounded appearance-none cursor-pointer" + /> + {renderPointBudget >= 1_000_000 ? (renderPointBudget / 1_000_000).toFixed(1) + 'M' : (renderPointBudget / 1000).toFixed(0) + 'k'} +
+
+
+ + {/* 右侧面板 - 分析结果 */} + {showRightPanel && ( + + )} +
+
+ ); +} diff --git a/cp_file.sh b/cp_file.sh new file mode 100755 index 0000000..fe835c5 --- /dev/null +++ b/cp_file.sh @@ -0,0 +1,27 @@ +TASK_ID=1 +SRC_DIR=/home/lipan/massive-cloud/data/pointcloud + +# 在容器内创建目录 +docker exec massive-point-cloud-app-dev mkdir -p /data/tasks/${TASK_ID}/pointcloud + +# 循环复制并写库 +for f in ${SRC_DIR}/points*.txt; do + name=$(basename "$f") + size=$(stat -c%s "$f") + + echo "复制 $name ($((size/1024/1024)) MB)..." + docker cp "$f" massive-point-cloud-app-dev:/data/tasks/${TASK_ID}/pointcloud/${name} + + docker exec massive-point-cloud-mysql \ + mysql -u pointcloud -ppointcloud_password pointcloud_db -e \ + "INSERT IGNORE INTO task_pointcloud_files (taskId, name, fileSize, filePath, createdAt) + VALUES (${TASK_ID}, '${name}', ${size}, '/data/tasks/${TASK_ID}/pointcloud/${name}', NOW());" + + echo " ✓ 完成" + done + + echo "" + echo "=== 验证 ===" + docker exec massive-point-cloud-mysql \ + mysql -u pointcloud -ppointcloud_password pointcloud_db -e \ + "SELECT id, name, ROUND(fileSize/1024/1024,1) AS size_MB FROM task_pointcloud_files WHERE taskId=${TASK_ID};" diff --git a/docker-compose.gpu-v1.yml b/docker-compose.gpu-v1.yml new file mode 100755 index 0000000..47714ee --- /dev/null +++ b/docker-compose.gpu-v1.yml @@ -0,0 +1,15 @@ +# GPU 叠加配置(旧版 docker-compose V1,使用 runtime: nvidia) +# 用法:docker-compose -f docker-compose.yml -f docker-compose.gpu-v1.yml up -d +# +# 前提:已注册 nvidia runtime +# sudo nvidia-ctk runtime configure --runtime=docker +# sudo systemctl restart docker +# 验证:docker info | grep -i nvidia + +version: '3.8' + +services: + app: + runtime: nvidia + environment: + - ASSEMBLY_USE_GPU=1 diff --git a/docker-compose.gpu-v2.yml b/docker-compose.gpu-v2.yml new file mode 100755 index 0000000..fc67ecb --- /dev/null +++ b/docker-compose.gpu-v2.yml @@ -0,0 +1,25 @@ +# GPU 叠加配置(Compose V2:docker compose) +# 用法:docker compose -f docker-compose.yml -f docker-compose.gpu-v2.yml up -d +# +# 前提:主机已安装 nvidia-container-toolkit +# distribution=$(. /etc/os-release; echo $ID$VERSION_ID) +# curl -s -L https://nvidia.github.io/libnvidia-container/gpgkey | sudo apt-key add - +# curl -s -L https://nvidia.github.io/libnvidia-container/$distribution/libnvidia-container.list \ +# | sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list +# sudo apt-get update && sudo apt-get install -y nvidia-container-toolkit +# sudo nvidia-ctk runtime configure --runtime=docker +# sudo systemctl restart docker + +version: '3.8' + +services: + app: + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: all + capabilities: [gpu] + environment: + - ASSEMBLY_USE_GPU=1 diff --git a/docker-compose.gpu.compose-v2.yml b/docker-compose.gpu.compose-v2.yml new file mode 100755 index 0000000..0cccb90 --- /dev/null +++ b/docker-compose.gpu.compose-v2.yml @@ -0,0 +1,16 @@ +# GPU overlay (Docker Compose V2 插件, docker compose 命令) +# 不需要额外注册 nvidia runtime +# +# 启动: docker compose -f docker-compose.yml -f docker-compose.gpu.compose-v2.yml up -d + +version: '3.8' + +services: + app: + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: all + capabilities: [gpu] diff --git a/docker-compose.gpu.yml b/docker-compose.gpu.yml new file mode 100755 index 0000000..6efb88f --- /dev/null +++ b/docker-compose.gpu.yml @@ -0,0 +1,12 @@ +# GPU overlay (旧版 docker-compose / Docker Engine < 19.03) +# 需先在宿主机注册 nvidia 运行时: +# sudo nvidia-ctk runtime configure --runtime=docker && sudo systemctl restart docker +# 验证: docker info | grep -i nvidia +# +# 启动: docker-compose -f docker-compose.yml -f docker-compose.gpu.yml up -d + +version: '3.8' + +services: + app: + runtime: nvidia diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100755 index 0000000..3573326 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,134 @@ +version: '3.8' + +# 宏量点云处理系统 — 目标服务器部署配置 +# 使用方法: +# 1. 先执行 load-images.sh 载入镜像 +# 2. cp .env.example .env 然后按需修改 +# 3. docker-compose up -d +# +# GPU (可选):叠加 GPU overlay: +# 旧版 docker-compose: docker-compose -f docker-compose.yml -f docker-compose.gpu.yml up -d +# Compose V2: docker compose -f docker-compose.yml -f docker-compose.gpu.compose-v2.yml up -d +# +# 端口说明 (host 网络模式,直接占用宿主机端口): +# 应用: 3002 +# MySQL: 3307 +# MinIO API: 9010 +# MinIO 控制台: 9011 + +services: + app: + image: massive-point-cloud-app-dev:frozen-20260409 + container_name: massive-point-cloud-app-dev + working_dir: /app + restart: unless-stopped + network_mode: host + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: all + capabilities: [gpu] + environment: + - NODE_ENV=development + - PORT=3002 + - DATABASE_URL=mysql://pointcloud:${MYSQL_PASSWORD:-pointcloud_password}@localhost:3307/pointcloud_db + - JWT_SECRET=${JWT_SECRET:-please-change-this-secret-in-production} + - AUTH_MODE=local + - S3_ENDPOINT=http://localhost:9010 + - S3_ACCESS_KEY=${MINIO_ROOT_USER:-minioadmin} + - S3_SECRET_KEY=${MINIO_ROOT_PASSWORD:-minioadmin123} + - S3_BUCKET=pointcloud-data + - S3_REGION=us-east-1 + - DATA_DIR=/data + - PYTHON3_PATH=/usr/bin/python3 + - ASSEMBLY_USE_GPU=1 + - NVIDIA_VISIBLE_DEVICES=all + - NVIDIA_DRIVER_CAPABILITIES=compute,utility + - VITE_APP_TITLE=${VITE_APP_TITLE:-宏量点云处理系统} + - VITE_OAUTH_PORTAL_URL=disabled + - VITE_APP_ID= + - CHOKIDAR_USEPOLLING=true + volumes: + - ./src:/app + - app-node-modules:/app/node_modules + - ${DATA_DIR:-./data}:/data + command: sh -c "corepack enable && corepack prepare pnpm@latest --activate && pnpm install && pnpm dev --host 0.0.0.0" + depends_on: + mysql: + condition: service_healthy + minio: + condition: service_started + + mysql: + image: mysql:8.0 + container_name: massive-point-cloud-mysql + restart: unless-stopped + network_mode: host + environment: + - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD:-root_password_change_me} + - MYSQL_DATABASE=pointcloud_db + - MYSQL_USER=pointcloud + - MYSQL_PASSWORD=${MYSQL_PASSWORD:-pointcloud_password} + - MYSQL_TCP_PORT=3307 + volumes: + - mysql-data:/var/lib/mysql + - ./docker/mysql/init.sql:/docker-entrypoint-initdb.d/init.sql:ro + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-P", "3307", "-u", "root", "-p${MYSQL_ROOT_PASSWORD:-root_password_change_me}"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + command: > + --default-authentication-plugin=mysql_native_password + --character-set-server=utf8mb4 + --collation-server=utf8mb4_unicode_ci + --port=3307 + --sort_buffer_size=33554432 + --read_rnd_buffer_size=8388608 + --tmp_table_size=67108864 + --max_heap_table_size=67108864 + + minio: + image: minio/minio:latest + container_name: massive-point-cloud-minio + restart: unless-stopped + network_mode: host + environment: + - MINIO_ROOT_USER=${MINIO_ROOT_USER:-minioadmin} + - MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD:-minioadmin123} + volumes: + - minio-data:/data + command: server /data --address ":9010" --console-address ":9011" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:9010/minio/health/live"] + interval: 30s + timeout: 20s + retries: 3 + + minio-init: + image: minio/mc:latest + container_name: massive-point-cloud-minio-init + network_mode: host + restart: on-failure + depends_on: + minio: + condition: service_healthy + entrypoint: > + /bin/sh -c " + mc alias set myminio http://localhost:9010 ${MINIO_ROOT_USER:-minioadmin} ${MINIO_ROOT_PASSWORD:-minioadmin123}; + mc mb myminio/pointcloud-data --ignore-existing; + mc anonymous set public myminio/pointcloud-data; + echo 'MinIO bucket initialized'; + exit 0; + " + +volumes: + mysql-data: + name: pointcloud-mysql-data + minio-data: + name: pointcloud-minio-data + app-node-modules: + name: pointcloud-app-node-modules-dev diff --git a/docker/Dockerfile.dev b/docker/Dockerfile.dev new file mode 100755 index 0000000..7b64085 --- /dev/null +++ b/docker/Dockerfile.dev @@ -0,0 +1,43 @@ +# 开发镜像(仅 CPU,体积小):Node 22 + Python 3 + cadquery +# 需要 Docker 内装配检测 GPU 时改用 docker/Dockerfile.dev.gpu(见 docker-compose.dev.yml) +# 使用 glibc(bookworm),便于 cadquery / OCP 的 manylinux 轮子 +FROM node:22-bookworm-slim + +# cadquery/OCP 会链接 OpenGL 与 X11 客户端库(无显示器也会加载),slim 需一次性装齐 +RUN apt-get update && apt-get install -y --no-install-recommends \ + python3 \ + python3-pip \ + ca-certificates \ + libgl1 \ + libglib2.0-0 \ + libxrender1 \ + libxext6 \ + libsm6 \ + libice6 \ + libx11-6 \ + && rm -rf /var/lib/apt/lists/* + +# 先安装几何依赖(镜像构建时固化,避免每次 compose up 再 pip) +# cadquery-ocp 轮子约 70MB,默认 pip 超时过短会 Read timed out;可调大 PIP_DEFAULT_TIMEOUT +# 默认国内 PyPI 源;覆盖示例:--build-arg PIP_INDEX_URL=https://mirrors.aliyun.com/pypi/simple/ +ARG PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple +ARG PIP_DEFAULT_TIMEOUT=300 +COPY requirements-geometry.txt /tmp/requirements-geometry.txt +# 必须用 python3 -m pip,避免 pip3 与 /usr/bin/python3 不是同一套 site-packages(PEP 668 下常见踩坑) +RUN python3 -m pip install --no-cache-dir --break-system-packages \ + --default-timeout="${PIP_DEFAULT_TIMEOUT}" \ + --retries 10 \ + -i "${PIP_INDEX_URL}" \ + -r /tmp/requirements-geometry.txt \ + && rm /tmp/requirements-geometry.txt \ + && python3 -c "import cadquery; print('cadquery OK:', cadquery.__file__)" + +# Node 子进程解析 STEP 时使用;与 compose 中 PYTHON3_PATH 一致,避免仅 build 镜像未带 compose 时未设置 +ENV PYTHON3_PATH=/usr/bin/python3 + +WORKDIR /app + +RUN corepack enable && corepack prepare pnpm@latest --activate + +# 默认占位;docker-compose.dev.yml 会覆盖为 pnpm dev +CMD ["node", "-e", "console.log('use docker-compose command')"] diff --git a/docker/Dockerfile.dev.gpu b/docker/Dockerfile.dev.gpu new file mode 100755 index 0000000..e1a57fb --- /dev/null +++ b/docker/Dockerfile.dev.gpu @@ -0,0 +1,89 @@ +# 开发镜像(GPU):Ubuntu 22.04 + CUDA 12.1 runtime + Node 22 + cadquery + PyTorch(cu121) + PyTorch3D +# 用于装配检测「最近零件」服务端 GPU(scripts/assembly_nearest_gpu.py) +# 宿主机需:NVIDIA 驱动 + nvidia-container-toolkit;构建不要求 GPU,运行需 --gpus +# +# 构建:docker build -t massive-point-cloud-app-dev:local -f docker/Dockerfile.dev.gpu . +# +# 无 NVIDIA 的机器请改用 docker/Dockerfile.dev(仅 CPU),compose 中去掉 gpus 与 ASSEMBLY_USE_GPU + +FROM nvidia/cuda:12.1.1-cudnn8-runtime-ubuntu22.04 + +ENV DEBIAN_FRONTEND=noninteractive \ + PYTHON3_PATH=/usr/bin/python3 \ + ASSEMBLY_USE_GPU=1 \ + NVIDIA_VISIBLE_DEVICES=all \ + NVIDIA_DRIVER_CAPABILITIES=compute,utility + +# apt 换阿里云(国内构建加速);需官方源时可在下方注释掉本段 +RUN set -eux; \ + if [ -f /etc/apt/sources.list ]; then \ + sed -i 's|http://archive.ubuntu.com/ubuntu|http://mirrors.aliyun.com/ubuntu|g' /etc/apt/sources.list; \ + sed -i 's|http://security.ubuntu.com/ubuntu|http://mirrors.aliyun.com/ubuntu|g' /etc/apt/sources.list; \ + fi + +RUN apt-get update && apt-get install -y --no-install-recommends \ + python3 \ + python3-pip \ + python3-dev \ + ca-certificates \ + curl \ + gnupg \ + libgl1 \ + libglib2.0-0 \ + libxrender1 \ + libxext6 \ + libsm6 \ + libice6 \ + libx11-6 \ + && rm -rf /var/lib/apt/lists/* + +# CUDA runtime 镜像默认不带 nvidia-smi;装 Ubuntu 的 nvidia-utils(主版本与宿主机驱动大版本一致,如 535/550) +# 构建失败时改:--build-arg NVIDIA_UTILS_MAJOR=550 +ARG NVIDIA_UTILS_MAJOR=535 +RUN apt-get update && apt-get install -y --no-install-recommends \ + "nvidia-utils-${NVIDIA_UTILS_MAJOR}" \ + && rm -rf /var/lib/apt/lists/* + +RUN mkdir -p /etc/apt/keyrings \ + && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \ + && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" > /etc/apt/sources.list.d/nodesource.list \ + && apt-get update && apt-get install -y --no-install-recommends nodejs \ + && rm -rf /var/lib/apt/lists/* + +RUN python3 -m pip install --no-cache-dir --upgrade pip setuptools wheel + +# cadquery(与 Dockerfile.dev 一致) +ARG PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple +ARG PIP_DEFAULT_TIMEOUT=300 +COPY requirements-geometry.txt /tmp/requirements-geometry.txt +RUN python3 -m pip install --no-cache-dir --break-system-packages \ + --default-timeout="${PIP_DEFAULT_TIMEOUT}" \ + --retries 10 \ + -i "${PIP_INDEX_URL}" \ + -r /tmp/requirements-geometry.txt \ + && rm /tmp/requirements-geometry.txt \ + && python3 -c "import cadquery; print('cadquery OK')" + +# PyTorch CUDA 12.1:默认清华镜像;覆盖示例:--build-arg TORCH_INDEX_URL=https://download.pytorch.org/whl/cu121 +ARG TORCH_INDEX_URL=https://mirrors.tuna.tsinghua.edu.cn/pytorch/whl/cu121 +RUN python3 -m pip install --no-cache-dir --break-system-packages \ + --default-timeout=300 --retries 10 \ + torch==2.3.1 torchvision==0.18.1 \ + --index-url "${TORCH_INDEX_URL}" + +# PyTorch3D 预编译轮仅 Meta 官方 CDN,无稳定国内镜像;加大超时。可本地下载 wheel 后 COPY + pip install。 +RUN python3 -m pip install --no-cache-dir --break-system-packages \ + --default-timeout=300 --retries 10 \ + -i https://pypi.tuna.tsinghua.edu.cn/simple \ + trimesh \ + && python3 -m pip install --no-cache-dir --break-system-packages \ + --default-timeout=600 --retries 15 \ + pytorch3d -f https://dl.fbaipublicfiles.com/pytorch3d/packaging/wheels/py310_cu121_pyt231/download.html + +RUN python3 -c "import torch; import pytorch3d; import trimesh; print('pytorch', torch.__version__, 'pytorch3d', pytorch3d.__version__)" + +WORKDIR /app + +RUN corepack enable && corepack prepare pnpm@latest --activate + +CMD ["node", "-e", "console.log('use docker-compose command')"] diff --git a/docker/env-config.md b/docker/env-config.md new file mode 100755 index 0000000..6b85ea4 --- /dev/null +++ b/docker/env-config.md @@ -0,0 +1,41 @@ +# 环境变量配置说明 + +部署前,请在项目根目录创建 `.env` 文件,包含以下配置: + +## 安全配置 (必须修改!) + +```bash +# JWT密钥 - 用于会话签名,请使用随机字符串 +# 生成方法: openssl rand -base64 32 +JWT_SECRET=your-super-secret-jwt-key-change-in-production + +# MySQL root密码 +MYSQL_ROOT_PASSWORD=root_password_change_me +``` + +## MinIO对象存储配置 + +```bash +# MinIO管理员用户名 +MINIO_ROOT_USER=minioadmin + +# MinIO管理员密码 (至少8个字符) +MINIO_ROOT_PASSWORD=minioadmin123 +``` + +## 应用配置 + +```bash +# 应用标题 +VITE_APP_TITLE=宏量点云处理系统 +``` + +## 可选配置 + +```bash +# 启用Redis缓存 (需要在docker-compose中启用with-cache profile) +# REDIS_URL=redis://redis:6379 + +# 日志级别: debug, info, warn, error +# LOG_LEVEL=info +``` diff --git a/docker/mysql/init.sql b/docker/mysql/init.sql new file mode 100755 index 0000000..0c09d8f --- /dev/null +++ b/docker/mysql/init.sql @@ -0,0 +1,166 @@ +-- 宏量点云处理软件 - 数据库初始化脚本 +-- 该脚本在MySQL容器首次启动时自动执行 +-- 后续增量迁移由 server/_core/index.ts runMigrations() 从 drizzle/*.sql 自动应用 + +-- 设置字符集 +SET NAMES utf8mb4; +SET CHARACTER SET utf8mb4; + +-- 确保数据库存在 +CREATE DATABASE IF NOT EXISTS pointcloud_db + CHARACTER SET utf8mb4 + COLLATE utf8mb4_unicode_ci; + +USE pointcloud_db; + +-- 用户表 +CREATE TABLE IF NOT EXISTS users ( + id INT AUTO_INCREMENT PRIMARY KEY, + openId VARCHAR(64) NOT NULL UNIQUE, + name TEXT, + email VARCHAR(320), + loginMethod VARCHAR(64), + role ENUM('user', 'admin') NOT NULL DEFAULT 'user', + createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updatedAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + lastSignedIn TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + INDEX idx_openId (openId), + INDEX idx_email (email(255)) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- 点云项目表(旧版,兼容保留) +CREATE TABLE IF NOT EXISTS point_cloud_projects ( + id INT AUTO_INCREMENT PRIMARY KEY, + userId INT NOT NULL, + name VARCHAR(255) NOT NULL, + description TEXT, + cadModelUrl TEXT, + cadModelName VARCHAR(255), + totalChunks INT NOT NULL DEFAULT 0, + processedChunks INT NOT NULL DEFAULT 0, + totalPoints BIGINT NOT NULL DEFAULT 0, + status ENUM('pending', 'uploading', 'processing', 'completed', 'error') NOT NULL DEFAULT 'pending', + createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updatedAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_userId (userId), + INDEX idx_status (status), + FOREIGN KEY (userId) REFERENCES users(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- 点云数据块表(旧版,兼容保留) +CREATE TABLE IF NOT EXISTS point_cloud_chunks ( + id INT AUTO_INCREMENT PRIMARY KEY, + projectId INT NOT NULL, + chunkIndex INT NOT NULL, + fileName VARCHAR(255) NOT NULL, + fileUrl TEXT NOT NULL, + fileSize BIGINT NOT NULL DEFAULT 0, + pointCount INT NOT NULL DEFAULT 0, + status ENUM('pending', 'uploading', 'processing', 'completed', 'error') NOT NULL DEFAULT 'pending', + lodLevel INT NOT NULL DEFAULT 0, + boundingBox JSON, + createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updatedAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_projectId (projectId), + INDEX idx_status (status), + UNIQUE KEY uk_project_chunk (projectId, chunkIndex), + FOREIGN KEY (projectId) REFERENCES point_cloud_projects(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- CAD模型表(旧版,兼容保留) +CREATE TABLE IF NOT EXISTS cad_models ( + id INT AUTO_INCREMENT PRIMARY KEY, + userId INT NOT NULL, + name VARCHAR(255) NOT NULL, + format VARCHAR(32) NOT NULL, + fileUrl TEXT NOT NULL, + fileSize BIGINT NOT NULL DEFAULT 0, + vertices INT NOT NULL DEFAULT 0, + faces INT NOT NULL DEFAULT 0, + boundingBox JSON, + createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + INDEX idx_userId (userId), + FOREIGN KEY (userId) REFERENCES users(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- 分析结果表(旧版,兼容保留) +CREATE TABLE IF NOT EXISTS analysis_results ( + id INT AUTO_INCREMENT PRIMARY KEY, + projectId INT NOT NULL, + analysisType ENUM('error', 'measurement', 'compression') NOT NULL, + resultData JSON NOT NULL, + createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + INDEX idx_projectId (projectId), + INDEX idx_type (analysisType), + FOREIGN KEY (projectId) REFERENCES point_cloud_projects(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- ─── 车系/车型/检测任务/分析结果 层级结构 ────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS `vehicle_series` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `name` VARCHAR(100) NOT NULL, + `description` TEXT, + `createdAt` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updatedAt` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `vehicle_models` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `seriesId` INT NOT NULL, + `name` VARCHAR(100) NOT NULL, + `description` TEXT, + `createdAt` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updatedAt` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `model_cad_files` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `modelId` INT NOT NULL, + `name` VARCHAR(255) NOT NULL, + `format` VARCHAR(32) NOT NULL, + `fileSize` BIGINT NOT NULL, + `filePath` VARCHAR(512) NOT NULL, + `createdAt` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `inspection_tasks` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `modelId` INT NOT NULL, + `name` VARCHAR(255) NOT NULL, + `inspector` VARCHAR(100), + `notes` TEXT, + `status` ENUM('pending','processing','completed','error') NOT NULL DEFAULT 'pending', + `createdAt` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updatedAt` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `completedAt` TIMESTAMP NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `task_pointcloud_files` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `taskId` INT NOT NULL, + `name` VARCHAR(255) NOT NULL, + `fileSize` BIGINT NOT NULL, + `filePath` VARCHAR(512) NOT NULL, + `createdAt` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `task_analysis_results` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `taskId` INT NOT NULL, + `type` ENUM('error_analysis','measurement','compression','smooth_heatmap','assembly_inspection','obj_deviation') NOT NULL, + `status` ENUM('pending','processing','completed','error') NOT NULL DEFAULT 'pending', + `errorStats` JSON, + `measurements` JSON, + `resultData` JSON, + `createdAt` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updatedAt` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- 创建默认管理员用户 (内网部署使用) +INSERT INTO users (openId, name, email, role) VALUES + ('admin-local', '系统管理员', 'admin@localhost', 'admin') +ON DUPLICATE KEY UPDATE name = VALUES(name); + +-- 输出初始化完成信息 +SELECT 'Database initialization completed successfully!' AS message; diff --git a/docker/nginx/nginx.conf b/docker/nginx/nginx.conf new file mode 100755 index 0000000..07273c8 --- /dev/null +++ b/docker/nginx/nginx.conf @@ -0,0 +1,152 @@ +# 宏量点云处理软件 - Nginx反向代理配置 +# 适用于内网生产环境部署 + +user nginx; +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; + use epoll; + multi_accept on; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # 日志格式 + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for" ' + 'rt=$request_time uct="$upstream_connect_time" ' + 'uht="$upstream_header_time" urt="$upstream_response_time"'; + + access_log /var/log/nginx/access.log main; + + # 性能优化 + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + + # Gzip压缩 + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_types text/plain text/css text/xml application/json application/javascript + application/rss+xml application/atom+xml image/svg+xml; + + # 文件上传大小限制 (支持大型点云文件) + client_max_body_size 2G; + client_body_buffer_size 128M; + client_body_timeout 600s; + + # 代理缓冲设置 + proxy_buffer_size 128k; + proxy_buffers 4 256k; + proxy_busy_buffers_size 256k; + proxy_temp_file_write_size 256k; + + # 上游服务器 + upstream app_server { + server app:3000; + keepalive 32; + } + + upstream minio_server { + server minio:9000; + keepalive 32; + } + + # HTTP服务器 (重定向到HTTPS,生产环境启用) + server { + listen 80; + server_name _; + + # 如果启用HTTPS,取消下面的注释 + # return 301 https://$host$request_uri; + + # 如果不使用HTTPS,使用下面的配置 + location / { + proxy_pass http://app_server; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + proxy_read_timeout 600s; + proxy_connect_timeout 600s; + proxy_send_timeout 600s; + } + + # API路由 + location /api/ { + proxy_pass http://app_server; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 600s; + proxy_connect_timeout 600s; + proxy_send_timeout 600s; + } + + # MinIO存储代理 (可选) + location /storage/ { + rewrite ^/storage/(.*) /$1 break; + proxy_pass http://minio_server; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # 健康检查端点 + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } + } + + # HTTPS服务器 (生产环境启用,需要配置SSL证书) + # server { + # listen 443 ssl http2; + # server_name your-domain.com; + # + # ssl_certificate /etc/nginx/ssl/cert.pem; + # ssl_certificate_key /etc/nginx/ssl/key.pem; + # ssl_session_timeout 1d; + # ssl_session_cache shared:SSL:50m; + # ssl_session_tickets off; + # + # ssl_protocols TLSv1.2 TLSv1.3; + # ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256; + # ssl_prefer_server_ciphers off; + # + # # HSTS + # add_header Strict-Transport-Security "max-age=63072000" always; + # + # location / { + # proxy_pass http://app_server; + # proxy_http_version 1.1; + # proxy_set_header Upgrade $http_upgrade; + # proxy_set_header Connection 'upgrade'; + # proxy_set_header Host $host; + # proxy_set_header X-Real-IP $remote_addr; + # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + # proxy_set_header X-Forwarded-Proto $scheme; + # proxy_cache_bypass $http_upgrade; + # proxy_read_timeout 600s; + # } + # } +} diff --git a/load-images.sh b/load-images.sh new file mode 100755 index 0000000..d33c295 --- /dev/null +++ b/load-images.sh @@ -0,0 +1,47 @@ +#!/bin/bash +# 宏量点云处理系统 - 镜像载入脚本 +# 用法: bash load-images.sh + +set -e +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +IMAGES_DIR="$SCRIPT_DIR/images" + +echo "================================================" +echo " 宏量点云处理系统 - 离线镜像载入" +echo "================================================" +echo "" + +load_image() { + local name="$1" + local file="$IMAGES_DIR/$2" + if [ ! -f "$file" ]; then + echo "[跳过] $name → 文件不存在: $file" + return + fi + local size=$(du -BM "$file" | cut -f1) + echo "[载入] $name (${size}) ..." + docker load -i "$file" + echo "[完成] $name" + echo "" +} + +load_image "App (massive-point-cloud-app-dev:frozen-20260409)" "app-dev.tar" +load_image "MySQL 8.0" "mysql.tar" +load_image "MinIO" "minio.tar" +load_image "MinIO MC" "minio-mc.tar" + +echo "================================================" +echo " 所有镜像载入完毕,确认:" +echo "================================================" +docker images | grep -E 'massive-point-cloud-app-dev|mysql|minio' || true +echo "" +echo "下一步: cp .env.example .env 并修改密钥,然后运行:" +echo "" +echo " # 无 GPU:" +echo " docker-compose up -d" +echo "" +echo " # 有 NVIDIA GPU (旧版 docker-compose):" +echo " docker-compose -f docker-compose.yml -f docker-compose.gpu.yml up -d" +echo "" +echo " # 有 NVIDIA GPU (Compose V2):" +echo " docker compose -f docker-compose.yml -f docker-compose.gpu.compose-v2.yml up -d" diff --git a/src/.dockerignore b/src/.dockerignore new file mode 100644 index 0000000..3c527df --- /dev/null +++ b/src/.dockerignore @@ -0,0 +1,56 @@ +# 宏量点云处理软件 - Docker构建忽略文件 + +# 依赖目录 +node_modules +.pnpm-store + +# 构建输出 +dist +build +.next +.nuxt + +# 开发文件 +.git +.gitignore +.github +.vscode +.idea + +# 环境文件 +.env +.env.* +!.env.example + +# 日志 +logs +*.log +npm-debug.log* +pnpm-debug.log* + +# 测试 +coverage +.nyc_output +*.test.ts +*.spec.ts +__tests__ + +# 文档 +*.md +!README.md +docs + +# Docker相关 +Dockerfile* +docker-compose* +.docker + +# 本地大数据目录(开发 compose 挂载到 /data,勿打入构建上下文) +data + +# 其他 +.DS_Store +Thumbs.db +*.swp +*.swo +.manus-logs diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 0000000..c1dbd8b --- /dev/null +++ b/src/.gitignore @@ -0,0 +1,107 @@ +# Dependencies +**/node_modules +.pnpm-store/ + +# Build outputs +dist/ +build/ +*.dist + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# IDE and editor files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock +*.bak + +# Coverage directory used by tools like istanbul +coverage/ +*.lcov + +# nyc test coverage +.nyc_output + +# Dependency directories +jspm_packages/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt + +# Gatsby files +.cache/ + +# Storybook build outputs +.out +.storybook-out + +# Temporary folders +tmp/ +temp/ + +# Database +*.db +*.sqlite +*.sqlite3 diff --git a/src/.gitkeep b/src/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/.prettierignore b/src/.prettierignore new file mode 100644 index 0000000..7284259 --- /dev/null +++ b/src/.prettierignore @@ -0,0 +1,35 @@ +# Dependencies +node_modules/ +.pnpm-store/ + +# Build outputs +dist/ +build/ +*.dist + +# Generated files +*.tsbuildinfo +coverage/ + +# Package files +package-lock.json +pnpm-lock.yaml + +# Database +*.db +*.sqlite +*.sqlite3 + +# Logs +*.log + +# Environment files +.env* + +# IDE files +.vscode/ +.idea/ + +# OS files +.DS_Store +Thumbs.db diff --git a/src/.prettierrc b/src/.prettierrc new file mode 100644 index 0000000..67c0bc8 --- /dev/null +++ b/src/.prettierrc @@ -0,0 +1,15 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": false, + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "bracketSpacing": true, + "bracketSameLine": false, + "arrowParens": "avoid", + "endOfLine": "lf", + "quoteProps": "as-needed", + "jsxSingleQuote": false, + "proseWrap": "preserve" +} diff --git a/src/DEPLOYMENT.md b/src/DEPLOYMENT.md new file mode 100644 index 0000000..84f75d6 --- /dev/null +++ b/src/DEPLOYMENT.md @@ -0,0 +1,264 @@ +# 宏量点云处理软件 - 内网Docker部署指南 + +本文档详细说明如何在内网服务器上使用Docker部署宏量点云处理软件。 + +## 系统要求 + +### 硬件要求 + +| 配置项 | 最低要求 | 推荐配置 | +|--------|----------|----------| +| CPU | 4核 | 8核+ | +| 内存 | 8GB | 16GB+ | +| 存储 | 100GB SSD | 500GB+ SSD | +| 网络 | 100Mbps | 1Gbps | + +### 软件要求 + +- 操作系统: Linux (Ubuntu 20.04+, CentOS 7+, Debian 10+) +- Docker: 20.10+ +- Docker Compose: 2.0+ + +## 快速开始 + +### 1. 安装Docker和Docker Compose + +```bash +# Ubuntu/Debian +curl -fsSL https://get.docker.com | sh +sudo usermod -aG docker $USER + +# 安装Docker Compose +sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose +sudo chmod +x /usr/local/bin/docker-compose + +# 重新登录以应用用户组更改 +``` + +### 2. 获取项目代码 + +```bash +# 从GitHub克隆 (如果已导出到GitHub) +git clone https://github.com/your-org/massive-point-cloud.git +cd massive-point-cloud + +# 或者直接上传项目文件到服务器 +``` + +### 3. 配置环境变量 + +```bash +# 创建环境变量文件 +cat > .env << 'EOF' +# 安全配置 - 请修改为随机值 +JWT_SECRET=$(openssl rand -base64 32) +MYSQL_ROOT_PASSWORD=$(openssl rand -base64 16) + +# MinIO配置 +MINIO_ROOT_USER=minioadmin +MINIO_ROOT_PASSWORD=$(openssl rand -base64 16) + +# 应用配置 +VITE_APP_TITLE=宏量点云处理系统 +EOF + +# 或者手动编辑 +nano .env +``` + +### 4. 启动服务 + +```bash +# 基础部署 (应用 + MySQL + MinIO) +docker-compose up -d + +# 查看服务状态 +docker-compose ps + +# 查看日志 +docker-compose logs -f +``` + +### 5. 访问应用 + +- **Web应用**: http://your-server-ip:3000 +- **MinIO控制台**: http://your-server-ip:9001 + +## 部署架构 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 内网服务器 │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ Docker Network │ │ +│ │ │ │ +│ │ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │ │ +│ │ │ App │ │ MySQL │ │ MinIO │ │ │ +│ │ │ :3000 │ │ :3306 │ │ :9000 / :9001 │ │ │ +│ │ └────┬─────┘ └────┬─────┘ └────────┬─────────┘ │ │ +│ │ │ │ │ │ │ +│ │ └─────────────┴──────────────────┘ │ │ +│ │ │ │ │ +│ └──────────────────────┼──────────────────────────────┘ │ +│ │ │ +│ ┌──────────────────────┴──────────────────────────────┐ │ +│ │ Nginx (可选, 生产环境) │ │ +│ │ :80 / :443 │ │ +│ └──────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +## 服务说明 + +| 服务 | 端口 | 说明 | +|------|------|------| +| app | 3000 | 主应用服务 | +| mysql | 3306 | MySQL数据库 | +| minio | 9000 | MinIO API | +| minio | 9001 | MinIO控制台 | +| nginx | 80/443 | 反向代理 (可选) | +| redis | 6379 | 缓存服务 (可选) | + +## 高级配置 + +### 启用Nginx反向代理 + +```bash +# 使用production profile启动 +docker-compose --profile production up -d +``` + +### 启用Redis缓存 + +```bash +# 使用with-cache profile启动 +docker-compose --profile with-cache up -d +``` + +### 配置HTTPS + +1. 将SSL证书放置到 `docker/nginx/ssl/` 目录: + - `cert.pem` - 证书文件 + - `key.pem` - 私钥文件 + +2. 编辑 `docker/nginx/nginx.conf`,取消HTTPS服务器配置的注释 + +3. 重启Nginx: +```bash +docker-compose restart nginx +``` + +### 数据持久化 + +所有数据存储在Docker卷中: + +| 卷名 | 用途 | +|------|------| +| pointcloud-mysql-data | MySQL数据 | +| pointcloud-minio-data | 点云文件存储 | +| pointcloud-redis-data | Redis缓存 | +| pointcloud-app-logs | 应用日志 | + +备份数据: +```bash +# 备份MySQL +docker exec massive-point-cloud-mysql mysqldump -u root -p pointcloud_db > backup.sql + +# 备份MinIO数据 +docker run --rm -v pointcloud-minio-data:/data -v $(pwd):/backup alpine tar cvf /backup/minio-backup.tar /data +``` + +## 运维命令 + +```bash +# 查看所有服务状态 +docker-compose ps + +# 查看实时日志 +docker-compose logs -f app + +# 重启单个服务 +docker-compose restart app + +# 停止所有服务 +docker-compose down + +# 停止并删除数据卷 (危险!) +docker-compose down -v + +# 更新应用 +git pull +docker-compose build app +docker-compose up -d app + +# 进入容器调试 +docker exec -it massive-point-cloud-app sh +docker exec -it massive-point-cloud-mysql mysql -u root -p +``` + +## 故障排查 + +### 应用无法启动 + +```bash +# 检查日志 +docker-compose logs app + +# 检查数据库连接 +docker exec massive-point-cloud-app curl -f http://mysql:3306 || echo "MySQL not ready" +``` + +### 数据库连接失败 + +```bash +# 检查MySQL状态 +docker-compose logs mysql + +# 手动测试连接 +docker exec -it massive-point-cloud-mysql mysql -u pointcloud -p -e "SELECT 1" +``` + +### MinIO无法访问 + +```bash +# 检查MinIO状态 +docker-compose logs minio + +# 检查存储桶 +docker exec massive-point-cloud-minio-init mc ls myminio/ +``` + +## 性能优化 + +### MySQL优化 + +编辑 `docker/mysql/my.cnf`: +```ini +[mysqld] +innodb_buffer_pool_size = 2G +innodb_log_file_size = 512M +max_connections = 200 +``` + +### 增加文件上传限制 + +修改 `docker/nginx/nginx.conf`: +```nginx +client_max_body_size 5G; +``` + +## 安全建议 + +1. **修改默认密码**: 务必修改 `.env` 中的所有默认密码 +2. **限制端口暴露**: 生产环境只暴露80/443端口 +3. **启用防火墙**: 配置iptables或firewalld +4. **定期备份**: 设置自动备份任务 +5. **更新镜像**: 定期更新Docker镜像修复安全漏洞 + +## 联系支持 + +如遇到部署问题,请检查: +1. Docker和Docker Compose版本是否满足要求 +2. 服务器资源是否充足 +3. 端口是否被占用 +4. 防火墙规则是否正确 diff --git a/src/Dockerfile b/src/Dockerfile new file mode 100644 index 0000000..cba2c09 --- /dev/null +++ b/src/Dockerfile @@ -0,0 +1,114 @@ +# 宏量点云处理软件 - 生产环境Dockerfile +# 多阶段构建,优化镜像大小 + +# ============================================ +# 阶段1: 依赖安装 +# ============================================ +FROM node:22-alpine AS deps + +WORKDIR /app + +# 安装pnpm +RUN corepack enable && corepack prepare pnpm@latest --activate + +# 复制依赖文件 +COPY package.json pnpm-lock.yaml* ./ +COPY patches ./patches + +# 安装依赖 +RUN pnpm install --frozen-lockfile --prod=false + +# ============================================ +# 阶段2: 构建应用 +# ============================================ +FROM node:22-alpine AS builder + +WORKDIR /app + +# 安装pnpm +RUN corepack enable && corepack prepare pnpm@latest --activate + +# 复制依赖 +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# 设置构建时环境变量 +ARG VITE_APP_TITLE="宏量点云处理系统" +ARG VITE_OAUTH_PORTAL_URL="disabled" +ARG VITE_APP_ID="" +ENV VITE_APP_TITLE=$VITE_APP_TITLE +ENV VITE_OAUTH_PORTAL_URL=$VITE_OAUTH_PORTAL_URL +ENV VITE_APP_ID=$VITE_APP_ID + +# 构建应用 +RUN pnpm build + +# ============================================ +# 阶段3: 生产运行环境(bookworm:与 dev 一致,便于 cadquery manylinux 轮子) +# ============================================ +FROM node:22-bookworm-slim AS runner + +WORKDIR /app + +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl \ + dumb-init \ + python3 \ + python3-pip \ + ca-certificates \ + libgl1 \ + libglib2.0-0 \ + libxrender1 \ + libxext6 \ + libsm6 \ + libice6 \ + libx11-6 \ + && rm -rf /var/lib/apt/lists/* + +# cadquery-ocp 轮子较大,须加大 pip 超时;覆盖:--build-arg PIP_DEFAULT_TIMEOUT=600 +ARG PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple +ARG PIP_DEFAULT_TIMEOUT=300 +COPY requirements-geometry.txt /tmp/requirements-geometry.txt +RUN python3 -m pip install --no-cache-dir --break-system-packages \ + --default-timeout="${PIP_DEFAULT_TIMEOUT}" \ + --retries 10 \ + -i "${PIP_INDEX_URL}" \ + -r /tmp/requirements-geometry.txt \ + && rm /tmp/requirements-geometry.txt \ + && python3 -c "import cadquery; print('cadquery OK')" + +ENV PYTHON3_PATH=/usr/bin/python3 + +# 创建非 root 用户(bookworm 用 groupadd/useradd) +RUN groupadd --gid 1001 nodejs && \ + useradd --uid 1001 --gid nodejs --system --shell /usr/sbin/nologin appuser + +# 安装pnpm (用于运行时) +RUN corepack enable && corepack prepare pnpm@latest --activate + +# 复制构建产物 +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/package.json ./package.json +COPY --from=builder /app/drizzle ./drizzle +# STEP 装配检测:子进程脚本(runStepParseMvp 从 dist 相对定位 ../scripts) +COPY --from=builder /app/scripts ./scripts + +# 创建日志目录 +RUN mkdir -p /app/logs && chown -R appuser:nodejs /app + +# 切换到非root用户 +USER appuser + +# 暴露端口 +EXPOSE 3000 + +# 健康检查 (使用环境变量中的端口,默认3000) +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:${PORT:-3000}/api/health || exit 1 + +# 使用dumb-init作为PID 1进程 +ENTRYPOINT ["dumb-init", "--"] + +# 启动应用 +CMD ["node", "dist/index.js"] diff --git a/src/client/index.html b/src/client/index.html new file mode 100644 index 0000000..58ddc9c --- /dev/null +++ b/src/client/index.html @@ -0,0 +1,22 @@ + + + + + + + 宏量点云处理软件 + + + + +
+ + + + diff --git a/src/client/public/.gitkeep b/src/client/public/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/client/public/__manus__/debug-collector.js b/src/client/public/__manus__/debug-collector.js new file mode 100644 index 0000000..0504555 --- /dev/null +++ b/src/client/public/__manus__/debug-collector.js @@ -0,0 +1,821 @@ +/** + * Manus Debug Collector (agent-friendly) + * + * Captures: + * 1) Console logs + * 2) Network requests (fetch + XHR) + * 3) User interactions (semantic uiEvents: click/type/submit/nav/scroll/etc.) + * + * Data is periodically sent to /__manus__/logs + * Note: uiEvents are mirrored to sessionEvents for sessionReplay.log + */ +(function () { + "use strict"; + + // Prevent double initialization + if (window.__MANUS_DEBUG_COLLECTOR__) return; + + // ========================================================================== + // Configuration + // ========================================================================== + const CONFIG = { + reportEndpoint: "/__manus__/logs", + bufferSize: { + console: 500, + network: 200, + // semantic, agent-friendly UI events + ui: 500, + }, + reportInterval: 2000, + sensitiveFields: [ + "password", + "token", + "secret", + "key", + "authorization", + "cookie", + "session", + ], + maxBodyLength: 10240, + // UI event logging privacy policy: + // - inputs matching sensitiveFields or type=password are masked by default + // - non-sensitive inputs log up to 200 chars + uiInputMaxLen: 200, + uiTextMaxLen: 80, + // Scroll throttling: minimum ms between scroll events + scrollThrottleMs: 500, + }; + + // ========================================================================== + // Storage + // ========================================================================== + const store = { + consoleLogs: [], + networkRequests: [], + uiEvents: [], + lastReportTime: Date.now(), + lastScrollTime: 0, + }; + + // ========================================================================== + // Utility Functions + // ========================================================================== + + function sanitizeValue(value, depth) { + if (depth === void 0) depth = 0; + if (depth > 5) return "[Max Depth]"; + if (value === null) return null; + if (value === undefined) return undefined; + + if (typeof value === "string") { + return value.length > 1000 ? value.slice(0, 1000) + "...[truncated]" : value; + } + + if (typeof value !== "object") return value; + + if (Array.isArray(value)) { + return value.slice(0, 100).map(function (v) { + return sanitizeValue(v, depth + 1); + }); + } + + var sanitized = {}; + for (var k in value) { + if (Object.prototype.hasOwnProperty.call(value, k)) { + var isSensitive = CONFIG.sensitiveFields.some(function (f) { + return k.toLowerCase().indexOf(f) !== -1; + }); + if (isSensitive) { + sanitized[k] = "[REDACTED]"; + } else { + sanitized[k] = sanitizeValue(value[k], depth + 1); + } + } + } + return sanitized; + } + + function formatArg(arg) { + try { + if (arg instanceof Error) { + return { type: "Error", message: arg.message, stack: arg.stack }; + } + if (typeof arg === "object") return sanitizeValue(arg); + return String(arg); + } catch (e) { + return "[Unserializable]"; + } + } + + function formatArgs(args) { + var result = []; + for (var i = 0; i < args.length; i++) result.push(formatArg(args[i])); + return result; + } + + function pruneBuffer(buffer, maxSize) { + if (buffer.length > maxSize) buffer.splice(0, buffer.length - maxSize); + } + + function tryParseJson(str) { + if (typeof str !== "string") return str; + try { + return JSON.parse(str); + } catch (e) { + return str; + } + } + + // ========================================================================== + // Semantic UI Event Logging (agent-friendly) + // ========================================================================== + + function shouldIgnoreTarget(target) { + try { + if (!target || !(target instanceof Element)) return false; + return !!target.closest(".manus-no-record"); + } catch (e) { + return false; + } + } + + function compactText(s, maxLen) { + try { + var t = (s || "").trim().replace(/\s+/g, " "); + if (!t) return ""; + return t.length > maxLen ? t.slice(0, maxLen) + "…" : t; + } catch (e) { + return ""; + } + } + + function elText(el) { + try { + var t = el.innerText || el.textContent || ""; + return compactText(t, CONFIG.uiTextMaxLen); + } catch (e) { + return ""; + } + } + + function describeElement(el) { + if (!el || !(el instanceof Element)) return null; + + var getAttr = function (name) { + return el.getAttribute(name); + }; + + var tag = el.tagName ? el.tagName.toLowerCase() : null; + var id = el.id || null; + var name = getAttr("name") || null; + var role = getAttr("role") || null; + var ariaLabel = getAttr("aria-label") || null; + + var dataLoc = getAttr("data-loc") || null; + var testId = + getAttr("data-testid") || + getAttr("data-test-id") || + getAttr("data-test") || + null; + + var type = tag === "input" ? (getAttr("type") || "text") : null; + var href = tag === "a" ? getAttr("href") || null : null; + + // a small, stable hint for agents (avoid building full CSS paths) + var selectorHint = null; + if (testId) selectorHint = '[data-testid="' + testId + '"]'; + else if (dataLoc) selectorHint = '[data-loc="' + dataLoc + '"]'; + else if (id) selectorHint = "#" + id; + else selectorHint = tag || "unknown"; + + return { + tag: tag, + id: id, + name: name, + type: type, + role: role, + ariaLabel: ariaLabel, + testId: testId, + dataLoc: dataLoc, + href: href, + text: elText(el), + selectorHint: selectorHint, + }; + } + + function isSensitiveField(el) { + if (!el || !(el instanceof Element)) return false; + var tag = el.tagName ? el.tagName.toLowerCase() : ""; + if (tag !== "input" && tag !== "textarea") return false; + + var type = (el.getAttribute("type") || "").toLowerCase(); + if (type === "password") return true; + + var name = (el.getAttribute("name") || "").toLowerCase(); + var id = (el.id || "").toLowerCase(); + + return CONFIG.sensitiveFields.some(function (f) { + return name.indexOf(f) !== -1 || id.indexOf(f) !== -1; + }); + } + + function getInputValueSafe(el) { + if (!el || !(el instanceof Element)) return null; + var tag = el.tagName ? el.tagName.toLowerCase() : ""; + if (tag !== "input" && tag !== "textarea" && tag !== "select") return null; + + var v = ""; + try { + v = el.value != null ? String(el.value) : ""; + } catch (e) { + v = ""; + } + + if (isSensitiveField(el)) return { masked: true, length: v.length }; + + if (v.length > CONFIG.uiInputMaxLen) v = v.slice(0, CONFIG.uiInputMaxLen) + "…"; + return v; + } + + function logUiEvent(kind, payload) { + var entry = { + timestamp: Date.now(), + kind: kind, + url: location.href, + viewport: { width: window.innerWidth, height: window.innerHeight }, + payload: sanitizeValue(payload), + }; + store.uiEvents.push(entry); + pruneBuffer(store.uiEvents, CONFIG.bufferSize.ui); + } + + function installUiEventListeners() { + // Clicks + document.addEventListener( + "click", + function (e) { + var t = e.target; + if (shouldIgnoreTarget(t)) return; + logUiEvent("click", { + target: describeElement(t), + x: e.clientX, + y: e.clientY, + }); + }, + true + ); + + // Typing "commit" events + document.addEventListener( + "change", + function (e) { + var t = e.target; + if (shouldIgnoreTarget(t)) return; + logUiEvent("change", { + target: describeElement(t), + value: getInputValueSafe(t), + }); + }, + true + ); + + document.addEventListener( + "focusin", + function (e) { + var t = e.target; + if (shouldIgnoreTarget(t)) return; + logUiEvent("focusin", { target: describeElement(t) }); + }, + true + ); + + document.addEventListener( + "focusout", + function (e) { + var t = e.target; + if (shouldIgnoreTarget(t)) return; + logUiEvent("focusout", { + target: describeElement(t), + value: getInputValueSafe(t), + }); + }, + true + ); + + // Enter/Escape are useful for form flows & modals + document.addEventListener( + "keydown", + function (e) { + if (e.key !== "Enter" && e.key !== "Escape") return; + var t = e.target; + if (shouldIgnoreTarget(t)) return; + logUiEvent("keydown", { key: e.key, target: describeElement(t) }); + }, + true + ); + + // Form submissions + document.addEventListener( + "submit", + function (e) { + var t = e.target; + if (shouldIgnoreTarget(t)) return; + logUiEvent("submit", { target: describeElement(t) }); + }, + true + ); + + // Throttled scroll events + window.addEventListener( + "scroll", + function () { + var now = Date.now(); + if (now - store.lastScrollTime < CONFIG.scrollThrottleMs) return; + store.lastScrollTime = now; + + logUiEvent("scroll", { + scrollX: window.scrollX, + scrollY: window.scrollY, + documentHeight: document.documentElement.scrollHeight, + viewportHeight: window.innerHeight, + }); + }, + { passive: true } + ); + + // Navigation tracking for SPAs + function nav(reason) { + logUiEvent("navigate", { reason: reason }); + } + + var origPush = history.pushState; + history.pushState = function () { + origPush.apply(this, arguments); + nav("pushState"); + }; + + var origReplace = history.replaceState; + history.replaceState = function () { + origReplace.apply(this, arguments); + nav("replaceState"); + }; + + window.addEventListener("popstate", function () { + nav("popstate"); + }); + window.addEventListener("hashchange", function () { + nav("hashchange"); + }); + } + + // ========================================================================== + // Console Interception + // ========================================================================== + + var originalConsole = { + log: console.log.bind(console), + debug: console.debug.bind(console), + info: console.info.bind(console), + warn: console.warn.bind(console), + error: console.error.bind(console), + }; + + ["log", "debug", "info", "warn", "error"].forEach(function (method) { + console[method] = function () { + var args = Array.prototype.slice.call(arguments); + + var entry = { + timestamp: Date.now(), + level: method.toUpperCase(), + args: formatArgs(args), + stack: method === "error" ? new Error().stack : null, + }; + + store.consoleLogs.push(entry); + pruneBuffer(store.consoleLogs, CONFIG.bufferSize.console); + + originalConsole[method].apply(console, args); + }; + }); + + window.addEventListener("error", function (event) { + store.consoleLogs.push({ + timestamp: Date.now(), + level: "ERROR", + args: [ + { + type: "UncaughtError", + message: event.message, + filename: event.filename, + lineno: event.lineno, + colno: event.colno, + stack: event.error ? event.error.stack : null, + }, + ], + stack: event.error ? event.error.stack : null, + }); + pruneBuffer(store.consoleLogs, CONFIG.bufferSize.console); + + // Mark an error moment in UI event stream for agents + logUiEvent("error", { + message: event.message, + filename: event.filename, + lineno: event.lineno, + colno: event.colno, + }); + }); + + window.addEventListener("unhandledrejection", function (event) { + var reason = event.reason; + store.consoleLogs.push({ + timestamp: Date.now(), + level: "ERROR", + args: [ + { + type: "UnhandledRejection", + reason: reason && reason.message ? reason.message : String(reason), + stack: reason && reason.stack ? reason.stack : null, + }, + ], + stack: reason && reason.stack ? reason.stack : null, + }); + pruneBuffer(store.consoleLogs, CONFIG.bufferSize.console); + + logUiEvent("unhandledrejection", { + reason: reason && reason.message ? reason.message : String(reason), + }); + }); + + // ========================================================================== + // Fetch Interception + // ========================================================================== + + var originalFetch = window.fetch.bind(window); + + window.fetch = function (input, init) { + init = init || {}; + var startTime = Date.now(); + // Handle string, Request object, or URL object + var url = typeof input === "string" + ? input + : (input && (input.url || input.href || String(input))) || ""; + var method = init.method || (input && input.method) || "GET"; + + // Don't intercept internal requests + if (url.indexOf("/__manus__/") === 0) { + return originalFetch(input, init); + } + + // Safely parse headers (avoid breaking if headers format is invalid) + var requestHeaders = {}; + try { + if (init.headers) { + requestHeaders = Object.fromEntries(new Headers(init.headers).entries()); + } + } catch (e) { + requestHeaders = { _parseError: true }; + } + + var entry = { + timestamp: startTime, + type: "fetch", + method: method.toUpperCase(), + url: url, + request: { + headers: requestHeaders, + body: init.body ? sanitizeValue(tryParseJson(init.body)) : null, + }, + response: null, + duration: null, + error: null, + }; + + return originalFetch(input, init) + .then(function (response) { + entry.duration = Date.now() - startTime; + + var contentType = (response.headers.get("content-type") || "").toLowerCase(); + var contentLength = response.headers.get("content-length"); + + entry.response = { + status: response.status, + statusText: response.statusText, + headers: Object.fromEntries(response.headers.entries()), + body: null, + }; + + // Semantic network hint for agents on failures (sync, no need to wait for body) + if (response.status >= 400) { + logUiEvent("network_error", { + kind: "fetch", + method: entry.method, + url: entry.url, + status: response.status, + statusText: response.statusText, + }); + } + + // Skip body capture for streaming responses (SSE, etc.) to avoid memory leaks + var isStreaming = contentType.indexOf("text/event-stream") !== -1 || + contentType.indexOf("application/stream") !== -1 || + contentType.indexOf("application/x-ndjson") !== -1; + if (isStreaming) { + entry.response.body = "[Streaming response - not captured]"; + store.networkRequests.push(entry); + pruneBuffer(store.networkRequests, CONFIG.bufferSize.network); + return response; + } + + // Skip body capture for large responses to avoid memory issues + if (contentLength && parseInt(contentLength, 10) > CONFIG.maxBodyLength) { + entry.response.body = "[Response too large: " + contentLength + " bytes]"; + store.networkRequests.push(entry); + pruneBuffer(store.networkRequests, CONFIG.bufferSize.network); + return response; + } + + // Skip body capture for binary content types + var isBinary = contentType.indexOf("image/") !== -1 || + contentType.indexOf("video/") !== -1 || + contentType.indexOf("audio/") !== -1 || + contentType.indexOf("application/octet-stream") !== -1 || + contentType.indexOf("application/pdf") !== -1 || + contentType.indexOf("application/zip") !== -1; + if (isBinary) { + entry.response.body = "[Binary content: " + contentType + "]"; + store.networkRequests.push(entry); + pruneBuffer(store.networkRequests, CONFIG.bufferSize.network); + return response; + } + + // For text responses, clone and read body in background + var clonedResponse = response.clone(); + + // Async: read body in background, don't block the response + clonedResponse + .text() + .then(function (text) { + if (text.length <= CONFIG.maxBodyLength) { + entry.response.body = sanitizeValue(tryParseJson(text)); + } else { + entry.response.body = text.slice(0, CONFIG.maxBodyLength) + "...[truncated]"; + } + }) + .catch(function () { + entry.response.body = "[Unable to read body]"; + }) + .finally(function () { + store.networkRequests.push(entry); + pruneBuffer(store.networkRequests, CONFIG.bufferSize.network); + }); + + // Return response immediately, don't wait for body reading + return response; + }) + .catch(function (error) { + entry.duration = Date.now() - startTime; + entry.error = { message: error.message, stack: error.stack }; + + store.networkRequests.push(entry); + pruneBuffer(store.networkRequests, CONFIG.bufferSize.network); + + logUiEvent("network_error", { + kind: "fetch", + method: entry.method, + url: entry.url, + message: error.message, + }); + + throw error; + }); + }; + + // ========================================================================== + // XHR Interception + // ========================================================================== + + var originalXHROpen = XMLHttpRequest.prototype.open; + var originalXHRSend = XMLHttpRequest.prototype.send; + + XMLHttpRequest.prototype.open = function (method, url) { + this._manusData = { + method: (method || "GET").toUpperCase(), + url: url, + startTime: null, + }; + return originalXHROpen.apply(this, arguments); + }; + + XMLHttpRequest.prototype.send = function (body) { + var xhr = this; + + if ( + xhr._manusData && + xhr._manusData.url && + xhr._manusData.url.indexOf("/__manus__/") !== 0 + ) { + xhr._manusData.startTime = Date.now(); + xhr._manusData.requestBody = body ? sanitizeValue(tryParseJson(body)) : null; + + xhr.addEventListener("load", function () { + var contentType = (xhr.getResponseHeader("content-type") || "").toLowerCase(); + var responseBody = null; + + // Skip body capture for streaming responses + var isStreaming = contentType.indexOf("text/event-stream") !== -1 || + contentType.indexOf("application/stream") !== -1 || + contentType.indexOf("application/x-ndjson") !== -1; + + // Skip body capture for binary content types + var isBinary = contentType.indexOf("image/") !== -1 || + contentType.indexOf("video/") !== -1 || + contentType.indexOf("audio/") !== -1 || + contentType.indexOf("application/octet-stream") !== -1 || + contentType.indexOf("application/pdf") !== -1 || + contentType.indexOf("application/zip") !== -1; + + if (isStreaming) { + responseBody = "[Streaming response - not captured]"; + } else if (isBinary) { + responseBody = "[Binary content: " + contentType + "]"; + } else { + // Safe to read responseText for text responses + try { + var text = xhr.responseText || ""; + if (text.length > CONFIG.maxBodyLength) { + responseBody = text.slice(0, CONFIG.maxBodyLength) + "...[truncated]"; + } else { + responseBody = sanitizeValue(tryParseJson(text)); + } + } catch (e) { + // responseText may throw for non-text responses + responseBody = "[Unable to read response: " + e.message + "]"; + } + } + + var entry = { + timestamp: xhr._manusData.startTime, + type: "xhr", + method: xhr._manusData.method, + url: xhr._manusData.url, + request: { body: xhr._manusData.requestBody }, + response: { + status: xhr.status, + statusText: xhr.statusText, + body: responseBody, + }, + duration: Date.now() - xhr._manusData.startTime, + error: null, + }; + + store.networkRequests.push(entry); + pruneBuffer(store.networkRequests, CONFIG.bufferSize.network); + + if (entry.response && entry.response.status >= 400) { + logUiEvent("network_error", { + kind: "xhr", + method: entry.method, + url: entry.url, + status: entry.response.status, + statusText: entry.response.statusText, + }); + } + }); + + xhr.addEventListener("error", function () { + var entry = { + timestamp: xhr._manusData.startTime, + type: "xhr", + method: xhr._manusData.method, + url: xhr._manusData.url, + request: { body: xhr._manusData.requestBody }, + response: null, + duration: Date.now() - xhr._manusData.startTime, + error: { message: "Network error" }, + }; + + store.networkRequests.push(entry); + pruneBuffer(store.networkRequests, CONFIG.bufferSize.network); + + logUiEvent("network_error", { + kind: "xhr", + method: entry.method, + url: entry.url, + message: "Network error", + }); + }); + } + + return originalXHRSend.apply(this, arguments); + }; + + // ========================================================================== + // Data Reporting + // ========================================================================== + + function reportLogs() { + var consoleLogs = store.consoleLogs.splice(0); + var networkRequests = store.networkRequests.splice(0); + var uiEvents = store.uiEvents.splice(0); + + // Skip if no new data + if ( + consoleLogs.length === 0 && + networkRequests.length === 0 && + uiEvents.length === 0 + ) { + return Promise.resolve(); + } + + var payload = { + timestamp: Date.now(), + consoleLogs: consoleLogs, + networkRequests: networkRequests, + // Mirror uiEvents to sessionEvents for sessionReplay.log + sessionEvents: uiEvents, + // agent-friendly semantic events + uiEvents: uiEvents, + }; + + return originalFetch(CONFIG.reportEndpoint, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }).catch(function () { + // Put data back on failure (but respect limits) + store.consoleLogs = consoleLogs.concat(store.consoleLogs); + store.networkRequests = networkRequests.concat(store.networkRequests); + store.uiEvents = uiEvents.concat(store.uiEvents); + + pruneBuffer(store.consoleLogs, CONFIG.bufferSize.console); + pruneBuffer(store.networkRequests, CONFIG.bufferSize.network); + pruneBuffer(store.uiEvents, CONFIG.bufferSize.ui); + }); + } + + // Periodic reporting + setInterval(reportLogs, CONFIG.reportInterval); + + // Report on page unload + window.addEventListener("beforeunload", function () { + var consoleLogs = store.consoleLogs; + var networkRequests = store.networkRequests; + var uiEvents = store.uiEvents; + + if ( + consoleLogs.length === 0 && + networkRequests.length === 0 && + uiEvents.length === 0 + ) { + return; + } + + var payload = { + timestamp: Date.now(), + consoleLogs: consoleLogs, + networkRequests: networkRequests, + // Mirror uiEvents to sessionEvents for sessionReplay.log + sessionEvents: uiEvents, + uiEvents: uiEvents, + }; + + if (navigator.sendBeacon) { + var payloadStr = JSON.stringify(payload); + // sendBeacon has ~64KB limit, truncate if too large + var MAX_BEACON_SIZE = 60000; // Leave some margin + if (payloadStr.length > MAX_BEACON_SIZE) { + // Prioritize: keep recent events, drop older logs + var truncatedPayload = { + timestamp: Date.now(), + consoleLogs: consoleLogs.slice(-50), + networkRequests: networkRequests.slice(-20), + sessionEvents: uiEvents.slice(-100), + uiEvents: uiEvents.slice(-100), + _truncated: true, + }; + payloadStr = JSON.stringify(truncatedPayload); + } + navigator.sendBeacon(CONFIG.reportEndpoint, payloadStr); + } + }); + + // ========================================================================== + // Initialization + // ========================================================================== + + // Install semantic UI listeners ASAP + try { + installUiEventListeners(); + } catch (e) { + console.warn("[Manus] Failed to install UI listeners:", e); + } + + // Mark as initialized + window.__MANUS_DEBUG_COLLECTOR__ = { + version: "2.0-no-rrweb", + store: store, + forceReport: reportLogs, + }; + + console.debug("[Manus] Debug collector initialized (no rrweb, UI events only)"); +})(); diff --git a/src/client/src/App.tsx b/src/client/src/App.tsx new file mode 100644 index 0000000..6f24bf4 --- /dev/null +++ b/src/client/src/App.tsx @@ -0,0 +1,42 @@ +import { Toaster } from "@/components/ui/sonner"; +import { TooltipProvider } from "@/components/ui/tooltip"; +import NotFound from "@/pages/NotFound"; +import { Route, Switch } from "wouter"; +import ErrorBoundary from "./components/ErrorBoundary"; +import { ThemeProvider } from "./contexts/ThemeContext"; +import Home from "./pages/Home"; + +function Router() { + // make sure to consider if you need authentication for certain routes + return ( + + + + {/* Final fallback route */} + + + ); +} + +// NOTE: About Theme +// - First choose a default theme according to your design style (dark or light bg), than change color palette in index.css +// to keep consistent foreground/background color across components +// - If you want to make theme switchable, pass `switchable` ThemeProvider and use `useTheme` hook + +function App() { + return ( + + + + + + + + + ); +} + +export default App; diff --git a/src/client/src/_core/hooks/useAuth.ts b/src/client/src/_core/hooks/useAuth.ts new file mode 100644 index 0000000..dcef9bd --- /dev/null +++ b/src/client/src/_core/hooks/useAuth.ts @@ -0,0 +1,84 @@ +import { getLoginUrl } from "@/const"; +import { trpc } from "@/lib/trpc"; +import { TRPCClientError } from "@trpc/client"; +import { useCallback, useEffect, useMemo } from "react"; + +type UseAuthOptions = { + redirectOnUnauthenticated?: boolean; + redirectPath?: string; +}; + +export function useAuth(options?: UseAuthOptions) { + const { redirectOnUnauthenticated = false, redirectPath = getLoginUrl() } = + options ?? {}; + const utils = trpc.useUtils(); + + const meQuery = trpc.auth.me.useQuery(undefined, { + retry: false, + refetchOnWindowFocus: false, + }); + + const logoutMutation = trpc.auth.logout.useMutation({ + onSuccess: () => { + utils.auth.me.setData(undefined, null); + }, + }); + + const logout = useCallback(async () => { + try { + await logoutMutation.mutateAsync(); + } catch (error: unknown) { + if ( + error instanceof TRPCClientError && + error.data?.code === "UNAUTHORIZED" + ) { + return; + } + throw error; + } finally { + utils.auth.me.setData(undefined, null); + await utils.auth.me.invalidate(); + } + }, [logoutMutation, utils]); + + const state = useMemo(() => { + localStorage.setItem( + "manus-runtime-user-info", + JSON.stringify(meQuery.data) + ); + return { + user: meQuery.data ?? null, + loading: meQuery.isLoading || logoutMutation.isPending, + error: meQuery.error ?? logoutMutation.error ?? null, + isAuthenticated: Boolean(meQuery.data), + }; + }, [ + meQuery.data, + meQuery.error, + meQuery.isLoading, + logoutMutation.error, + logoutMutation.isPending, + ]); + + useEffect(() => { + if (!redirectOnUnauthenticated) return; + if (meQuery.isLoading || logoutMutation.isPending) return; + if (state.user) return; + if (typeof window === "undefined") return; + if (window.location.pathname === redirectPath) return; + + window.location.href = redirectPath + }, [ + redirectOnUnauthenticated, + redirectPath, + logoutMutation.isPending, + meQuery.isLoading, + state.user, + ]); + + return { + ...state, + refresh: () => meQuery.refetch(), + logout, + }; +} diff --git a/src/client/src/components/AIChatBox.tsx b/src/client/src/components/AIChatBox.tsx new file mode 100644 index 0000000..1c00871 --- /dev/null +++ b/src/client/src/components/AIChatBox.tsx @@ -0,0 +1,335 @@ +import { Button } from "@/components/ui/button"; +import { Textarea } from "@/components/ui/textarea"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { cn } from "@/lib/utils"; +import { Loader2, Send, User, Sparkles } from "lucide-react"; +import { useState, useEffect, useRef } from "react"; +import { Streamdown } from "streamdown"; + +/** + * Message type matching server-side LLM Message interface + */ +export type Message = { + role: "system" | "user" | "assistant"; + content: string; +}; + +export type AIChatBoxProps = { + /** + * Messages array to display in the chat. + * Should match the format used by invokeLLM on the server. + */ + messages: Message[]; + + /** + * Callback when user sends a message. + * Typically you'll call a tRPC mutation here to invoke the LLM. + */ + onSendMessage: (content: string) => void; + + /** + * Whether the AI is currently generating a response + */ + isLoading?: boolean; + + /** + * Placeholder text for the input field + */ + placeholder?: string; + + /** + * Custom className for the container + */ + className?: string; + + /** + * Height of the chat box (default: 600px) + */ + height?: string | number; + + /** + * Empty state message to display when no messages + */ + emptyStateMessage?: string; + + /** + * Suggested prompts to display in empty state + * Click to send directly + */ + suggestedPrompts?: string[]; +}; + +/** + * A ready-to-use AI chat box component that integrates with the LLM system. + * + * Features: + * - Matches server-side Message interface for seamless integration + * - Markdown rendering with Streamdown + * - Auto-scrolls to latest message + * - Loading states + * - Uses global theme colors from index.css + * + * @example + * ```tsx + * const ChatPage = () => { + * const [messages, setMessages] = useState([ + * { role: "system", content: "You are a helpful assistant." } + * ]); + * + * const chatMutation = trpc.ai.chat.useMutation({ + * onSuccess: (response) => { + * // Assuming your tRPC endpoint returns the AI response as a string + * setMessages(prev => [...prev, { + * role: "assistant", + * content: response + * }]); + * }, + * onError: (error) => { + * console.error("Chat error:", error); + * // Optionally show error message to user + * } + * }); + * + * const handleSend = (content: string) => { + * const newMessages = [...messages, { role: "user", content }]; + * setMessages(newMessages); + * chatMutation.mutate({ messages: newMessages }); + * }; + * + * return ( + * + * ); + * }; + * ``` + */ +export function AIChatBox({ + messages, + onSendMessage, + isLoading = false, + placeholder = "Type your message...", + className, + height = "600px", + emptyStateMessage = "Start a conversation with AI", + suggestedPrompts, +}: AIChatBoxProps) { + const [input, setInput] = useState(""); + const scrollAreaRef = useRef(null); + const containerRef = useRef(null); + const inputAreaRef = useRef(null); + const textareaRef = useRef(null); + + // Filter out system messages + const displayMessages = messages.filter((msg) => msg.role !== "system"); + + // Calculate min-height for last assistant message to push user message to top + const [minHeightForLastMessage, setMinHeightForLastMessage] = useState(0); + + useEffect(() => { + if (containerRef.current && inputAreaRef.current) { + const containerHeight = containerRef.current.offsetHeight; + const inputHeight = inputAreaRef.current.offsetHeight; + const scrollAreaHeight = containerHeight - inputHeight; + + // Reserve space for: + // - padding (p-4 = 32px top+bottom) + // - user message: 40px (item height) + 16px (margin-top from space-y-4) = 56px + // Note: margin-bottom is not counted because it naturally pushes the assistant message down + const userMessageReservedHeight = 56; + const calculatedHeight = scrollAreaHeight - 32 - userMessageReservedHeight; + + setMinHeightForLastMessage(Math.max(0, calculatedHeight)); + } + }, []); + + // Scroll to bottom helper function with smooth animation + const scrollToBottom = () => { + const viewport = scrollAreaRef.current?.querySelector( + '[data-radix-scroll-area-viewport]' + ) as HTMLDivElement; + + if (viewport) { + requestAnimationFrame(() => { + viewport.scrollTo({ + top: viewport.scrollHeight, + behavior: 'smooth' + }); + }); + } + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const trimmedInput = input.trim(); + if (!trimmedInput || isLoading) return; + + onSendMessage(trimmedInput); + setInput(""); + + // Scroll immediately after sending + scrollToBottom(); + + // Keep focus on input + textareaRef.current?.focus(); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleSubmit(e); + } + }; + + return ( +
+ {/* Messages Area */} +
+ {displayMessages.length === 0 ? ( +
+
+
+ +

{emptyStateMessage}

+
+ + {suggestedPrompts && suggestedPrompts.length > 0 && ( +
+ {suggestedPrompts.map((prompt, index) => ( + + ))} +
+ )} +
+
+ ) : ( + +
+ {displayMessages.map((message, index) => { + // Apply min-height to last message only if NOT loading (when loading, the loading indicator gets it) + const isLastMessage = index === displayMessages.length - 1; + const shouldApplyMinHeight = + isLastMessage && !isLoading && minHeightForLastMessage > 0; + + return ( +
+ {message.role === "assistant" && ( +
+ +
+ )} + +
+ {message.role === "assistant" ? ( +
+ {message.content} +
+ ) : ( +

+ {message.content} +

+ )} +
+ + {message.role === "user" && ( +
+ +
+ )} +
+ ); + })} + + {isLoading && ( +
0 + ? { minHeight: `${minHeightForLastMessage}px` } + : undefined + } + > +
+ +
+
+ +
+
+ )} +
+
+ )} +
+ + {/* Input Area */} +
+