From 38070c78d9e442b2ccd2aa98cf7ace0f05e518a2 Mon Sep 17 00:00:00 2001 From: insome Date: Thu, 21 May 2026 19:12:01 +0800 Subject: [PATCH 01/24] Align Prengine engineering runtime strategy --- .env.production.example | 14 + .tmp-paperclip-direct-flow.png | Bin 0 -> 138931 bytes .tmp-paperclip-flow.png | Bin 0 -> 127733 bytes 01-product/PRD.md | 2 + 02-architecture/ARCHITOKEN-SOURCE-OF-TRUTH.md | 2 + 02-architecture/BUSINESS_MODULE_WORKBENCH.md | 192 +- 02-architecture/CONSTITUTION.md | 17 +- 02-architecture/DIGITAL_TWIN.md | 6 +- ...EAVY_STEEL_AI_TOKEN_COMMERCIAL_WORKFLOW.md | 2 +- 02-architecture/MODULES.md | 9 +- 02-architecture/OPEN_SOURCE_RADAR.md | 8 +- 03-frontend/app/api/ai/openclaw/chat/route.ts | 392 ++ .../[fileId]/cad-derivative/route.ts | 7 +- .../[fileId]/ifc-derivative/route.ts | 65 +- .../local-files/[fileId]/native-open/route.ts | 176 +- .../api/local-files/[fileId]/preview/route.ts | 145 +- .../[fileId]/skp-derivative/route.ts | 127 + .../app/api/openclaw/ui/[[...path]]/route.ts | 153 + .../app/api/paperclip/ui/[[...path]]/route.ts | 299 + 03-frontend/app/globals.css | 1225 +++- 03-frontend/app/studio/page.tsx | 130 +- .../app/studio/works/[workId]/page.tsx | 2 +- 03-frontend/bun.lock | 30 + .../components/AICenterManagementPanels.tsx | 557 ++ 03-frontend/components/AICenterWorkbench.tsx | 10 +- .../components/ArchivePackageViewer.tsx | 2 +- 03-frontend/components/BIMViewer.tsx | 331 +- .../ConceptDesignStudioWorkbench.tsx | 381 ++ .../DetailedDesignPlanFinderWorkbench.tsx | 2741 +++++++++ .../components/DigitalTwinOperationsPanel.tsx | 1412 ++++- .../components/DigitalTwinWebGPUViewport.tsx | 439 ++ .../components/FeichuanPlanningWorkbench.tsx | 1697 ++++++ .../components/FileManagerWorkbench.tsx | 11 +- .../components/FileOperationDialog.tsx | 1 + 03-frontend/components/FilePreviewDrawer.tsx | 7 +- .../components/InsomeModuleWorkbench.tsx | 8 +- .../LeadRequirementWorkflowPanel.tsx | 452 +- 03-frontend/components/LocalFileUploader.tsx | 16 +- .../components/ModuleDetailWorkbench.tsx | 91 +- 03-frontend/components/ModuleFileExplorer.tsx | 31 +- .../components/ModuleOperationalPanel.tsx | 4 +- .../components/ModuleWorkbenchShell.tsx | 377 +- .../components/OfficeDocumentViewer.tsx | 60 +- .../components/OpenEngineeringViewer.test.ts | 129 +- .../components/OpenEngineeringViewer.tsx | 5124 +++++++++++++++-- .../PaperclipProductionWorkbench.tsx | 120 + .../components/ProjectPlanningStudio.tsx | 1215 +++- ...StandardLibrarySemanticDictionaryPanel.tsx | 290 + .../components/UniversalFileViewer.tsx | 19 +- 03-frontend/components/home/home-top-nav.tsx | 2 +- .../home/inspiration-page/inspiration-nav.tsx | 2 +- .../template-detail-dialog.tsx | 4 +- .../components/landing/landing-hero-3d.tsx | 11 +- .../components/landing/landing-hero-model.tsx | 7 +- .../components/landing/landing-hero.tsx | 29 +- .../components/landing/landing-nav.tsx | 2 +- 03-frontend/components/landing/top-nav.tsx | 2 +- .../planning/feichuan-gantt/LICENSE | 21 + .../planning/feichuan-gantt/README.md | 13 + .../feichuan-gantt/src/assets/images/drag.png | Bin 0 -> 2544 bytes .../src/assets/images/folder.png | Bin 0 -> 3193 bytes .../src/component/drag-tree-table/column.scss | 51 + .../src/component/drag-tree-table/column.tsx | 154 + .../src/component/drag-tree-table/func.ts | 53 + .../src/component/drag-tree-table/row.scss | 119 + .../src/component/drag-tree-table/row.tsx | 101 + .../component/drag-tree-table/treetable.scss | 48 + .../gantt-task/canlendar/calendar.scss | 62 + .../gantt-task/canlendar/canlendar.tsx | 162 + .../component/gantt-task/gantt/critical-path | 142 + .../gantt-task/gantt/gantt-demo-bigdata.tsx | 116 + .../gantt-task/gantt/gantt-demo-viewer.tsx | 297 + .../gantt-task/gantt/gantt-demo.scss | 116 + .../component/gantt-task/gantt/gantt-demo.tsx | 395 ++ .../gantt-task/gantt/gantt-task.scss | 419 ++ .../gantt-task/gantt/network-gantt.tsx | 757 +++ .../component/gantt-task/gantt/network.tsx | 155 + .../gantt-task/helpers/bar-helper.ts | 333 ++ .../gantt-task/helpers/date-helper.ts | 245 + .../gantt-task/task-bar/linkLine.tsx | 64 + .../component/gantt-task/task-bar/taskBar.tsx | 185 + .../gantt-task/time-scaled/ArrowCircle.tsx | 48 + .../gantt-task/time-scaled/ArrowLine.tsx | 119 + .../gantt-task/time-scaled/DashedLine.tsx | 62 + .../gantt-task/time-scaled/PertArrow.tsx | 29 + .../gantt-task/time-scaled/PertBlock.tsx | 59 + .../gantt-task/time-scaled/Scaled.tsx | 45 + .../gantt-task/time-scaled/ScaledArrow.tsx | 88 + .../src/component/template/class-tsx.tsx | 14 + .../src/redux/networkGantt/ganttReducer.ts | 20 + .../feichuan-gantt/src/redux/store.ts | 46 + .../feichuan-gantt/src/types/bar-task.ts | 95 + .../feichuan-gantt/src/types/date-setup.ts | 7 + .../feichuan-gantt/src/types/global.d.ts | 8 + .../feichuan-gantt/src/types/public-types.ts | 207 + 03-frontend/components/shared/unified-nav.tsx | 7 +- 03-frontend/components/shared/work-card.tsx | 8 +- .../components/shared/work-detail-dialog.tsx | 4 +- .../components/shared/works-explorer.tsx | 9 +- .../editor/properties/wall-properties.tsx | 18 +- .../showcase-page/studio-showcase-nav.tsx | 2 +- 03-frontend/content/works.mock.ts | 41 +- 03-frontend/eslint.config.mjs | 8 +- 03-frontend/lib/adapter-source-registry.ts | 49 +- 03-frontend/lib/ai-provider-router.ts | 18 +- 03-frontend/lib/api.ts | 168 + 03-frontend/lib/business-workflow.test.ts | 440 +- 03-frontend/lib/cad-derivative-server.test.ts | 23 +- 03-frontend/lib/cad-derivative-server.ts | 308 +- 03-frontend/lib/digital-twin.test.ts | 91 + 03-frontend/lib/digital-twin.ts | 465 +- 03-frontend/lib/file-type-registry.test.ts | 20 +- 03-frontend/lib/file-type-registry.ts | 55 +- 03-frontend/lib/ifc-derivative-server.test.ts | 83 +- 03-frontend/lib/ifc-derivative-server.ts | 674 ++- .../lib/insome/scene/camera/controls.tsx | 2 +- .../scene/components/camera-transition.tsx | 38 +- 03-frontend/lib/llm-provider.ts | 3 +- 03-frontend/lib/module-backend-adapter.ts | 13 +- 03-frontend/lib/module-file-system.ts | 741 +-- 03-frontend/lib/module-operations.ts | 1808 +++++- 03-frontend/lib/module-registry.ts | 3054 +++++++--- 03-frontend/lib/openclaw-workbench-chat.ts | 203 + .../lib/project-planning-studio.test.ts | 87 +- 03-frontend/lib/project-planning-studio.ts | 1285 ++++- 03-frontend/lib/skp-derivative-server.test.ts | 70 + 03-frontend/lib/skp-derivative-server.ts | 460 ++ 03-frontend/package.json | 4 + .../public/assets/projects-photo/alpine.svg | 26 + .../public/assets/projects-photo/camp.svg | 30 + .../public/assets/projects-photo/interior.svg | 23 + .../public/assets/projects-photo/resort.svg | 26 + .../public/assets/projects-photo/ryokan.svg | 31 + .../assets/projects-photo/villa-pool.svg | 28 + 03-frontend/public/file.svg | 1 + 03-frontend/public/globe.svg | 1 + 03-frontend/public/next.svg | 1 + 03-frontend/public/vercel.svg | 1 + .../public/wasm/ifc-lite/ifc-lite_bg.wasm | Bin 0 -> 1149830 bytes 03-frontend/public/wasm/rhino3dm/rhino3dm.js | 21 + .../public/wasm/rhino3dm/rhino3dm.module.js | 16 + .../public/wasm/rhino3dm/rhino3dm.wasm | Bin 0 -> 2545991 bytes 03-frontend/public/window.svg | 1 + 03-frontend/tsconfig.json | 3 +- 04-backend/Cargo.lock | 1 + 04-backend/Cargo.toml | 1 + .../production_manufacturing/evaluator.md | 4 +- .../production_manufacturing/generator.md | 9 + .../production_manufacturing/planner.md | 3 + 04-backend/harness-core/Cargo.toml | 1 + .../harness-core/src/ai_center_management.rs | 167 + 04-backend/harness-core/src/bin/gateway.rs | 856 ++- 04-backend/harness-core/src/config.rs | 96 +- .../harness-core/src/file_runtime_registry.rs | 136 +- .../harness-core/src/generation_engine.rs | 95 + 04-backend/harness-core/src/lib.rs | 2 + .../harness-core/src/module_generation.rs | 820 ++- .../harness-core/src/module_registry.rs | 2 +- .../harness-core/src/runtime_capabilities.rs | 139 +- .../harness-core/src/semantic_dictionary.rs | 181 + .../20260419000001_initial_schema.sql | 2 +- ...0001_project_planning_progress_control.sql | 81 + ...60521000002_sjg157_semantic_dictionary.sql | 388 ++ .../20260521000003_ai_center_management.sql | 125 + 04-backend/openapi.yaml | 439 +- .../import-sjg157-semantic-dictionary.py | 453 ++ .../shared/src/modules/planning_management.rs | 1 + .../src/modules/production_manufacturing.rs | 3 +- 05-infra/docker/docker-compose.yml | 36 +- .../architoken_workers/blender_worker.py | 28 +- docker-compose.production.yml | 38 + docs/01_ARCHITOKEN_REQUIREMENTS_FULL.md | 1 + docs/03_ARCHITOKEN_TECH_STACK.md | 11 +- docs/18_PHASE7_CAD_PIPELINE.md | 6 +- docs/ADAPTER_SOURCE_MAP.md | 10 +- docs/ARCHITOKEN_PLATFORM_FUNCTIONAL_MAP.md | 2 +- ...KEN_PROJECT_PROGRESS_PAPER_2026-05-21.html | 1249 ++++ ...EN_PROJECT_PROGRESS_REPORT_2026-05-21.html | 577 ++ docs/FILE_TYPE_REGISTRY.md | 4 +- docs/OPENBIM_STANDARD_BASELINE.md | 11 +- .../SJG157_SEMANTIC_DICTIONARY_INTEGRATION.md | 38 + output.html | 1130 ++++ tools/production_readiness_contract.py | 9 +- tools/test_production_readiness_contract.py | 5 + 184 files changed, 37249 insertions(+), 3821 deletions(-) create mode 100644 .tmp-paperclip-direct-flow.png create mode 100644 .tmp-paperclip-flow.png create mode 100644 03-frontend/app/api/ai/openclaw/chat/route.ts create mode 100644 03-frontend/app/api/local-files/[fileId]/skp-derivative/route.ts create mode 100644 03-frontend/app/api/openclaw/ui/[[...path]]/route.ts create mode 100644 03-frontend/app/api/paperclip/ui/[[...path]]/route.ts create mode 100644 03-frontend/components/AICenterManagementPanels.tsx create mode 100644 03-frontend/components/ConceptDesignStudioWorkbench.tsx create mode 100644 03-frontend/components/DetailedDesignPlanFinderWorkbench.tsx create mode 100644 03-frontend/components/DigitalTwinWebGPUViewport.tsx create mode 100644 03-frontend/components/FeichuanPlanningWorkbench.tsx create mode 100644 03-frontend/components/PaperclipProductionWorkbench.tsx create mode 100644 03-frontend/components/StandardLibrarySemanticDictionaryPanel.tsx create mode 100644 03-frontend/components/planning/feichuan-gantt/LICENSE create mode 100644 03-frontend/components/planning/feichuan-gantt/README.md create mode 100644 03-frontend/components/planning/feichuan-gantt/src/assets/images/drag.png create mode 100644 03-frontend/components/planning/feichuan-gantt/src/assets/images/folder.png create mode 100644 03-frontend/components/planning/feichuan-gantt/src/component/drag-tree-table/column.scss create mode 100644 03-frontend/components/planning/feichuan-gantt/src/component/drag-tree-table/column.tsx create mode 100644 03-frontend/components/planning/feichuan-gantt/src/component/drag-tree-table/func.ts create mode 100644 03-frontend/components/planning/feichuan-gantt/src/component/drag-tree-table/row.scss create mode 100644 03-frontend/components/planning/feichuan-gantt/src/component/drag-tree-table/row.tsx create mode 100644 03-frontend/components/planning/feichuan-gantt/src/component/drag-tree-table/treetable.scss create mode 100644 03-frontend/components/planning/feichuan-gantt/src/component/gantt-task/canlendar/calendar.scss create mode 100644 03-frontend/components/planning/feichuan-gantt/src/component/gantt-task/canlendar/canlendar.tsx create mode 100644 03-frontend/components/planning/feichuan-gantt/src/component/gantt-task/gantt/critical-path create mode 100644 03-frontend/components/planning/feichuan-gantt/src/component/gantt-task/gantt/gantt-demo-bigdata.tsx create mode 100644 03-frontend/components/planning/feichuan-gantt/src/component/gantt-task/gantt/gantt-demo-viewer.tsx create mode 100644 03-frontend/components/planning/feichuan-gantt/src/component/gantt-task/gantt/gantt-demo.scss create mode 100644 03-frontend/components/planning/feichuan-gantt/src/component/gantt-task/gantt/gantt-demo.tsx create mode 100644 03-frontend/components/planning/feichuan-gantt/src/component/gantt-task/gantt/gantt-task.scss create mode 100644 03-frontend/components/planning/feichuan-gantt/src/component/gantt-task/gantt/network-gantt.tsx create mode 100644 03-frontend/components/planning/feichuan-gantt/src/component/gantt-task/gantt/network.tsx create mode 100644 03-frontend/components/planning/feichuan-gantt/src/component/gantt-task/helpers/bar-helper.ts create mode 100644 03-frontend/components/planning/feichuan-gantt/src/component/gantt-task/helpers/date-helper.ts create mode 100644 03-frontend/components/planning/feichuan-gantt/src/component/gantt-task/task-bar/linkLine.tsx create mode 100644 03-frontend/components/planning/feichuan-gantt/src/component/gantt-task/task-bar/taskBar.tsx create mode 100644 03-frontend/components/planning/feichuan-gantt/src/component/gantt-task/time-scaled/ArrowCircle.tsx create mode 100644 03-frontend/components/planning/feichuan-gantt/src/component/gantt-task/time-scaled/ArrowLine.tsx create mode 100644 03-frontend/components/planning/feichuan-gantt/src/component/gantt-task/time-scaled/DashedLine.tsx create mode 100644 03-frontend/components/planning/feichuan-gantt/src/component/gantt-task/time-scaled/PertArrow.tsx create mode 100644 03-frontend/components/planning/feichuan-gantt/src/component/gantt-task/time-scaled/PertBlock.tsx create mode 100644 03-frontend/components/planning/feichuan-gantt/src/component/gantt-task/time-scaled/Scaled.tsx create mode 100644 03-frontend/components/planning/feichuan-gantt/src/component/gantt-task/time-scaled/ScaledArrow.tsx create mode 100644 03-frontend/components/planning/feichuan-gantt/src/component/template/class-tsx.tsx create mode 100644 03-frontend/components/planning/feichuan-gantt/src/redux/networkGantt/ganttReducer.ts create mode 100644 03-frontend/components/planning/feichuan-gantt/src/redux/store.ts create mode 100644 03-frontend/components/planning/feichuan-gantt/src/types/bar-task.ts create mode 100644 03-frontend/components/planning/feichuan-gantt/src/types/date-setup.ts create mode 100644 03-frontend/components/planning/feichuan-gantt/src/types/global.d.ts create mode 100644 03-frontend/components/planning/feichuan-gantt/src/types/public-types.ts create mode 100644 03-frontend/lib/openclaw-workbench-chat.ts create mode 100644 03-frontend/lib/skp-derivative-server.test.ts create mode 100644 03-frontend/lib/skp-derivative-server.ts create mode 100644 03-frontend/public/assets/projects-photo/alpine.svg create mode 100644 03-frontend/public/assets/projects-photo/camp.svg create mode 100644 03-frontend/public/assets/projects-photo/interior.svg create mode 100644 03-frontend/public/assets/projects-photo/resort.svg create mode 100644 03-frontend/public/assets/projects-photo/ryokan.svg create mode 100644 03-frontend/public/assets/projects-photo/villa-pool.svg create mode 100644 03-frontend/public/file.svg create mode 100644 03-frontend/public/globe.svg create mode 100644 03-frontend/public/next.svg create mode 100644 03-frontend/public/vercel.svg create mode 100644 03-frontend/public/wasm/ifc-lite/ifc-lite_bg.wasm create mode 100644 03-frontend/public/wasm/rhino3dm/rhino3dm.js create mode 100644 03-frontend/public/wasm/rhino3dm/rhino3dm.module.js create mode 100644 03-frontend/public/wasm/rhino3dm/rhino3dm.wasm create mode 100644 03-frontend/public/window.svg create mode 100644 04-backend/harness-core/src/ai_center_management.rs create mode 100644 04-backend/harness-core/src/semantic_dictionary.rs create mode 100644 04-backend/migrations/20260521000001_project_planning_progress_control.sql create mode 100644 04-backend/migrations/20260521000002_sjg157_semantic_dictionary.sql create mode 100644 04-backend/migrations/20260521000003_ai_center_management.sql create mode 100644 04-backend/scripts/import-sjg157-semantic-dictionary.py create mode 100644 docs/ARCHITOKEN_PROJECT_PROGRESS_PAPER_2026-05-21.html create mode 100644 docs/ARCHITOKEN_PROJECT_PROGRESS_REPORT_2026-05-21.html create mode 100644 docs/SJG157_SEMANTIC_DICTIONARY_INTEGRATION.md create mode 100644 output.html diff --git a/.env.production.example b/.env.production.example index 5b16f60f..f2504765 100644 --- a/.env.production.example +++ b/.env.production.example @@ -48,6 +48,20 @@ ARCHITOKEN_GENERATION__PROVIDER=http_text_to_bim ARCHITOKEN_GENERATION__TEXT_TO_BIM_URL=https://generation.example.com/v1/generate/text-to-bim # File adapter services. Empty values are not production-ready for that family. +PAPERCLIP_DASHBOARD_URL=http://paperclip:3111 +NEXT_PUBLIC_PAPERCLIP_PUBLIC_URL=https://paperclip.architoken.example.com +PAPERCLIP_INSTANCE_ID=architoken-production-manufacturing +PAPERCLIP_CONFIG=/paperclip/instances/architoken-production-manufacturing/config.json +PAPERCLIP_PUBLIC_URL=https://paperclip.architoken.example.com +PAPERCLIP_AUTH_PUBLIC_BASE_URL=https://paperclip.architoken.example.com +PAPERCLIP_ALLOWED_HOSTNAMES=paperclip.architoken.example.com,architoken.example.com +BETTER_AUTH_BASE_URL=https://paperclip.architoken.example.com +BETTER_AUTH_URL=https://paperclip.architoken.example.com +BETTER_AUTH_TRUSTED_ORIGINS=https://paperclip.architoken.example.com,https://architoken.example.com +PAPERCLIP_DEPLOYMENT_MODE=authenticated +PAPERCLIP_DEPLOYMENT_EXPOSURE=private +PAPERCLIP_BETTER_AUTH_SECRET=REPLACE_WITH_32_BYTES_RANDOM_SECRET +PAPERCLIP_AGENT_JWT_SECRET=REPLACE_WITH_32_BYTES_RANDOM_SECRET LIBREOFFICE_ADAPTER_URL=https://office-adapter.example.com UNIVER_DOCUMENT_SERVICE_URL=https://univer.example.com MARKITDOWN_WORKER_URL=http://markitdown-worker:8080 diff --git a/.tmp-paperclip-direct-flow.png b/.tmp-paperclip-direct-flow.png new file mode 100644 index 0000000000000000000000000000000000000000..bdaa2cb0f90150726b522232e07298f761b44b06 GIT binary patch literal 138931 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 literal 0 HcmV?d00001 diff --git a/.tmp-paperclip-flow.png b/.tmp-paperclip-flow.png new file mode 100644 index 0000000000000000000000000000000000000000..4ba982ecf53332125643265e94126a59ccb05bb9 GIT binary patch literal 127733 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
  • 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 W&na9brR4AgaPyYuF=AVoJ literal 0 HcmV?d00001 diff --git a/01-product/PRD.md b/01-product/PRD.md index e56b2032..033eccbd 100644 --- a/01-product/PRD.md +++ b/01-product/PRD.md @@ -93,6 +93,8 @@ ArchIToken 不是 Revit、Tekla、PKPM、广联达、中望、Siemens Building X | 13 | `ai_center` | AI中心 | 模型 / RAG / MCP / Agent 配置 | 模型路由 + 工具权限 + 成本审计策略 | 实时 | | 14 | `settings_center` *(side-car)* | 设置中心 | 租户 / RBAC / 模型路由 / 预算配置 | 全局配置推送 (被其它 13 模块拉取) | 实时 | +当前阶段,Paperclip v2026.517.0 完整接管 `production_manufacturing` 模块主工作区,用于 Agent 组织、工厂任务、heartbeat、预算和治理编排;不得替代 ArchIToken 的模块 ID、CDE 文件、CNC/QC/MES/ERP 真源或专业审批结论。 + **架构承诺**: 未来新增模块(例如"拿地分析 / 方案投标 / 碳排放核算")不需改任何已有代码 —— 只在 `modules` 表 + Rust `REGISTRY` + Python `MODULE_REGISTRY` 各加一行,加配 3 个 prompt 文件即可。 diff --git a/02-architecture/ARCHITOKEN-SOURCE-OF-TRUTH.md b/02-architecture/ARCHITOKEN-SOURCE-OF-TRUTH.md index d744bc89..0ac1a254 100644 --- a/02-architecture/ARCHITOKEN-SOURCE-OF-TRUTH.md +++ b/02-architecture/ARCHITOKEN-SOURCE-OF-TRUTH.md @@ -179,6 +179,8 @@ K3s 可作为极端资源受限场景的可选适配,但不是正式部署基线 `production_manufacturing` 是生产制造模块,包括生产计划、工序路线、下料优化、CNC / 数控文件、焊接、喷涂 / 防腐 / 防火、质检、工厂排产、MES / ERP 对接、构件编码和包装发运。 +当前阶段 Paperclip v2026.517.0 完整接管 `production_manufacturing` 模块主工作区,作为 Agent 组织、工厂任务、heartbeat、预算和治理编排的外部进程 / 服务适配器;不得替代 ArchIToken 的模块 ID、CDE 文件、CNC/QC/MES/ERP 真源或专业审批结论。 + ### 4.4 施工管理 施工管理包括方案、进度、质量、安全、日志、AR、360 全景、三维扫描、倾斜摄影、无人机巡检、建筑机器人、IoT、影像对比、整改闭环和竣工资料。 diff --git a/02-architecture/BUSINESS_MODULE_WORKBENCH.md b/02-architecture/BUSINESS_MODULE_WORKBENCH.md index f929a944..493483e8 100644 --- a/02-architecture/BUSINESS_MODULE_WORKBENCH.md +++ b/02-architecture/BUSINESS_MODULE_WORKBENCH.md @@ -37,7 +37,7 @@ - 右侧上下文审计、审批、生命周期和 AI 建议默认是抽屉,按需打开,不得常驻占位。 - 大屏不得被窄 `container` 或过小 `max-width` 限制。 - 窄屏时模块导航变为横向滚动,主功能区优先展示,审计面板自然下沉。 -- 全局浮动 `ArchIToken AI` 默认贴边折叠,可展开、停靠并打开聊天抽屉;移动端表现为底部抽屉式卡片。 +- 全局浮动 `OpenClaw` 默认贴边折叠,打开后显示 OpenClaw Control 原生工作台;移动端表现为自适应浮窗。 - 文件/审批/审计右侧面板可折叠,不得遮挡主业务区。 - 主题和字号是平台能力,不是模块硬编码。默认主题是 `huly_light`;内置 `huly_dark`、`huly_system`、`huly_spacious` 和 `huly_compact` 可切换,旧 `wechat_light`、`industrial_dark` 仅作为迁移别名读取。 - 前端设计系统统一切换为 Ant Design 生态体系。新增 UI 必须优先使用 `antd`、`@ant-design/icons`、`@ant-design/pro-components`、`@ant-design/charts`、`@ant-design/x` 或基于 Ant Design token 的封装,不得再新增第二套按钮、表格、表单、抽屉、弹窗、图标、图表或 AI 对话组件体系。 @@ -50,61 +50,61 @@ ## 2. Active Module IDs -| order | id | 中文名 | 入口 | -|---:|---|---|---| -| 1 | `marketing_service` | 市场客服 | `/app/modules/marketing_service` | -| 2 | `planning_management` | 计划管理 | `/app/modules/planning_management` | -| 3 | `concept_design` | 方案设计 | `/app/modules/concept_design` | -| 4 | `standard_library` | 标准族库 | `/app/modules/standard_library` | -| 5 | `detailed_design` | 深化设计 | `/app/modules/detailed_design` | -| 6 | `quantity_costing` | 计量造价 | `/app/modules/quantity_costing` | -| 7 | `material_logistics` | 材料物流 | `/app/modules/material_logistics` | -| 8 | `production_manufacturing` | 生产制造 | `/app/modules/production_manufacturing` | -| 9 | `construction_management` | 施工管理 | `/app/modules/construction_management` | -| 10 | `digital_twin` | 数字孪生 | `/app/modules/digital_twin` | -| 11 | `digital_archive` | 数字档案 | `/app/modules/digital_archive` | -| 12 | `finance_hr` | 财务人力 | `/app/modules/finance_hr` | -| 13 | `ai_center` | AI中心 | `/app/modules/ai_center` | -| 14 | `settings_center` | 设置中心 | `/app/modules/settings_center` | +| order | id | 中文名 | 入口 | +| ----: | -------------------------- | -------- | --------------------------------------- | +| 1 | `marketing_service` | 市场客服 | `/app/modules/marketing_service` | +| 2 | `planning_management` | 计划管理 | `/app/modules/planning_management` | +| 3 | `concept_design` | 方案设计 | `/app/modules/concept_design` | +| 4 | `standard_library` | 标准族库 | `/app/modules/standard_library` | +| 5 | `detailed_design` | 深化设计 | `/app/modules/detailed_design` | +| 6 | `quantity_costing` | 计量造价 | `/app/modules/quantity_costing` | +| 7 | `material_logistics` | 材料物流 | `/app/modules/material_logistics` | +| 8 | `production_manufacturing` | 生产制造 | `/app/modules/production_manufacturing` | +| 9 | `construction_management` | 施工管理 | `/app/modules/construction_management` | +| 10 | `digital_twin` | 数字孪生 | `/app/modules/digital_twin` | +| 11 | `digital_archive` | 数字档案 | `/app/modules/digital_archive` | +| 12 | `finance_hr` | 财务人力 | `/app/modules/finance_hr` | +| 13 | `ai_center` | AI中心 | `/app/modules/ai_center` | +| 14 | `settings_center` | 设置中心 | `/app/modules/settings_center` | --- ## 3. 前端实现映射 -| 文件 | 职责 | -|---|---| -| `03-frontend/lib/module-registry.ts` | Module Schema fixture,定义 `ModuleSpec`、`SubdomainSpec`、`ArtifactSpec`、`WorkflowStep`、`AgentGate`、`ModuleAction` 并导出 14 模块 registry | -| `03-frontend/lib/module-actions.ts` | session action handlers: `generateArtifact`、`evaluateArtifact`、`runRuleCheck`、`validateSchema`、`approveArtifact`、`archiveArtifact` | -| `03-frontend/lib/business-workflow.ts` | 前端 runtime state 与 action 应用辅助函数 | -| `03-frontend/lib/module-operations.ts` | 14 模块专属业务功能卡片、模块操作按钮和状态轨道 | -| `03-frontend/lib/module-file-system.ts` | 14 模块 typed session file tree、文件节点、权限、审计轨迹、下载任务和分享链接 | -| `03-frontend/lib/module-lifecycle.ts` | `ModuleTransaction`、审批结构、状态机事件和状态迁移规则 | -| `03-frontend/lib/module-backend-adapter.ts` | `ModuleBackendAdapter` 合同与 `SessionModuleBackendAdapter`,所有文件/事务操作先经 adapter | -| `03-frontend/lib/design-system-registry.ts` | Ant Design 生态运行包、参考包、许可证和后续开发规则 | -| `03-frontend/lib/theme-registry.ts` | `huly_light`、`huly_dark`、`huly_system` 主题注册、旧主题迁移与 `architoken_theme` 存储键 | -| `03-frontend/lib/font-registry.ts` | `huly_spacious`、`huly_compact` 字号注册与 `architoken_font` 存储键 | -| `03-frontend/lib/ant-design-theme.ts` | ArchIToken 主题到 Ant Design token / `ConfigProvider` 的映射 | -| `03-frontend/lib/ai-assistant-profile.ts` | 全局浮动 AI 助手 profile、作品、能力标签和模块上下文建议 | -| `03-frontend/components/ThemeProvider.tsx` | 全局 `data-theme`、CSS variables、Ant Design `ConfigProvider` 与中文 locale provider | -| `03-frontend/components/ThemeSwitcher.tsx` | 顶部工具栏主题切换器 | -| `03-frontend/components/ModuleWorkbenchShell.tsx` | 总平台壳: 左侧模块导航、顶部搜索、主详情、右侧审计面板 | -| `03-frontend/components/ModuleDetailWorkbench.tsx` | 单模块详情页主体 | -| `03-frontend/components/ModuleOperationalPanel.tsx` | 模块专属功能面板:功能卡片、状态切换、专属业务交互 | -| `03-frontend/components/ModuleFileExplorer.tsx` | 模块文件/文件夹业务系统: 对象树、列表、右键菜单、预览、属性、下载/分享任务 | -| `03-frontend/components/FileContextMenu.tsx` | 文件/文件夹右键菜单 | -| `03-frontend/components/FilePreviewDrawer.tsx` | 文件/文件夹预览抽屉和完整查看模式 | -| `03-frontend/components/FilePropertiesPanel.tsx` | 文件属性、权限、标签、分享链接和审计轨迹 | -| `03-frontend/components/FileOperationDialog.tsx` | 新建、上传、移动、分享、删除、重命名等操作弹窗 | -| `03-frontend/components/LifecycleTransactionPanel.tsx` | 生命周期事务列表、创建事务和状态迁移按钮 | -| `03-frontend/components/ApprovalWorkflowPanel.tsx` | 审批人、审批状态、意见、通过/驳回/退回修改 | -| `03-frontend/components/StateMachinePanel.tsx` | 状态机当前状态与后续可触发事件 | -| `03-frontend/components/AgentGateTimeline.tsx` | Planner → Generator → Evaluator → RuleChecker → SchemaValidator → Approver | -| `03-frontend/components/ArtifactBoard.tsx` | 交付物列表和可点击操作按钮 | -| `03-frontend/components/ModuleRelationshipMap.tsx` | 上下游模块关系 | -| `03-frontend/components/FloatingAIAssistant.tsx` | 右下角全局 AI 客服 / AI 助手 | -| `03-frontend/app/app/modules/page.tsx` | 平台总入口 | -| `03-frontend/app/app/modules/[moduleId]/page.tsx` | 动态模块详情路由 | -| `03-frontend/components/BusinessModuleWorkbench.tsx` | 保留兼容入口,转接到新 workbench | +| 文件 | 职责 | +| ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- | +| `03-frontend/lib/module-registry.ts` | Module Schema fixture,定义 `ModuleSpec`、`SubdomainSpec`、`ArtifactSpec`、`WorkflowStep`、`AgentGate`、`ModuleAction` 并导出 14 模块 registry | +| `03-frontend/lib/module-actions.ts` | session action handlers: `generateArtifact`、`evaluateArtifact`、`runRuleCheck`、`validateSchema`、`approveArtifact`、`archiveArtifact` | +| `03-frontend/lib/business-workflow.ts` | 前端 runtime state 与 action 应用辅助函数 | +| `03-frontend/lib/module-operations.ts` | 14 模块专属业务功能卡片、模块操作按钮和状态轨道 | +| `03-frontend/lib/module-file-system.ts` | 14 模块 typed session file tree、文件节点、权限、审计轨迹、下载任务和分享链接 | +| `03-frontend/lib/module-lifecycle.ts` | `ModuleTransaction`、审批结构、状态机事件和状态迁移规则 | +| `03-frontend/lib/module-backend-adapter.ts` | `ModuleBackendAdapter` 合同与 `SessionModuleBackendAdapter`,所有文件/事务操作先经 adapter | +| `03-frontend/lib/design-system-registry.ts` | Ant Design 生态运行包、参考包、许可证和后续开发规则 | +| `03-frontend/lib/theme-registry.ts` | `huly_light`、`huly_dark`、`huly_system` 主题注册、旧主题迁移与 `architoken_theme` 存储键 | +| `03-frontend/lib/font-registry.ts` | `huly_spacious`、`huly_compact` 字号注册与 `architoken_font` 存储键 | +| `03-frontend/lib/ant-design-theme.ts` | ArchIToken 主题到 Ant Design token / `ConfigProvider` 的映射 | +| `03-frontend/lib/ai-assistant-profile.ts` | 全局浮动 AI 助手 profile、作品、能力标签和模块上下文建议 | +| `03-frontend/components/ThemeProvider.tsx` | 全局 `data-theme`、CSS variables、Ant Design `ConfigProvider` 与中文 locale provider | +| `03-frontend/components/ThemeSwitcher.tsx` | 顶部工具栏主题切换器 | +| `03-frontend/components/ModuleWorkbenchShell.tsx` | 总平台壳: 左侧模块导航、顶部搜索、主详情、右侧审计面板 | +| `03-frontend/components/ModuleDetailWorkbench.tsx` | 单模块详情页主体 | +| `03-frontend/components/ModuleOperationalPanel.tsx` | 模块专属功能面板:功能卡片、状态切换、专属业务交互 | +| `03-frontend/components/ModuleFileExplorer.tsx` | 模块文件/文件夹业务系统: 对象树、列表、右键菜单、预览、属性、下载/分享任务 | +| `03-frontend/components/FileContextMenu.tsx` | 文件/文件夹右键菜单 | +| `03-frontend/components/FilePreviewDrawer.tsx` | 文件/文件夹预览抽屉和完整查看模式 | +| `03-frontend/components/FilePropertiesPanel.tsx` | 文件属性、权限、标签、分享链接和审计轨迹 | +| `03-frontend/components/FileOperationDialog.tsx` | 新建、上传、移动、分享、删除、重命名等操作弹窗 | +| `03-frontend/components/LifecycleTransactionPanel.tsx` | 生命周期事务列表、创建事务和状态迁移按钮 | +| `03-frontend/components/ApprovalWorkflowPanel.tsx` | 审批人、审批状态、意见、通过/驳回/退回修改 | +| `03-frontend/components/StateMachinePanel.tsx` | 状态机当前状态与后续可触发事件 | +| `03-frontend/components/AgentGateTimeline.tsx` | Planner → Generator → Evaluator → RuleChecker → SchemaValidator → Approver | +| `03-frontend/components/ArtifactBoard.tsx` | 交付物列表和可点击操作按钮 | +| `03-frontend/components/ModuleRelationshipMap.tsx` | 上下游模块关系 | +| `03-frontend/components/FloatingAIAssistant.tsx` | 右下角全局 AI 客服 / AI 助手 | +| `03-frontend/app/app/modules/page.tsx` | 平台总入口 | +| `03-frontend/app/app/modules/[moduleId]/page.tsx` | 动态模块详情路由 | +| `03-frontend/components/BusinessModuleWorkbench.tsx` | 保留兼容入口,转接到新 workbench | --- @@ -140,14 +140,14 @@ ## 5. 操作按钮语义 -| 按钮 | session handler | 状态变化 | -|---|---|---| -| 生成 | `generateArtifact` | `draft` → `generated` | -| 评估 | `evaluateArtifact` | artifact → `evaluated` | -| 校核 | `runRuleCheck` | artifact → `rule_checked` | -| Schema | `validateSchema` | artifact → `schema_validated` | -| 审批 | `approveArtifact` | artifact → `approved` | -| 归档 | `archiveArtifact` | artifact → `archived` | +| 按钮 | session handler | 状态变化 | +| ------ | ------------------ | ----------------------------- | +| 生成 | `generateArtifact` | `draft` → `generated` | +| 评估 | `evaluateArtifact` | artifact → `evaluated` | +| 校核 | `runRuleCheck` | artifact → `rule_checked` | +| Schema | `validateSchema` | artifact → `schema_validated` | +| 审批 | `approveArtifact` | artifact → `approved` | +| 归档 | `archiveArtifact` | artifact → `archived` | 每次 action 必须返回: @@ -164,12 +164,13 @@ 除交付物生命周期按钮外,每个模块还必须通过 `module-operations.ts` 提供至少 3 个业务操作。当前前端已覆盖: - `marketing_service`: 生成需求摘要、生成报价草案、创建跟进任务。 +- `planning_management`: 在线编制进度计划、拆解 WBS/任务、登记进度反馈、分析图表、生成预警、调整计划窗口和更新任务状态。 - `concept_design`: 生成方案、评估规范、生成展示包。 - `standard_library`: 检索规范、生成族库、校核构件、发布版本。 -- `detailed_design`: 生成深化模型、生成图纸、运行碰撞检查。 +- `detailed_design`: 生成深化模型、生成图纸、运行碰撞检查、生成/适配平面候选、快速布置家具。 - `quantity_costing`: 生成 BOQ、生成造价、评估变更影响。 - `material_logistics`: 生成采购计划、生成下料单、安排物流、签收批次。 -- `production_manufacturing`: 生成工单、生成 CNC 文件、运行质检、安排发运。 +- `production_manufacturing`: 由 Paperclip v2026.517.0 接管主工作区,生成工单、生成 CNC 文件、运行质检、安排发运并同步模块内编排面板。 - `construction_management`: 生成施工日志、创建整改单、运行安全检查、归档竣工资料。 - `digital_twin`: 切换图层、选择构件、播放进度、生成孪生快照、导出模型包。 - `digital_archive`: 生成归档包、校验完整性、导出档案。 @@ -208,20 +209,20 @@ 右键菜单必须覆盖 12 个操作: -| action | 前端状态变化 | -|---|---| -| 打开 | 文件夹进入目录;文件打开预览 | -| 新建 | 在当前目录新增文件夹或文件节点 | -| 查看 | 打开预览抽屉或完整查看模式 | -| 上传 | 新增上传文件,状态为 `uploaded` | -| 下载 | 写入 audit event 并生成下载任务状态 | -| 移动 | 选择目标文件夹后更新 `parentId` | -| 复制 | 写入 clipboard state | -| 粘贴 | 在当前目录创建副本 | -| 分享 | 生成分享链接并打开分享结果 | -| 删除 | 标记为 `soft_deleted`,不直接物理删除 | -| 属性 | 打开属性面板 | -| 重命名 | 更新 `name`、版本和审计轨迹 | +| action | 前端状态变化 | +| ------ | ------------------------------------ | +| 打开 | 文件夹进入目录;文件打开预览 | +| 新建 | 在当前目录新增文件夹或文件节点 | +| 查看 | 打开预览抽屉或完整查看模式 | +| 上传 | 新增上传文件,状态为 `uploaded` | +| 下载 | 写入 audit event 并生成下载任务状态 | +| 移动 | 选择目标文件夹后更新 `parentId` | +| 复制 | 写入 clipboard state | +| 粘贴 | 在当前目录创建副本 | +| 分享 | 生成分享链接并打开分享结果 | +| 删除 | 标记为 `soft_deleted`,不直接物理删除 | +| 属性 | 打开属性面板 | +| 重命名 | 更新 `name`、版本和审计轨迹 | 所有文件操作必须通过 `ModuleBackendAdapter`,不得绕过 adapter 直接散落 `setState`。 @@ -247,26 +248,26 @@ 格式命令集合: -| 格式族 | 默认命令 | -|---|---| -| Office/PDF | 打开、下载、编辑、保存版本、导入、导出、分享、属性、字号、加粗、序号、复制、剪切、粘贴、对齐、排序、格式、协作 | -| Text/JSON/YAML/XML/HTML/MD/代码 | 打开、下载、搜索、编辑、保存版本、复制、行号、语法/结构化预览 | -| Archive/ZIP/IFCZIP/BCFZIP | 打开条目、搜索、筛选、下载源包、下载条目、哈希校验、风险路径/加密状态 | -| CAD/DWG/DXF | 选择、位置、坐标、图层、实体/构件、编辑、云线批注、查找、属性、视点、移动、缩放、场景、漫游、测量、导出 | -| BIM/IFC/glTF/STEP/IGES/STL/OBJ/FBX/USD/点云 | 选择构件、构件树、属性、坐标、图层、视点、显隐/隔离、移动/变换、测量、剖切、漫游、场景、BOM 导出、轻量化 derivative 状态 | +| 格式族 | 默认命令 | +| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | +| Office/PDF | 打开、下载、编辑、保存版本、导入、导出、分享、属性、字号、加粗、序号、复制、剪切、粘贴、对齐、排序、格式、协作 | +| Text/JSON/YAML/XML/HTML/MD/代码 | 打开、下载、搜索、编辑、保存版本、复制、行号、语法/结构化预览 | +| Archive/ZIP/IFCZIP/BCFZIP | 打开条目、搜索、筛选、下载源包、下载条目、哈希校验、风险路径/加密状态 | +| CAD/DWG/DXF | 选择、位置、坐标、图层、实体/构件、编辑、云线批注、查找、属性、视点、移动、缩放、场景、漫游、测量、导出 | +| BIM/IFC/OpenUSD/USDZ/3D Tiles/glTF fallback/STEP/IGES/STL/点云 | 选择构件、构件树、属性、坐标、图层、视点、显隐/隔离、移动/变换、测量、剖切、漫游、场景、BOM 导出、轻量化 derivative 状态 | 这些命令最终必须映射到 `ViewerAdapter` / `ModuleBackendAdapter` / 后端 worker manifest。前端可以先提供真实查看和命令入口,但生产编辑必须写入版本、审计、权限和审批状态。 Native/source viewer rule: - 后端必须优先打开源文件或源文件派生的实体级轻量格式。DWG 优先 ODA/LibreDWG/dwg2dxf/DWG-to-DXF adapter,DXF 优先实体级 CAD 画布,IFC 优先 openBIM worker 轻量化,STEP/STP/IGES/IGS/STL 优先 OCCT/OCP/FreeCAD/mesh worker。 -- PDF、图片、SVG、GLB、tiles 或 vector PDF 只能作为显式导出、缓存、降级或授权适配器结果,不能冒充源格式语义。 +- PDF、图片、SVG、OpenUSD/USDZ、3D Tiles、GLB 兜底或 vector PDF 只能作为显式导出、缓存、降级或授权适配器结果,不能冒充源格式语义。 - 缺失 ODA、LibreDWG、IfcOpenShell、OCCT、FreeCAD 或类似依赖时,优先从官方 GitHub 源码编译为 sidecar;apt/snap 不可用不是停止条件。 - viewer manifest 必须暴露 source id、checksum、adapter、engine、derivative artifact、cache hit、ETag 和权限边界,使前端能区分“源格式查看”“实体派生查看”“只读降级查看”。 IFC lightweight rule: -- 首次上传 IFC 后,worker 必须生成或排队生成 GLB、fragments/tiles、properties index 和 derivative manifest。 +- 首次上传 IFC 后,worker 必须生成或排队生成 OpenUSD/USDZ、3D Tiles、GLB 兜底、fragments、properties index 和 derivative manifest。 - 前端只加载轻量几何和分页属性;不得每次打开都在浏览器完整解析原始 IFC。 - API 必须支持 stream、Range、ETag/cache 和 checksum 匹配,避免重复整文件读入内存。 @@ -339,6 +340,8 @@ request_approval, approve, reject, archive, reopen, block, resolve_blocker 必须覆盖生产计划、工序路线、下料优化、CNC/数控文件、焊接、喷涂/防腐/防火、质检、工厂排产、MES/ERP 对接、构件编码、包装发运、返工处理。 +当前阶段允许 Paperclip v2026.517.0 完整接管 `production_manufacturing` 模块主工作区,作为 Agent 组织、issue、heartbeat、预算、运行日志和治理审批的外部进程 / 服务适配器。Paperclip 负责生产制造操作界面、任务编排和运行证据记录,但不得替代 CDE 文件系统、CNC 源文件、质检记录、MES/ERP 数据、AI 门禁链或生产负责人 / 专业责任人的审批结论。 + ### 6.4 `construction_management` 必须覆盖施工方案、进度、质量、安全、日志、AR、360 全景、三维扫描、倾斜摄影、无人机、建筑机器人、IoT、影像对比、整改闭环、竣工资料。 @@ -359,17 +362,20 @@ request_approval, approve, reject, archive, reopen, block, resolve_blocker --- -## 6.6 全局 AI 助手 +## 6.6 OpenClaw 原生接管窗口 -`FloatingAIAssistant` 是全局浮动 AI 客服 / AI 助手: +模块工作台的 AI 入口必须由 OpenClaw Control 原生 UI 接管: -- 折叠态显示 AI 头像、在线状态和未读建议数。 -- 展开态显示 `ArchIToken AI`、`Lv.7 工程智能体`、认证、角色、作品展示、能力标签、快捷操作和聊天消息。 -- 头像 / 主页区域可切换 AI 主页卡片。 -- 当前模块上下文建议来自 `moduleAssistantSuggestions`。 -- 快捷操作当前写入会话消息和审计事件。 -- 默认折叠贴边,展开后支持左/右停靠和聊天抽屉,避免遮挡主业务操作区。 -- 后续可映射到 Hermes Agent / LangGraph / Langfuse trace / MCP tool call。 +- 折叠态显示 `OpenClaw` 入口按钮。 +- 展开态不得渲染前端自研聊天消息流、模拟能力图谱或本地草案回复。 +- 展开态通过 `/api/openclaw/ui` 同源代理加载 OpenClaw Control,用于绕开 OpenClaw 原生响应中的 `X-Frame-Options: DENY` 与 `frame-ancestors 'none'` 嵌入限制。 +- `/api/openclaw/ui` 只代理 OpenClaw Control 静态 UI 与必要配置注入,不得在前端业务组件中直连 Hugging Face、Ollama、LM Studio 或其他外部模型 API。 +- OpenClaw 默认模型必须优先使用本机 `Ollama` / 本地 Hugging Face 适配器;不得把云端 5.4、OpenAI 或其他外部 provider 写成业务系统默认模型。 +- OpenClaw Control 的会话配置必须包含当前 `moduleId`、模块中文名和当前业务对象上下文。 +- OpenClaw Gateway 未真实接通时,窗口只能显示 OpenClaw 自身的连接/错误状态;不得回退到 Harness、本地草案或前端模拟回复。 +- OpenClaw 只能作为接管层和 Agent Runtime,所有工具调用仍必须经过 `WorkflowRouter -> ToolRouter -> ModelRouter/InferenceRouter -> CDE/AuditTrail -> Approver`。 +- 配图请求只生成图像任务和提示词,并通过 `GenerationRouter` 使用 Hugging Face provider hint 或本地缓存适配器;业务聊天 UI 不持有或传递 `HF_TOKEN`。 +- 没有专业来源、规范、审批或运行证据时,AI 输出只能标记为启发草案,不得标记为合规、送审、施工、验收或发布完成。 --- diff --git a/02-architecture/CONSTITUTION.md b/02-architecture/CONSTITUTION.md index 30df9606..222d22e9 100644 --- a/02-architecture/CONSTITUTION.md +++ b/02-architecture/CONSTITUTION.md @@ -108,8 +108,10 @@ ArchIToken 的技术选型必须以实现能力、生产价值、生态成熟度 - 开源项目、源码仓库和用户提供的 GitHub 链接是一等技术来源。需要能力时优先从源代码、上游文档、release tag 和可复现构建路线落地; apt、snap、系统包或二进制分发只是加速路径,不是能力边界。 - ArchIToken 可以按业务需要整体开源、局部开源、私有部署或混合分发。是否开源不能成为限制技术路线的理由。 -- CAD/BIM/PDF/工程几何遵循矢量优先、语义优先、原生源文件优先。DWG、DXF、IFC、STEP/STP、IGES/IGS、STL、SKP、3DM、RVT、PDF 和 3D PDF 等格式必须优先读取源文件实体、图层、属性、B-Rep、mesh、材质、单位和构件关系; 只有在真实原生/矢量/轻量化路线不可用时,才允许降级到 glTF/GLB/3D Tiles/OBJ/IFC 派生或其它可审计 derivative。 -- GitHub / 上游源码接入基线必须覆盖 OpenCascade/OCCT、FreeCAD、CGAL、LibreDWG、IfcOpenShell、Bonsai、buildingSMART、Blender、rhino3dm、OpenNURBS、ForgeCAD、Trimble/Tekla、Speckle、ThatOpen/WebIFC、Microsoft IFC、DataDrivenConstruction、louistrue IFC/CAD、OpenCDE、PDF/Office/Image/Video workers。它们必须进入 AdapterSourceRegistry、格式能力路由、AI 生成/在线编辑动作和审计链,不得只停留在口头技术选型。 +- CAD/BIM/PDF/工程几何遵循矢量优先、语义优先、原生源文件优先。DWG、DXF、IFC、STEP/STP、IGES/IGS、STL、SKP、3DM、RVT、PDF 和 3D PDF 等格式必须优先读取源文件实体、图层、属性、B-Rep、mesh、材质、单位和构件关系。 +- Prengine 是 ArchIToken 自研工程智能、几何、属性、资产派生和协同调用引擎名称,不是第三方 viewer 名称,也不是单一前端组件名称。Prengine 能力必须覆盖 OpenEngineeringViewer、AI 生成、可编辑几何、可编辑属性、构件选择、尺寸量测、坐标/单位、BOM/清单导出、worker 派生、审计、SDK 和 API 调用。 +- 工程模型/数字孪生资产主链路必须优先选择 OpenUSD/USDZ/3D Tiles。OpenUSD/USDZ 负责工程资产组合、层级、变体、引用、材质、属性、BOM 绑定和跨工具交换; 3D Tiles 负责超大场景、城市/园区、点云、倾斜摄影和分块 LOD 流式交付。只有在 OpenUSD/USDZ/3D Tiles 运行时、目标平台、授权边界、worker 转换、交付对象或浏览器能力明确不可用且形成审计证据时,才允许降级到 glTF/GLB。OBJ 和 FBX 不得作为新的平台主路线、默认导出目标、默认查看派生或长期资产标准,只允许作为历史导入兼容边界,并必须尽快归一到 OpenUSD/USDZ/3D Tiles 或 glTF/GLB 兜底资产。 +- GitHub / 上游源码接入基线必须覆盖 OpenCascade/OCCT、FreeCAD、CGAL、LibreDWG、IfcOpenShell、Bonsai、buildingSMART、Blender、OpenUSD/Pixar USD、Khronos glTF、Cesium/OGC 3D Tiles、rhino3dm、OpenNURBS、ForgeCAD、Trimble/Tekla、Speckle、ThatOpen/WebIFC、Microsoft IFC、DataDrivenConstruction、louistrue IFC/CAD、OpenCDE、PDF/Office/Image/Video workers。它们必须进入 AdapterSourceRegistry、格式能力路由、AI 生成/在线编辑动作和审计链,不得只停留在口头技术选型。 - WebGPU 是浏览器和交互式工程视口的第一渲染/计算路线。WebGL 只能作为兼容回退、缩略图或第三方遗留组件边界,不得作为 BIM、CAD、数字孪生、视频/图像 AI 编辑或工程模型在线编辑的默认核心路线。 - GPU 和平台能力必须全面覆盖 NVIDIA/CUDA/OptiX、AMD ROCm/HIP、Intel oneAPI/Level Zero/Vulkan、Apple Metal、Windows DirectX 12、Linux/Vulkan/WebGPU、Android/Vulkan/WebGPU、iOS/Metal/WebGPU、Triton AI kernel 和 CPU SIMD fallback。ARM64、x86_64、NVIDIA、AMD、Intel、Apple Silicon 都是生产目标,不是兼容性例外。 - 可进入核心分发边界的依赖优先使用 Apache-2.0 / MIT / BSD / ISC / MPL-2.0 / MPL-2.0 等宽松许可。 @@ -199,6 +201,8 @@ OpenAPI + AsyncAPI + JSON Schema + IFC Schema + Module Schema | IFC Schema | BIM / AEC 模型语义、构件、属性、关系校验 | | Module Schema | 模块注册、模块输入输出、能力、SLA、权限、UI 元数据 | +Prengine 对外能力必须通过 OpenAPI、AsyncAPI、JSON Schema 和生成 SDK 暴露,不得只通过前端组件或临时代码调用。Prengine SDK/API 的最小能力边界包括: 源模型导入、AI 模型/构件生成、可编辑几何操作、可编辑属性写回、构件选择与高亮、尺寸/坐标/单位读取、BOM/清单导出、格式派生、异步 worker 任务、审计事件和审批状态。所有模型资产 API 必须声明源文件真源、目标格式优先级、OpenUSD/USDZ/3D Tiles 优先策略、glTF/GLB 降级理由、属性 Schema、元素 ID 映射、单位/坐标系和审计 ID。 + **CI 执行**: Schema 变更必须有 diff 检查、生成物检查、兼容性检查。 --- @@ -297,6 +301,11 @@ ArchIToken 可以使用 Next.js 16.2.6 + React 19.2.5 + TypeScript 6.0.3 + WASM Next.js + React + TypeScript = 应用工程基座 GPU-first = 默认执行策略 WebGPU + WASM = 浏览器高性能计算与渲染核心 +Prengine = ArchIToken 自研工程智能、几何、属性、资产派生与 SDK/API 调用引擎 +OpenUSD/USDZ = AI 生成、可编辑几何、可编辑属性、BOM、工程场景与数字孪生资产主派生/交换路线 +3D Tiles = 超大场景、城市/园区、点云、倾斜摄影与分块 LOD 主流式路线 +glTF / GLB = OpenUSD/USDZ/3D Tiles 不可用时的 Web 运行时与交付兜底 +OBJ / FBX = 废弃兼容输入,不得作为新主链路 CUDA / ROCm / DirectX 12 / Metal / Vulkan / Triton = 平台原生 GPU 加速路线 Three.js = WebGPU 承载层 / 兼容层 / 生态层 / 快速验证层 WebGL = 最后兼容回退,非默认核心 @@ -305,6 +314,10 @@ CPU = 有证据的兼容回退,非默认热路径 WebGPU 是 CAD/BIM/IFC/STEP/STL/IGES/3DM/SKP/PDF 图形层、数字孪生、图片/视频 AI 编辑和在线工程编辑的默认交互式渲染与浏览器计算路线。Three.js 可以作为 WebGPU renderer、场景组织、loader 生态和 fallback 承载层,但禁止把 Three.js/WebGL 当作唯一渲染路线,也禁止为了“纯 WebGPU”放弃成熟工程框架。 +Prengine 是 ArchIToken 自研工程智能、几何、属性、资产派生和 SDK/API 调用引擎。Prengine 不是单一 viewer,其能力边界必须覆盖 AI 生成、在线几何编辑、属性编辑、构件选择/高亮、尺寸量测、坐标/单位处理、BOM/清单导出、worker 派生、审计、审批和外部 SDK/API 调用。 + +工程模型和数字孪生派生格式的优先级为 OpenUSD/USDZ/3D Tiles -> glTF/GLB。OpenUSD/USDZ 承担 AI 生成结果、长期资产组合、层级、变体、引用、材质、属性、BOM 绑定和跨工具交换; 3D Tiles 承担超大场景、园区、城市、点云、倾斜摄影和分块 LOD 主流式交付; glTF/GLB 只有在 OpenUSD/USDZ/3D Tiles 不可用并写入审计理由时,才承担浏览器交互运行时和交付兜底。OBJ/FBX 不得作为新功能默认 viewer、默认 export、默认 worker artifact 或长期资产标准。 + GPU-first 是默认执行策略。只要目标设备、浏览器、驱动、运行时或集群节点具备可用 GPU,以下能力必须优先选择 GPU 路线: CAD/BIM/数字孪生视口、点云/mesh/IFC/STEP/STL/IGES/3DM/SKP 几何处理、PDF/Office 图形层编辑、图片/视频 AI 生成与在线编辑、模型推理、向量/矩阵/栅格/几何 kernel、渲染、转码和批量派生。CPU-only、WebGL-only 或纯前端 Canvas 路线只能作为明确记录的兼容 fallback、无 GPU 环境的离线模式、缩略图生成或失败恢复路径,不得成为生产默认热路径。 平台 GPU 路线必须覆盖: diff --git a/02-architecture/DIGITAL_TWIN.md b/02-architecture/DIGITAL_TWIN.md index a5b007c8..c9d1af53 100644 --- a/02-architecture/DIGITAL_TWIN.md +++ b/02-architecture/DIGITAL_TWIN.md @@ -90,8 +90,9 @@ | 层 | 当前实现 | 后续目标 | |----|----------|----------| -| HMI 驾驶舱 | Next.js + React + Tailwind CSS | 驾驶舱配置 JSON 化,支持租户主题 | -| 3D / CIM 主视图 | CSS/SVG HMI prototype | WebGPU renderer + OpenUSD / IFC / 3D Tiles | +| HMI 驾驶舱 | Next.js + React + Ant Design tokenized CSS | 驾驶舱配置 JSON 化,支持租户主题 | +| 3D / CIM 主视图 | WebGPU 原生 WGSL 视口 + Three.js fallback | OpenUSD / IFC / 3D Tiles 真实运行时接入 | +| 场景与资产组合 | openBIM IFC4.3 / IDS / BCF + glTF/GLB + 3D Tiles + OpenUSD 契约 | 后端原生 worker 输出可审计 scene package | | 3DGS 实景层 | fixture 标注为 video/photo/360 来源 | SPZ / PLY runtime loader,支持视频重建实景 | | 点云校核层 | E57 / LiDAR residual 标注 | E57 / LAS / LAZ 控制点与残差热图 | | BIM 语义层 | IFC4.3 / MBD fixture | buildingSMART IFC4.3 / IDS / BCF 校验 | @@ -135,6 +136,7 @@ | 文档需求 | 实现文件 | |----------|----------| | UI 信息架构 | [`../03-frontend/components/ModuleWorkbenchShell.tsx`](../03-frontend/components/ModuleWorkbenchShell.tsx), [`../03-frontend/components/ModuleFileExplorer.tsx`](../03-frontend/components/ModuleFileExplorer.tsx) | +| WebGPU 主视口 | [`../03-frontend/components/DigitalTwinWebGPUViewport.tsx`](../03-frontend/components/DigitalTwinWebGPUViewport.tsx), [`../03-frontend/components/DigitalTwinOperationsPanel.tsx`](../03-frontend/components/DigitalTwinOperationsPanel.tsx) | | 数据契约 | [`../03-frontend/lib/digital-twin.ts`](../03-frontend/lib/digital-twin.ts) | | 验收测试 | [`../03-frontend/lib/digital-twin.test.ts`](../03-frontend/lib/digital-twin.test.ts) | | 模块工作台入口 | [`../03-frontend/app/app/modules/[moduleId]/page.tsx`](../03-frontend/app/app/modules/%5BmoduleId%5D/page.tsx) | diff --git a/02-architecture/HEAVY_STEEL_AI_TOKEN_COMMERCIAL_WORKFLOW.md b/02-architecture/HEAVY_STEEL_AI_TOKEN_COMMERCIAL_WORKFLOW.md index a045d413..9b3a972d 100644 --- a/02-architecture/HEAVY_STEEL_AI_TOKEN_COMMERCIAL_WORKFLOW.md +++ b/02-architecture/HEAVY_STEEL_AI_TOKEN_COMMERCIAL_WORKFLOW.md @@ -38,7 +38,7 @@ ## 3. 文件与模型运行时 -IFC、DWG、DXF、RVT、DGN、STEP、STP、IGES、IGS、STL、OBJ、FBX、glTF、GLB、3DM、SKP、USD、PDF、3D PDF、Office、代码和压缩包都必须通过 FileTypeRegistry、Adapter Isolation Registry、StorageRouter 和 Worker 管线进入。 +IFC、DWG、DXF、RVT、DGN、STEP、STP、IGES、IGS、STL、PLY、DAE、OpenUSD/USDZ、3D Tiles、glTF/GLB 兜底、3DM、SKP、PDF、3D PDF、Office、代码和压缩包都必须通过 FileTypeRegistry、Adapter Isolation Registry、StorageRouter 和 Worker 管线进入。OBJ/FBX 只作为废弃历史输入兼容边界,不得作为新默认 viewer/export/worker artifact。 优先顺序: diff --git a/02-architecture/MODULES.md b/02-architecture/MODULES.md index 9699ec6b..403b4989 100644 --- a/02-architecture/MODULES.md +++ b/02-architecture/MODULES.md @@ -104,11 +104,12 @@ ArchIToken = AEC AI-Native + Harness Engineering + OpenBIM CDE Workflow OS - **description**: 项目立项、WBS、里程碑、资源计划、审批计划与跨模块交付总控模块。 承接市场客服形成的商机和需求,将其转化为可执行的项目计划、责任矩阵和交付节奏。 + 支持进度计划在线编制、任务拆解、进度反馈、图表分析、进度预警、进度调整和任务状态闭环。 为方案设计、计量造价、生产制造、施工管理和财务人力提供统一计划基线。 - **inputs**: `[marketing_service]` - **outputs**: `[concept_design, quantity_costing, production_manufacturing, construction_management, finance_hr]` - **prompt_dir**: `prompts/planning_management/` -- **tables**: `project_plans`, `wbs_items`, `milestones`, `resource_plans`, `approval_plans` +- **tables**: `project_plans`, `wbs_items`, `milestones`, `resource_plans`, `approval_plans`, `project_plan_progress_feedback`, `project_plan_schedule_alerts`, `project_plan_schedule_adjustments` ### 2.3 `concept_design` · 方案设计 @@ -195,10 +196,11 @@ ArchIToken = AEC AI-Native + Harness Engineering + OpenBIM CDE Workflow OS 面向重钢结构、装配式构件和工厂预制全流程。 把 BIM 构件翻译成 CNC / 焊接文件 + 加工 BOM + 质检单。 对接工厂 MES / ERP,回传加工进度、发运批次与质检结果。 + 当前阶段由 Paperclip v2026.517.0 完整接管本模块主工作区,用于 Agent 组织、工厂任务、心跳、预算和治理编排;它不替代 ArchIToken 的生产制造模块 ID、CDE 文件、CNC/QC/MES/ERP 真源或专业签审门禁。 - **inputs**: `[planning_management, detailed_design, quantity_costing, standard_library]` - **outputs**: `[material_logistics, construction_management, finance_hr]` - **prompt_dir**: `prompts/production_manufacturing/` -- **tables**: `work_orders`, `cnc_files`, `qc_records`, `production_batches` +- **tables**: `work_orders`, `cnc_files`, `qc_records`, `production_batches`, `paperclip_agent_runs` ### 2.9 `construction_management` · 施工管理 · **status: active · depth: production-ready** @@ -281,12 +283,13 @@ ArchIToken = AEC AI-Native + Harness Engineering + OpenBIM CDE Workflow OS - **order**: 13 - **description**: 企业 AI、API、RAG、MCP、Agent、模型路由、工具权限、安全审计和成本策略模块。 + AI 中心同时承载接口管理、数据库管理和可视化面板治理,用于登记接口合同、数据对象、RLS 边界、运行视图和发布门禁。 为所有业务模块提供统一 AI 能力编排、上下文治理、审计和成本控制。 与设置中心共同构成平台级能力底座。 - **inputs**: `[]` (平台能力底座) - **outputs**: `[]` (被其它模块引用) - **prompt_dir**: `prompts/ai_center/` -- **tables**: `model_routes`, `rag_sources`, `mcp_tools`, `agent_runs`, `ai_cost_events` +- **tables**: `model_routes`, `interface_contracts`, `database_bindings`, `visualization_panels`, `rag_sources`, `mcp_tools`, `agent_runs`, `ai_cost_events` ### 2.14 `settings_center` · 设置中心 diff --git a/02-architecture/OPEN_SOURCE_RADAR.md b/02-architecture/OPEN_SOURCE_RADAR.md index 335abea1..f9a81dbe 100644 --- a/02-architecture/OPEN_SOURCE_RADAR.md +++ b/02-architecture/OPEN_SOURCE_RADAR.md @@ -97,7 +97,7 @@ Use these for the digital twin station and construction evidence model. | [playcanvas/supersplat](https://github.com/playcanvas/supersplat) | Gaussian Splat editing and optimization; strong reference for point-cloud replacement workflows. | | [Scthe/gaussian-splatting-webgpu](https://github.com/Scthe/gaussian-splatting-webgpu) | WebGPU 3D Gaussian Splatting renderer reference. | | [Visionary-Laboratory/visionary](https://github.com/Visionary-Laboratory/visionary) | WebGPU-powered Gaussian Splatting/world-model direction. | -| [louistrue/ifc-lite](https://github.com/louistrue/ifc-lite) | Browser-native IFC viewer with WebGPU and Rust/WASM parser. Watch closely. | +| [LTplus-AG/ifc-lite](https://github.com/LTplus-AG/ifc-lite) | Browser-native IFC viewer with WebGPU and Rust/WASM parser. Watch closely. | | [xyzbety/IFCFlux](https://github.com/xyzbety/IFCFlux) | Lightweight WebGPU IFC engine and rule checks. Early but aligned. | Decision: @@ -160,7 +160,7 @@ Goal: make every generated or uploaded model machine-checkable. Inputs: -- IFC, glTF, OBJ, STEP, CAD drawings, PDF drawing sets. +- IFC, OpenUSD/USDZ, 3D Tiles, glTF/GLB fallback, STEP, CAD drawings, PDF drawing sets. - IDS requirements, bSDD classifications, local project standards. Outputs: @@ -261,7 +261,7 @@ Run these regularly as GitHub heatmap searches: 1. Add ADR: WebGPU digital twin editor uses Pascal-style editable scene graph. 2. Add ADR: Gaussian Splatting is the default field reality layer for point cloud, 360, drone, and video reconstruction. -3. Add ADR: Generated geometry must produce editable parametric nodes plus exportable IFC/glTF/USD/3D Tiles. +3. Add ADR: Generated geometry must produce editable parametric nodes plus exportable IFC/OpenUSD/USDZ/3D Tiles, with glTF/GLB only as audited fallback. 4. Build a small `ifc-webgpu-spike` branch using Web-IFC or ifc-lite for browser-side IFC loading. 5. Build a `cadquery-sidecar-spike` for text-to-parametric-model generation and BOQ extraction. 6. Build a `pdf-archive-spike` that extracts contract clauses and drawing sheets into Archive Token records. @@ -288,7 +288,7 @@ Additional heatmap hits: - - -- +- - - - diff --git a/03-frontend/app/api/ai/openclaw/chat/route.ts b/03-frontend/app/api/ai/openclaw/chat/route.ts new file mode 100644 index 00000000..08cfb27c --- /dev/null +++ b/03-frontend/app/api/ai/openclaw/chat/route.ts @@ -0,0 +1,392 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { spawn } from 'node:child_process'; + +import { + buildImagePrompt, + createOpenClawMessage, + isOpenClawImageIntent, + type OpenClawChatArtifact, + type OpenClawWorkbenchChatRequest, + type OpenClawWorkbenchChatResponse, +} from '@/lib/openclaw-workbench-chat'; + +export const dynamic = 'force-dynamic'; +export const runtime = 'nodejs'; + +interface OpenAiCompatibleChoice { + message?: { + content?: string; + }; + text?: string; +} + +interface OpenAiCompatibleResponse { + choices?: OpenAiCompatibleChoice[]; + output_text?: string; + content?: string; + model?: string; +} + +interface OpenClawCliResponse { + ok?: boolean; + provider?: string; + model?: string; + transport?: string; + outputs?: Array<{ + text?: string; + mediaUrl?: string | null; + }>; +} + +export async function POST(request: NextRequest) { + let body: OpenClawWorkbenchChatRequest; + + try { + body = await request.json() as OpenClawWorkbenchChatRequest; + } catch { + return NextResponse.json( + { error: 'Invalid OpenClaw chat request body.' }, + { status: 400 }, + ); + } + + const diagnostics: string[] = []; + const systemPrompt = buildSystemPrompt(body); + const latestUserMessage = [...body.messages].reverse().find((message) => message.role === 'user'); + const latestInput = latestUserMessage?.content ?? ''; + const artifacts = await buildArtifacts(body, latestInput, diagnostics); + + const openClawGateway = await invokeOpenClawGateway(body, systemPrompt, diagnostics); + if (openClawGateway) { + return NextResponse.json({ + message: createOpenClawMessage('assistant', openClawGateway.content, { + route: 'OpenClaw Gateway /v1/chat/completions', + artifacts, + }), + routedBy: 'openclaw_gateway', + routeStatus: 'routed', + model: openClawGateway.model ?? 'openclaw/default', + diagnostics, + } satisfies OpenClawWorkbenchChatResponse); + } + + const openClawCli = await invokeOpenClawCliGateway(body, systemPrompt, diagnostics); + if (openClawCli) { + return NextResponse.json({ + message: createOpenClawMessage('assistant', openClawCli.content, { + route: 'OpenClaw CLI -> Gateway -> model.run', + artifacts, + }), + routedBy: 'openclaw_cli_gateway', + routeStatus: 'routed', + model: openClawCli.model, + diagnostics, + } satisfies OpenClawWorkbenchChatResponse); + } + + return NextResponse.json( + { + error: 'OpenClaw Gateway 未真实接通,已拒绝生成假回复。', + diagnostics, + }, + { status: 503 }, + ); +} + +function buildSystemPrompt(request: OpenClawWorkbenchChatRequest): string { + const capabilitySummary = request.capabilities + .slice(0, 28) + .map((capability) => `- ${capability.label}: ${capability.description}`) + .join('\n'); + const auditSummary = request.auditEvents + .slice(0, 6) + .map((event) => `- ${event.summary}`) + .join('\n') || '- 暂无当前页审计事件'; + + return [ + '你是 ArchIToken 平台内的 OpenClaw 接管层,不是孤立聊天机器人。', + '你必须通过 WorkflowRouter、ToolRouter、ModelRouter、InferenceRouter、GenerationRouter、CDE、AuditTrail 和 Approver 表达执行路径。', + '你可以协调市场客服、计划管理、方案设计、标准族库、深化设计、计量造价、材料物流、生产制造、施工管理、数字孪生、数字档案、财务人力、AI中心和设置中心。', + '没有专业来源、规范、审批或运行证据时,只能输出启发草案,不得声称合规、送审、施工、验收或发布完成。', + `当前模块: ${request.moduleName} (${request.moduleId})`, + request.selectedFeatureTitle ? `当前业务对象: ${request.selectedFeatureTitle}` : '当前业务对象: 未锁定', + '可用平台能力:', + capabilitySummary, + '最近审计:', + auditSummary, + '回答要求: 使用简体中文,先给可执行路由,再给下一步动作;涉及图像时生成可交给 Hugging Face 图像端点的英文提示词。', + ].join('\n'); +} + +async function invokeOpenClawGateway( + request: OpenClawWorkbenchChatRequest, + systemPrompt: string, + diagnostics: string[], +): Promise<{ content: string; model?: string } | null> { + const baseUrl = process.env.OPENCLAW_GATEWAY_URL; + if (!baseUrl) { + diagnostics.push('OPENCLAW_GATEWAY_URL 未配置,使用 OpenClaw CLI Gateway adapter。'); + return null; + } + + const model = process.env.OPENCLAW_MODEL ?? 'openai-codex/gpt-5.4'; + const headers: Record = { 'Content-Type': 'application/json' }; + if (process.env.OPENCLAW_GATEWAY_TOKEN) { + headers.Authorization = `Bearer ${process.env.OPENCLAW_GATEWAY_TOKEN}`; + } + + try { + const response = await fetch(new URL('/v1/chat/completions', normalizedBaseUrl(baseUrl)), { + method: 'POST', + headers, + body: JSON.stringify({ + model, + stream: false, + temperature: 0.2, + messages: [ + { role: 'system', content: systemPrompt }, + ...request.messages.slice(-10).map((message) => ({ + role: message.role, + content: message.content, + })), + ], + }), + cache: 'no-store', + }); + + if (!response.ok) { + diagnostics.push(`OpenClaw Gateway 返回 HTTP ${response.status}。`); + return null; + } + + const payload = await response.json() as OpenAiCompatibleResponse; + const content = extractOpenAiCompatibleContent(payload); + if (!content) { + diagnostics.push('OpenClaw Gateway 响应没有可用文本。'); + return null; + } + + return { + content, + model: payload.model ?? model, + }; + } catch (error) { + diagnostics.push(`OpenClaw Gateway 调用失败: ${formatError(error)}。`); + return null; + } +} + +async function invokeOpenClawCliGateway( + request: OpenClawWorkbenchChatRequest, + systemPrompt: string, + diagnostics: string[], +): Promise<{ content: string; model: string } | null> { + const cli = process.env.OPENCLAW_CLI_PATH ?? '/usr/bin/openclaw'; + const model = process.env.OPENCLAW_MODEL ?? 'openai-codex/gpt-5.4'; + const prompt = buildOpenClawPrompt(systemPrompt, request); + + try { + const result = await runOpenClawCli(cli, [ + 'infer', + 'model', + 'run', + '--gateway', + '--model', + model, + '--prompt', + prompt, + '--json', + ]); + + if (result.code !== 0) { + diagnostics.push(`OpenClaw CLI Gateway 退出码 ${result.code}: ${trimForDiagnostic(result.stderr || result.stdout)}`); + return null; + } + + const payload = extractJsonPayload(result.stdout) as OpenClawCliResponse | null; + const text = payload?.outputs?.map((output) => output.text).find(Boolean); + if (!payload?.ok || !text) { + diagnostics.push(`OpenClaw CLI Gateway 没有返回有效文本: ${trimForDiagnostic(result.stdout)}`); + return null; + } + + return { + content: text, + model: `${payload.provider ?? 'openclaw'}/${payload.model ?? model}`, + }; + } catch (error) { + diagnostics.push(`OpenClaw CLI Gateway 调用失败: ${formatError(error)}。`); + return null; + } +} + +function buildOpenClawPrompt(systemPrompt: string, request: OpenClawWorkbenchChatRequest): string { + const conversation = request.messages + .slice(-10) + .map((message) => `${message.role === 'user' ? '用户' : 'OpenClaw'}: ${message.content}`) + .join('\n\n'); + + return [ + systemPrompt, + '下面是当前工作台真实会话。你必须作为 OpenClaw Gateway 的模型执行结果回复,不要声称本地草案或模拟执行。', + conversation, + ].join('\n\n'); +} + +function runOpenClawCli( + cli: string, + args: string[], +): Promise<{ code: number | null; stdout: string; stderr: string }> { + return new Promise((resolve, reject) => { + const child = spawn(cli, args, { + env: process.env, + stdio: ['ignore', 'pipe', 'pipe'], + }); + const timer = setTimeout(() => { + child.kill('SIGTERM'); + reject(new Error('OpenClaw CLI Gateway 调用超时')); + }, 120_000); + let stdout = ''; + let stderr = ''; + + child.stdout.setEncoding('utf8'); + child.stderr.setEncoding('utf8'); + child.stdout.on('data', (chunk) => { + stdout += chunk; + }); + child.stderr.on('data', (chunk) => { + stderr += chunk; + }); + child.on('error', (error) => { + clearTimeout(timer); + reject(error); + }); + child.on('close', (code) => { + clearTimeout(timer); + resolve({ code, stdout, stderr }); + }); + }); +} + +async function buildArtifacts( + request: OpenClawWorkbenchChatRequest, + latestInput: string, + diagnostics: string[], +): Promise { + const artifacts: OpenClawChatArtifact[] = []; + if (!isOpenClawImageIntent(latestInput, request.activeCapabilityId)) { + return artifacts; + } + + const prompt = buildImagePrompt(request, latestInput); + artifacts.push({ + id: 'hf-image-prompt', + kind: 'image_prompt', + title: 'Hugging Face 配图提示词', + content: prompt, + status: 'pending_router', + }); + + const job = await createImageGenerationJob(request, prompt, diagnostics); + if (job) { + artifacts.push(job); + } + + return artifacts; +} + +async function createImageGenerationJob( + request: OpenClawWorkbenchChatRequest, + prompt: string, + diagnostics: string[], +): Promise { + const baseUrl = process.env.ARCHITOKEN_GATEWAY_BASE_URL + ?? process.env.NEXT_PUBLIC_ARCHITOKEN_API_BASE_URL + ?? 'http://127.0.0.1:8080'; + + try { + const response = await fetch(new URL('/v1/generation/jobs', normalizedBaseUrl(baseUrl)), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Tenant-Id': process.env.ARCHITOKEN_TENANT_ID ?? '11111111-1111-4111-8111-111111111111', + 'X-Project-Id': process.env.ARCHITOKEN_PROJECT_ID ?? '22222222-2222-4222-8222-222222222222', + 'X-Actor': 'openclaw', + 'X-Roles': 'admin', + 'X-Request-Id': `openclaw-chat-${Date.now()}`, + 'X-Correlation-Id': `openclaw-chat-${request.moduleId}`, + }, + body: JSON.stringify({ + mode: 'text_to_image', + moduleId: request.moduleId, + prompt, + actor: 'openclaw', + constraints: { + router: 'GenerationRouter', + providerHint: 'hugging_face', + provenance: { + source: 'openclaw_workbench_chat', + selectedFeatureTitle: request.selectedFeatureTitle ?? null, + }, + }, + }), + cache: 'no-store', + }); + + if (!response.ok) { + diagnostics.push(`GenerationRouter 配图任务创建返回 HTTP ${response.status}。`); + return null; + } + + const payload = await response.json() as { id?: string; job_id?: string; status?: string }; + const jobId = payload.id ?? payload.job_id ?? 'pending'; + return { + id: `generation-job-${jobId}`, + kind: 'generation_job', + title: 'GenerationRouter 图像任务', + content: `已提交 Hugging Face providerHint 的配图任务: ${jobId}`, + status: payload.status === 'blocked' ? 'blocked' : 'pending_router', + }; + } catch (error) { + diagnostics.push(`GenerationRouter 配图任务创建失败: ${formatError(error)}。`); + return null; + } +} + +function extractOpenAiCompatibleContent(payload: OpenAiCompatibleResponse): string { + const choice = payload.choices?.[0]; + return choice?.message?.content ?? choice?.text ?? payload.output_text ?? payload.content ?? ''; +} + +function extractJsonPayload(stdout: string): unknown { + const trimmed = stdout.trim(); + if (!trimmed) return null; + + try { + return JSON.parse(trimmed); + } catch { + const firstBrace = trimmed.indexOf('{'); + const lastBrace = trimmed.lastIndexOf('}'); + if (firstBrace === -1 || lastBrace === -1 || lastBrace <= firstBrace) { + return null; + } + + try { + return JSON.parse(trimmed.slice(firstBrace, lastBrace + 1)); + } catch { + return null; + } + } +} + +function normalizedBaseUrl(baseUrl: string): string { + return baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`; +} + +function trimForDiagnostic(value: string): string { + return value.replace(/\s+/g, ' ').trim().slice(0, 500); +} + +function formatError(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} diff --git a/03-frontend/app/api/local-files/[fileId]/cad-derivative/route.ts b/03-frontend/app/api/local-files/[fileId]/cad-derivative/route.ts index 2340ae8e..2f36dfcd 100644 --- a/03-frontend/app/api/local-files/[fileId]/cad-derivative/route.ts +++ b/03-frontend/app/api/local-files/[fileId]/cad-derivative/route.ts @@ -84,7 +84,12 @@ export async function GET( } function normalizeFormat(value: string | null): CadDerivativeFormat { - if (value === 'dxf' || value === 'pdf' || value === 'manifest') { + if ( + value === 'dxf' || + value === 'pdf' || + value === 'svg' || + value === 'manifest' + ) { return value; } return 'manifest'; diff --git a/03-frontend/app/api/local-files/[fileId]/ifc-derivative/route.ts b/03-frontend/app/api/local-files/[fileId]/ifc-derivative/route.ts index f6d0bd9b..905d3f97 100644 --- a/03-frontend/app/api/local-files/[fileId]/ifc-derivative/route.ts +++ b/03-frontend/app/api/local-files/[fileId]/ifc-derivative/route.ts @@ -1,15 +1,15 @@ // app/api/local-files/[fileId]/ifc-derivative/route.ts - IFC derivative cache endpoint // License: Apache-2.0 -import { NextResponse } from 'next/server'; +import { NextResponse } from "next/server"; import { buildIfcDerivativeManifest, IfcDerivativeError, readIfcDerivativeBytes, type IfcDerivativeFormat, -} from '@/lib/ifc-derivative-server'; +} from "@/lib/ifc-derivative-server"; -export const runtime = 'nodejs'; +export const runtime = "nodejs"; export async function GET( request: Request, @@ -17,12 +17,13 @@ export async function GET( ) { const { fileId } = await params; const url = new URL(request.url); - const format = normalizeFormat(url.searchParams.get('format')); + const format = normalizeFormat(url.searchParams.get("format")); + const tilePath = url.searchParams.get("path"); try { - if (format === 'manifest') { + if (format === "manifest") { const manifest = await buildIfcDerivativeManifest(fileId); - if (request.headers.get('if-none-match') === manifest.etag) { + if (request.headers.get("if-none-match") === manifest.etag) { return new Response(null, { status: 304, headers: cacheHeaders(manifest.etag, manifest.fileId), @@ -33,8 +34,8 @@ export async function GET( }); } - const derivative = await readIfcDerivativeBytes(fileId, format); - if (request.headers.get('if-none-match') === derivative.etag) { + const derivative = await readIfcDerivativeBytes(fileId, format, tilePath); + if (request.headers.get("if-none-match") === derivative.etag) { return new Response(null, { status: 304, headers: derivativeHeaders(derivative), @@ -42,7 +43,7 @@ export async function GET( } const range = parseRangeHeader( - request.headers.get('range'), + request.headers.get("range"), derivative.bytes.byteLength, ); const payload = range @@ -54,11 +55,11 @@ export async function GET( status: range ? 206 : 200, headers: { ...derivativeHeaders(derivative), - 'content-length': String(payload.byteLength), - 'content-disposition': `inline; filename*=UTF-8''${encodeURIComponent(derivative.fileName)}`, + "content-length": String(payload.byteLength), + "content-disposition": `inline; filename*=UTF-8''${encodeURIComponent(derivative.fileName)}`, ...(range ? { - 'content-range': `bytes ${range.start}-${range.end}/${derivative.bytes.byteLength}`, + "content-range": `bytes ${range.start}-${range.end}/${derivative.bytes.byteLength}`, } : {}), }, @@ -76,7 +77,7 @@ export async function GET( } return NextResponse.json( { - error: 'ifc_derivative_failed', + error: "ifc_derivative_failed", message: error instanceof Error ? error.message : String(error), }, { status: 500 }, @@ -85,18 +86,30 @@ export async function GET( } function normalizeFormat(value: string | null): IfcDerivativeFormat { - if (value === 'properties-index') { + if (value === "properties-index") { return value; } - return 'manifest'; + if (value === "openusd") { + return value; + } + if (value === "tileset") { + return value; + } + if (value === "tile") { + return value; + } + if (value === "glb") { + return value; + } + return "manifest"; } function cacheHeaders(etag: string, fileId: string) { return { etag, - 'cache-control': 'private, max-age=0, must-revalidate', - 'x-architoken-file-id': fileId, - 'x-architoken-cache-contract': 'stream+etag+checksum', + "cache-control": "private, max-age=0, must-revalidate", + "x-architoken-file-id": fileId, + "x-architoken-cache-contract": "stream+etag+checksum", }; } @@ -106,11 +119,11 @@ function derivativeHeaders(derivative: { cacheHit: boolean; }) { return { - 'content-type': derivative.mediaType, + "content-type": derivative.mediaType, etag: derivative.etag, - 'cache-control': 'private, max-age=0, must-revalidate', - 'accept-ranges': 'bytes', - 'x-architoken-cache-hit': String(derivative.cacheHit), + "cache-control": "private, max-age=0, must-revalidate", + "accept-ranges": "bytes", + "x-architoken-cache-hit": String(derivative.cacheHit), }; } @@ -118,12 +131,12 @@ function parseRangeHeader( header: string | null, size: number, ): { start: number; end: number } | null { - if (!header?.startsWith('bytes=')) { + if (!header?.startsWith("bytes=")) { return null; } - const [startRaw, endRaw] = header.slice('bytes='.length).split('-', 2); - const start = Number.parseInt(startRaw ?? '', 10); - const requestedEnd = Number.parseInt(endRaw ?? '', 10); + const [startRaw, endRaw] = header.slice("bytes=".length).split("-", 2); + const start = Number.parseInt(startRaw ?? "", 10); + const requestedEnd = Number.parseInt(endRaw ?? "", 10); if (!Number.isFinite(start) || start < 0 || start >= size) { return null; } diff --git a/03-frontend/app/api/local-files/[fileId]/native-open/route.ts b/03-frontend/app/api/local-files/[fileId]/native-open/route.ts index 81df7017..5f133ccf 100644 --- a/03-frontend/app/api/local-files/[fileId]/native-open/route.ts +++ b/03-frontend/app/api/local-files/[fileId]/native-open/route.ts @@ -1,15 +1,15 @@ // app/api/local-files/[fileId]/native-open/route.ts - Native source open manifest // License: Apache-2.0 -import { stat } from 'node:fs/promises'; -import { NextResponse } from 'next/server'; -import { fileTypeForFileName } from '@/lib/file-type-registry'; +import { stat } from "node:fs/promises"; +import { NextResponse } from "next/server"; +import { fileTypeForFileName } from "@/lib/file-type-registry"; import { getLocalFileMetadata, resolveLocalUploadStoragePath, -} from '@/lib/local-file-runtime-server'; +} from "@/lib/local-file-runtime-server"; -export const runtime = 'nodejs'; +export const runtime = "nodejs"; export async function GET( _request: Request, @@ -19,7 +19,7 @@ export async function GET( const metadata = await getLocalFileMetadata(fileId); if (!metadata) { - return NextResponse.json({ error: 'file not found' }, { status: 404 }); + return NextResponse.json({ error: "file not found" }, { status: 404 }); } const path = resolveLocalUploadStoragePath(metadata); @@ -29,7 +29,7 @@ export async function GET( const sourceUrl = `/api/local-files/${encodeURIComponent(metadata.fileId)}`; return NextResponse.json({ - schema: 'architoken.native-open.v1', + schema: "architoken.native-open.v1", fileId: metadata.fileId, originalName: metadata.originalName, extension: ext, @@ -39,9 +39,9 @@ export async function GET( etag: `sha256-${metadata.checksum}`, sourceOfRecord: { url: sourceUrl, - method: 'GET', + method: "GET", rangeRequests: true, - cache: 'ETag + Last-Modified + private revalidation', + cache: "ETag + Last-Modified + private revalidation", substitutePreview: false, }, registry: registry @@ -58,162 +58,182 @@ export async function GET( } function nativeRoutesFor(ext: string, sourceUrl: string) { - if (ext === '.pdf') { + if (ext === ".pdf") { return [ { - id: 'pdf-native-stream', - status: 'ready', - viewer: 'browser-native-multipage-vector-pdf', + id: "pdf-native-stream", + status: "ready", + viewer: "browser-native-multipage-vector-pdf", sourceUrl, notes: [ - 'PDF and 3D PDF stay bound to the original source bytes.', - 'PRC/U3D extraction is a worker adapter boundary; source streaming still works immediately.', + "PDF and 3D PDF stay bound to the original source bytes.", + "PRC/U3D extraction is a worker adapter boundary; source streaming still works immediately.", ], }, ]; } - if (ext === '.xml' || ext === '.gbxml' || ext === '.ids') { + if (ext === ".xml" || ext === ".gbxml" || ext === ".ids") { return [ { - id: 'xml-native-stream', - status: 'ready', - viewer: 'code-editor', + id: "xml-native-stream", + status: "ready", + viewer: "code-editor", sourceUrl, - worker: 'xml/ids/gbxml parser when validation is requested', + worker: "xml/ids/gbxml parser when validation is requested", }, ]; } - if (ext === '.3dxml') { + if (ext === ".3dxml") { return [ { - id: '3dxml-source-stream', - status: 'ready', - viewer: 'source-bound-engineering-object', + id: "3dxml-source-stream", + status: "ready", + viewer: "source-bound-engineering-object", sourceUrl, }, { - id: '3dxml-cad-kernel', - status: 'adapter_required', - worker: '3D XML / OCCT-compatible isolated adapter', - outputs: ['glb', 'gltf', 'brep', 'properties-index'], + id: "3dxml-cad-kernel", + status: "adapter_required", + worker: "3D XML / OCCT-compatible isolated adapter", + outputs: ["glb", "gltf", "brep", "properties-index"], }, ]; } - if (ext === '.dxf') { + if (ext === ".dxf") { return [ { - id: 'dxf-native-entities', - status: 'ready', - viewer: 'cad-native-svg-entities', + id: "dxf-native-entities", + status: "ready", + viewer: "cad-native-svg-entities", sourceUrl, - worker: 'ezdxf_extract_entities', + worker: "ezdxf_extract_entities", }, ]; } - if (ext === '.dwg') { + if (ext === ".dwg") { return [ { - id: 'dwg-native-cad-vector-manifest', - status: 'ready', - viewer: 'dwg-native-adapter-to-cad-vector-entities', + id: "dwg-native-cad-vector-manifest", + status: "ready", + viewer: "dwg-native-adapter-to-cad-vector-entities", manifestUrl: `${sourceUrl}/cad-derivative?format=manifest`, }, ]; } - if (ext === '.ifc' || ext === '.ifczip') { + if (ext === ".ifc" || ext === ".ifczip") { return [ { - id: 'ifc-source-stream', - status: 'ready', - viewer: 'web-ifc-source-open', + id: "ifc-native-ifclite", + status: "ready", + viewer: "ifc-lite-webgpu-native-ifc", sourceUrl, + priority: [ + "prengine-native", + "prengine-cache", + "prengine-worker", + ], + note: "IFC 从源文件原生打开,不走 3D Tiles。", }, { - id: 'ifc-worker-cache', - status: 'ready', - worker: 'IfcOpenShell / ThatOpen fragments', + id: "ifc-worker-cache", + status: "available_for_background_cache", + worker: "Prengine 后台缓存服务", manifestUrl: `${sourceUrl}/ifc-derivative?format=manifest`, propertiesIndexUrl: `${sourceUrl}/ifc-derivative?format=properties-index`, - outputs: ['glb', 'fragments', 'tiles', 'properties-index'], - cache: 'checksum-keyed derivatives + paginated properties index', + outputs: ["model-cache", "properties-index"], + cache: "checksum-keyed background cache + paginated properties index", + note: "3D Tiles 仅用于数字孪生场景派生,不作为 IFC 原生打开入口。", }, ]; } - if (['.stl', '.step', '.stp', '.iges', '.igs'].includes(ext)) { + if ([".stl", ".step", ".stp", ".iges", ".igs"].includes(ext)) { return [ { - id: 'occt-native-open', - status: 'ready_in_worker_contract', - viewer: 'occt-native-brep-mesh-property-editor', + id: "occt-native-open", + status: "ready_in_worker_contract", + viewer: "occt-native-brep-mesh-property-editor", sourceUrl, - worker: 'occt_adapter', - outputs: ['brep', 'glb', 'properties-index'], + worker: "occt_adapter", + outputs: ["brep", "glb", "properties-index"], }, ]; } - if (ext === '.3dm') { + if (ext === ".3dm") { return [ { - id: 'rhino3dm-opennurbs-native-open', - status: 'adapter_required', - viewer: 'rhino3dm-opennurbs-source-property-editor', + id: "rhino3dm-opennurbs-native-open", + status: "adapter_required", + viewer: "rhino3dm-opennurbs-source-property-editor", sourceUrl, - worker: 'rhino3dm/opennurbs worker', - outputs: ['3dm', 'step', 'stp', 'ifc', 'glb', 'properties-index'], + worker: "rhino3dm/opennurbs worker", + outputs: ["3dm", "step", "stp", "ifc", "glb", "properties-index"], }, ]; } - if (ext === '.skp') { + if (ext === ".skp") { return [ { - id: 'sketchup-native-open', - status: 'licensed_adapter_required', - viewer: 'sketchup-speckle-blender-source-editor', + id: "sketchup-native-open", + status: "adapter_required", + viewer: "prengine-skp-true-model", sourceUrl, - worker: 'Speckle SketchUp / Blender isolated adapter / licensed SketchUp runtime', - outputs: ['skp', 'ifc', 'glb', 'obj', 'properties-index'], + manifestUrl: `${sourceUrl}/skp-derivative?format=manifest`, + worker: "Prengine 授权模型适配器", + outputs: ["model-cache", "properties-index"], }, ]; } - if (ext === '.blend') { + if (ext === ".blend") { return [ { - id: 'blender-native-open', - status: 'external_process_required', - viewer: 'blender-external-scene-service', + id: "blender-native-open", + status: "external_process_required", + viewer: "blender-external-scene-service", sourceUrl, - worker: 'Blender Python/MCP isolated service', - outputs: ['blend', 'glb', 'gltf', 'obj', 'stl', 'mp4', 'png'], + worker: "Blender Python/MCP isolated service", + outputs: ["blend", "glb", "gltf", "obj", "stl", "mp4", "png"], }, ]; } - if (['.docx', '.doc', '.xlsx', '.xls', '.pptx', '.ppt', '.odt', '.ods', '.odp'].includes(ext)) { + if ( + [ + ".docx", + ".doc", + ".xlsx", + ".xls", + ".pptx", + ".ppt", + ".odt", + ".ods", + ".odp", + ].includes(ext) + ) { return [ { - id: 'office-native-open', - status: 'ready', - viewer: 'office-document-runtime', + id: "office-native-open", + status: "ready", + viewer: "office-document-runtime", sourceUrl, - worker: 'libreoffice_headless when derivative/export is requested', + worker: "libreoffice_headless when derivative/export is requested", }, ]; } return [ { - id: 'source-stream', - status: 'ready', - viewer: 'source-bound', + id: "source-stream", + status: "ready", + viewer: "source-bound", sourceUrl, }, ]; diff --git a/03-frontend/app/api/local-files/[fileId]/preview/route.ts b/03-frontend/app/api/local-files/[fileId]/preview/route.ts index 544f8613..8031f8fc 100644 --- a/03-frontend/app/api/local-files/[fileId]/preview/route.ts +++ b/03-frontend/app/api/local-files/[fileId]/preview/route.ts @@ -1,10 +1,12 @@ // app/api/local-files/[fileId]/preview/route.ts - Frontend preview policy guard // License: Apache-2.0 +import { randomUUID } from 'node:crypto'; import { execFile } from 'node:child_process'; -import { mkdir, readFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, readdir, readFile, rm, stat } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { basename, join } from 'node:path'; +import { pathToFileURL } from 'node:url'; import { promisify } from 'node:util'; import { NextResponse } from 'next/server'; import { @@ -15,6 +17,7 @@ import { export const runtime = 'nodejs'; const execFileAsync = promisify(execFile); +const officePreviewConversions = new Map>(); export async function GET( request: Request, @@ -46,27 +49,45 @@ export async function GET( const sourcePath = resolveLocalUploadStoragePath(metadata); const outDir = join(tmpdir(), 'architoken-office-preview', metadata.fileId); await mkdir(outDir, { recursive: true }); - const binary = process.env.ARCHITOKEN_LIBREOFFICE_BIN || 'libreoffice'; - await execFileAsync(binary, [ - '--headless', - '--convert-to', - 'pdf', - '--outdir', + const outputPath = officePreviewOutputPath(outDir, sourcePath); + const cachedPdfPath = await resolveFreshConvertedPdfPath( outDir, + outputPath, sourcePath, - ], { timeout: 300_000 }); - const outputPath = join( - outDir, - `${basename(sourcePath).replace(/\.[^.]+$/, '')}.pdf`, ); - const bytes = await readFile(outputPath); - return new NextResponse(bytes, { - headers: { - 'content-type': 'application/pdf', - 'cache-control': 'no-store', - 'x-architoken-office-engine': 'libreoffice_headless', - }, - }); + if (cachedPdfPath) { + const bytes = await readFile(cachedPdfPath); + return officePdfResponse(bytes, 'hit'); + } + + const binary = process.env.ARCHITOKEN_LIBREOFFICE_BIN || 'libreoffice'; + + let conversion = officePreviewConversions.get(metadata.fileId); + if (!conversion) { + conversion = convertOfficeToPdf({ + binary, + outDir, + outputPath, + sourcePath, + }); + officePreviewConversions.set(metadata.fileId, conversion); + void conversion.then( + () => { + if (officePreviewConversions.get(metadata.fileId) === conversion) { + officePreviewConversions.delete(metadata.fileId); + } + }, + () => { + if (officePreviewConversions.get(metadata.fileId) === conversion) { + officePreviewConversions.delete(metadata.fileId); + } + }, + ); + } + + const pdfPath = await conversion; + const bytes = await readFile(pdfPath); + return officePdfResponse(bytes, 'miss'); } catch (error) { return NextResponse.json( { @@ -106,6 +127,92 @@ export async function GET( ); } +function officePreviewOutputPath(outDir: string, sourcePath: string): string { + return join(outDir, `${basename(sourcePath).replace(/\.[^.]+$/, '')}.pdf`); +} + +async function convertOfficeToPdf({ + binary, + outDir, + outputPath, + sourcePath, +}: { + binary: string; + outDir: string; + outputPath: string; + sourcePath: string; +}): Promise { + const profileDir = await mkdtemp( + join(tmpdir(), `architoken-lo-profile-${randomUUID()}-`), + ); + + try { + await execFileAsync(binary, [ + '--nologo', + '--nofirststartwizard', + '--headless', + `-env:UserInstallation=${pathToFileURL(profileDir).href}`, + '--convert-to', + 'pdf', + '--outdir', + outDir, + sourcePath, + ], { timeout: 300_000 }); + return resolveConvertedPdfPath(outDir, outputPath); + } finally { + await rm(profileDir, { recursive: true, force: true }); + } +} + +async function resolveFreshConvertedPdfPath( + outDir: string, + expectedPath: string, + sourcePath: string, +): Promise { + try { + const pdfPath = await resolveConvertedPdfPath(outDir, expectedPath); + const [sourceFile, pdfFile] = await Promise.all([ + stat(sourcePath), + stat(pdfPath), + ]); + return pdfFile.mtimeMs >= sourceFile.mtimeMs ? pdfPath : null; + } catch { + return null; + } +} + +async function resolveConvertedPdfPath( + outDir: string, + expectedPath: string, +): Promise { + try { + await stat(expectedPath); + return expectedPath; + } catch { + const entries = await readdir(outDir); + const pdf = entries.find((entry) => entry.toLowerCase().endsWith('.pdf')); + if (!pdf) throw new Error('LibreOffice did not produce a PDF file.'); + return join(outDir, pdf); + } +} + +function officePdfResponse(bytes: Buffer, cacheStatus: 'hit' | 'miss') { + const body = bytes.buffer.slice( + bytes.byteOffset, + bytes.byteOffset + bytes.byteLength, + ) as ArrayBuffer; + + return new NextResponse(body, { + headers: { + 'content-type': 'application/pdf', + 'cache-control': 'no-store', + 'x-architoken-office-engine': 'libreoffice_headless', + 'x-architoken-preview-engine': 'Prengine Office PDF adapter', + 'x-architoken-preview-cache': cacheStatus, + }, + }); +} + function isOfficeFile(ext: string, mimeType: string): boolean { const normalizedExt = ext.toLowerCase(); const normalizedMime = mimeType.toLowerCase(); diff --git a/03-frontend/app/api/local-files/[fileId]/skp-derivative/route.ts b/03-frontend/app/api/local-files/[fileId]/skp-derivative/route.ts new file mode 100644 index 00000000..717db02e --- /dev/null +++ b/03-frontend/app/api/local-files/[fileId]/skp-derivative/route.ts @@ -0,0 +1,127 @@ +// app/api/local-files/[fileId]/skp-derivative/route.ts - Licensed SKP derivative endpoint +// License: Apache-2.0 + +import { NextResponse } from 'next/server'; +import { + buildSkpDerivativeManifest, + readSkpDerivativeBytes, + SkpDerivativeError, + type SkpDerivativeFormat, +} from '@/lib/skp-derivative-server'; + +export const runtime = 'nodejs'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ fileId: string }> }, +) { + const { fileId } = await params; + const url = new URL(request.url); + const format = normalizeFormat(url.searchParams.get('format')); + + try { + if (format === 'manifest') { + const manifest = await buildSkpDerivativeManifest(fileId); + if (request.headers.get('if-none-match') === manifest.etag) { + return new Response(null, { + status: 304, + headers: manifestHeaders(manifest), + }); + } + return NextResponse.json(manifest, { headers: manifestHeaders(manifest) }); + } + + const derivative = await readSkpDerivativeBytes(fileId, format); + if (request.headers.get('if-none-match') === derivative.etag) { + return new Response(null, { + status: 304, + headers: derivativeHeaders(derivative), + }); + } + + const range = parseRangeHeader( + request.headers.get('range'), + derivative.bytes.byteLength, + ); + const payload = range + ? derivative.bytes.subarray(range.start, range.end + 1) + : derivative.bytes; + const body = new Uint8Array(payload.byteLength); + body.set(payload); + return new Response(body, { + status: range ? 206 : 200, + headers: { + ...derivativeHeaders(derivative), + 'content-length': String(payload.byteLength), + 'content-disposition': `inline; filename*=UTF-8''${encodeURIComponent(derivative.fileName)}`, + ...(range + ? { + 'content-range': `bytes ${range.start}-${range.end}/${derivative.bytes.byteLength}`, + } + : {}), + }, + }); + } catch (error) { + if (error instanceof SkpDerivativeError) { + return NextResponse.json( + { + error: error.code, + message: error.message, + ...error.details, + }, + { status: error.status }, + ); + } + return NextResponse.json( + { + error: 'skp_derivative_failed', + message: error instanceof Error ? error.message : String(error), + }, + { status: 500 }, + ); + } +} + +function normalizeFormat(value: string | null): SkpDerivativeFormat { + return value === 'glb' || value === 'manifest' ? value : 'manifest'; +} + +function manifestHeaders(manifest: { etag: string; fileId: string }) { + return { + etag: manifest.etag, + 'cache-control': 'private, max-age=0, must-revalidate', + 'x-architoken-file-id': manifest.fileId, + 'x-architoken-cache-contract': 'stream+etag+checksum', + }; +} + +function derivativeHeaders(derivative: { + mediaType: string; + engine: string; + etag: string; + cacheHit: boolean; +}) { + return { + 'content-type': derivative.mediaType, + etag: derivative.etag, + 'cache-control': 'private, max-age=0, must-revalidate', + 'accept-ranges': 'bytes', + 'x-architoken-skp-engine': derivative.engine, + 'x-architoken-cache-hit': String(derivative.cacheHit), + }; +} + +function parseRangeHeader( + header: string | null, + size: number, +): { start: number; end: number } | null { + if (!header?.startsWith('bytes=')) return null; + const [startRaw, endRaw] = header.slice('bytes='.length).split('-', 2); + const start = Number.parseInt(startRaw ?? '', 10); + const requestedEnd = Number.parseInt(endRaw ?? '', 10); + if (!Number.isFinite(start) || start < 0 || start >= size) return null; + const end = Number.isFinite(requestedEnd) + ? Math.min(requestedEnd, size - 1) + : size - 1; + return end >= start ? { start, end } : null; +} diff --git a/03-frontend/app/api/openclaw/ui/[[...path]]/route.ts b/03-frontend/app/api/openclaw/ui/[[...path]]/route.ts new file mode 100644 index 00000000..da63ee60 --- /dev/null +++ b/03-frontend/app/api/openclaw/ui/[[...path]]/route.ts @@ -0,0 +1,153 @@ +// app/api/openclaw/ui/[[...path]]/route.ts - Same-origin proxy for OpenClaw Control UI +// License: Apache-2.0 + +import { NextResponse } from 'next/server'; + +export const dynamic = 'force-dynamic'; +export const runtime = 'nodejs'; + +const defaultOpenClawHttpBase = 'http://127.0.0.1:18789'; + +type RouteContext = { + params: Promise<{ + path?: string[]; + }>; +}; + +export async function GET(request: Request, context: RouteContext) { + return proxyOpenClawUi(request, context); +} + +export async function HEAD(request: Request, context: RouteContext) { + return proxyOpenClawUi(request, context, true); +} + +async function proxyOpenClawUi( + request: Request, + { params }: RouteContext, + headOnly = false, +) { + const { path = [] } = await params; + const requestUrl = new URL(request.url); + const upstreamUrl = new URL(path.join('/') || '/', normalizedBaseUrl(openClawHttpBase())); + upstreamUrl.search = requestUrl.search; + + const upstream = await fetch(upstreamUrl, { + method: headOnly ? 'HEAD' : 'GET', + headers: { + Accept: request.headers.get('accept') ?? '*/*', + 'User-Agent': request.headers.get('user-agent') ?? 'ArchIToken OpenClaw UI proxy', + }, + cache: 'no-store', + }); + + if (headOnly) { + return new NextResponse(null, { + status: upstream.status, + headers: buildProxyHeaders(upstream.headers, false), + }); + } + + const contentType = upstream.headers.get('content-type') ?? ''; + if (contentType.includes('text/html')) { + const html = await upstream.text(); + return new NextResponse(injectOpenClawControlContext(html, requestUrl), { + status: upstream.status, + headers: buildProxyHeaders(upstream.headers, true), + }); + } + + return new NextResponse(await upstream.arrayBuffer(), { + status: upstream.status, + headers: buildProxyHeaders(upstream.headers, false), + }); +} + +function openClawHttpBase(): string { + return process.env.OPENCLAW_DASHBOARD_URL + ?? process.env.OPENCLAW_GATEWAY_HTTP_URL + ?? defaultOpenClawHttpBase; +} + +function openClawWsUrl(requestUrl: URL): string { + if (process.env.OPENCLAW_GATEWAY_WS_URL) { + return process.env.OPENCLAW_GATEWAY_WS_URL; + } + + const baseUrl = new URL(openClawHttpBase()); + const protocol = baseUrl.protocol === 'https:' ? 'wss:' : 'ws:'; + const host = baseUrl.host || requestUrl.host; + return `${protocol}//${host}`; +} + +function injectOpenClawControlContext(html: string, requestUrl: URL): string { + const moduleId = requestUrl.searchParams.get('architokenModule') ?? 'construction_management'; + const moduleName = requestUrl.searchParams.get('architokenModuleName') ?? moduleId; + const selectedFeature = requestUrl.searchParams.get('architokenFeature') ?? ''; + const locale = 'zh-CN'; + const safeModuleId = moduleId.replace(/[^a-z0-9_-]+/gi, '-'); + const sessionKey = `agent:dev:architoken-${safeModuleId}-main`; + const settings = { + gatewayUrl: openClawWsUrl(requestUrl), + sessionKey, + lastActiveSessionKey: sessionKey, + theme: 'claw', + themeMode: 'dark', + chatFocusMode: true, + chatShowThinking: true, + chatShowToolCalls: true, + splitRatio: 0.6, + navCollapsed: false, + }; + const injectedState = JSON.stringify({ + settings, + moduleId, + moduleName, + selectedFeature, + locale, + }).replace(/ + `; + + return html.replace('', `${bootstrap}`); +} + +function buildProxyHeaders(upstreamHeaders: Headers, html: boolean): Headers { + const headers = new Headers(); + const contentType = upstreamHeaders.get('content-type'); + const cacheControl = upstreamHeaders.get('cache-control'); + if (contentType) headers.set('content-type', contentType); + headers.set('cache-control', cacheControl ?? 'no-store'); + headers.set('referrer-policy', 'no-referrer'); + headers.set('x-content-type-options', 'nosniff'); + if (html) { + headers.set( + 'content-security-policy', + "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; connect-src 'self' ws: wss:", + ); + } + return headers; +} + +function normalizedBaseUrl(baseUrl: string): string { + return baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`; +} diff --git a/03-frontend/app/api/paperclip/ui/[[...path]]/route.ts b/03-frontend/app/api/paperclip/ui/[[...path]]/route.ts new file mode 100644 index 00000000..f79f6538 --- /dev/null +++ b/03-frontend/app/api/paperclip/ui/[[...path]]/route.ts @@ -0,0 +1,299 @@ +// app/api/paperclip/ui/[[...path]]/route.ts - Same-origin proxy for Paperclip UI +// License: Apache-2.0 + +import { NextResponse } from 'next/server'; + +export const dynamic = 'force-dynamic'; +export const runtime = 'nodejs'; + +const defaultPaperclipHttpBase = 'http://127.0.0.1:3111'; +const paperclipProxyBase = '/api/paperclip/ui'; +const hopByHopHeaders = new Set([ + 'connection', + 'content-encoding', + 'content-length', + 'keep-alive', + 'transfer-encoding', + 'upgrade', +]); + +type RouteContext = { + params: Promise<{ + path?: string[]; + }>; +}; + +export async function GET(request: Request, context: RouteContext) { + return proxyPaperclipUi(request, context); +} + +export async function HEAD(request: Request, context: RouteContext) { + return proxyPaperclipUi(request, context, true); +} + +export async function POST(request: Request, context: RouteContext) { + return proxyPaperclipUi(request, context); +} + +export async function PUT(request: Request, context: RouteContext) { + return proxyPaperclipUi(request, context); +} + +export async function PATCH(request: Request, context: RouteContext) { + return proxyPaperclipUi(request, context); +} + +export async function DELETE(request: Request, context: RouteContext) { + return proxyPaperclipUi(request, context); +} + +export async function OPTIONS() { + return new NextResponse(null, { + status: 204, + headers: buildCorsHeaders(), + }); +} + +async function proxyPaperclipUi( + request: Request, + { params }: RouteContext, + headOnly = false, +) { + const { path = [] } = await params; + const requestUrl = new URL(request.url); + const upstreamUrl = new URL(path.join('/') || '/', normalizedBaseUrl(paperclipHttpBase())); + upstreamUrl.search = requestUrl.search; + + let upstream: Response; + try { + const init: RequestInit & { duplex?: 'half' } = { + method: headOnly ? 'HEAD' : request.method, + headers: buildUpstreamHeaders(request), + cache: 'no-store', + redirect: 'manual', + }; + if (!headOnly && !['GET', 'HEAD'].includes(request.method)) { + init.body = await request.arrayBuffer(); + init.duplex = 'half'; + } + upstream = await fetch(upstreamUrl, init); + } catch (error) { + return paperclipUnavailableResponse(formatError(error)); + } + + if (headOnly) { + return new NextResponse(null, { + status: upstream.status, + headers: buildProxyHeaders(upstream.headers, false, false), + }); + } + + const contentType = upstream.headers.get('content-type') ?? ''; + if (contentType.includes('text/html')) { + const html = await upstream.text(); + return new NextResponse(injectPaperclipContext(rewritePaperclipHtml(html), requestUrl), { + status: upstream.status, + headers: buildProxyHeaders(upstream.headers, true, true), + }); + } + + if (isJavaScriptContent(contentType)) { + return new NextResponse(rewritePaperclipJavaScript(await upstream.text()), { + status: upstream.status, + headers: buildProxyHeaders(upstream.headers, false, true), + }); + } + + if (contentType.includes('text/css')) { + return new NextResponse(rewritePaperclipCss(await upstream.text()), { + status: upstream.status, + headers: buildProxyHeaders(upstream.headers, false, true), + }); + } + + return new NextResponse(await upstream.arrayBuffer(), { + status: upstream.status, + headers: buildProxyHeaders(upstream.headers, false, false), + }); +} + +function paperclipHttpBase(): string { + return process.env.PAPERCLIP_DASHBOARD_URL + ?? process.env.PAPERCLIP_HTTP_URL + ?? defaultPaperclipHttpBase; +} + +function rewritePaperclipHtml(html: string): string { + return html + .replaceAll('src="/assets/', `src="${paperclipProxyBase}/assets/`) + .replaceAll('href="/assets/', `href="${paperclipProxyBase}/assets/`) + .replaceAll('href="/favicon', `href="${paperclipProxyBase}/favicon`) + .replaceAll('href="/apple-touch-icon', `href="${paperclipProxyBase}/apple-touch-icon`) + .replaceAll('href="/site.webmanifest"', `href="${paperclipProxyBase}/site.webmanifest"`); +} + +function rewritePaperclipJavaScript(source: string): string { + return source + .replaceAll('"/api/', `"${paperclipProxyBase}/api/`) + .replaceAll("'/api/", `'${paperclipProxyBase}/api/`) + .replaceAll('`/api/', `\`${paperclipProxyBase}/api/`) + .replaceAll('"/assets/', `"${paperclipProxyBase}/assets/`) + .replaceAll("'/assets/", `'${paperclipProxyBase}/assets/`) + .replaceAll('`/assets/', `\`${paperclipProxyBase}/assets/`) + .replaceAll('"assets/', `"${paperclipProxyBase}/assets/`) + .replaceAll("'assets/", `'${paperclipProxyBase}/assets/`) + .replaceAll('`assets/', `\`${paperclipProxyBase}/assets/`) + .replaceAll('("/sw.js")', `("${paperclipProxyBase}/sw.js")`) + .replaceAll("('/sw.js')", `('${paperclipProxyBase}/sw.js')`) + .replaceAll('(`/sw.js`)', `(\`${paperclipProxyBase}/sw.js\`)`); +} + +function rewritePaperclipCss(source: string): string { + return source + .replaceAll('url(/assets/', `url(${paperclipProxyBase}/assets/`) + .replaceAll('url("/assets/', `url("${paperclipProxyBase}/assets/`) + .replaceAll("url('/assets/", `url('${paperclipProxyBase}/assets/`); +} + +function isJavaScriptContent(contentType: string): boolean { + return contentType.includes('javascript') + || contentType.includes('ecmascript') + || contentType.includes('application/x-javascript'); +} + +function injectPaperclipContext(html: string, requestUrl: URL): string { + const moduleId = requestUrl.searchParams.get('architokenModule') ?? 'production_manufacturing'; + const moduleName = requestUrl.searchParams.get('architokenModuleName') ?? '生产制造'; + const release = requestUrl.searchParams.get('architokenRelease') ?? 'v2026.517.0'; + const injectedState = JSON.stringify({ + moduleId, + moduleName, + release, + locale: 'zh-CN', + boundaries: [ + 'Paperclip controls this production module surface.', + 'ArchIToken remains source of truth for CDE files, CNC/QC/MES/ERP evidence and professional approvals.', + ], + }).replace(/ + `; + + if (html.includes('')) { + return html.replace('', `${bootstrap}`); + } + return `${bootstrap}${html}`; +} + +function buildUpstreamHeaders(request: Request): Headers { + const headers = new Headers(); + const requestUrl = new URL(request.url); + const host = request.headers.get('host') ?? requestUrl.host; + const accept = request.headers.get('accept'); + const contentType = request.headers.get('content-type'); + const cookie = request.headers.get('cookie'); + const origin = request.headers.get('origin'); + const referer = request.headers.get('referer'); + if (accept) headers.set('accept', accept); + if (contentType) headers.set('content-type', contentType); + if (cookie) headers.set('cookie', cookie); + if (origin) headers.set('origin', origin); + if (referer) headers.set('referer', referer); + headers.set('x-forwarded-host', host); + headers.set('x-forwarded-proto', requestUrl.protocol.replace(':', '')); + headers.set('user-agent', request.headers.get('user-agent') ?? 'ArchIToken Paperclip UI proxy'); + return headers; +} + +function buildProxyHeaders(upstreamHeaders: Headers, html: boolean, transformed: boolean): Headers { + const headers = buildCorsHeaders(); + upstreamHeaders.forEach((value, key) => { + if (!hopByHopHeaders.has(key.toLowerCase())) { + headers.set(key, value); + } + }); + if (transformed) { + headers.delete('etag'); + headers.delete('last-modified'); + headers.set('cache-control', 'no-store'); + } else { + headers.set('cache-control', upstreamHeaders.get('cache-control') ?? 'no-store'); + } + headers.set('referrer-policy', 'no-referrer'); + headers.set('x-content-type-options', 'nosniff'); + if (html) { + headers.set( + 'content-security-policy', + "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; connect-src 'self' http: https: ws: wss:", + ); + } + return headers; +} + +function buildCorsHeaders(): Headers { + const headers = new Headers(); + headers.set('access-control-allow-methods', 'GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS'); + headers.set('access-control-allow-headers', 'content-type,authorization'); + return headers; +} + +function paperclipUnavailableResponse(detail: string): NextResponse { + const body = ` + + + + + Paperclip 未连接 + + + +
    +
    +

    Paperclip v2026.517.0 未连接

    +

    生产制造模块已完整切换为 Paperclip 控制台。请启动 Paperclip 服务或设置 PAPERCLIP_DASHBOARD_URL / PAPERCLIP_HTTP_URL。默认地址为 ${escapeHtml(defaultPaperclipHttpBase)}

    +

    连接错误: ${escapeHtml(detail)}

    +
    +
    + +`; + return new NextResponse(body, { + status: 502, + headers: { + 'content-type': 'text/html; charset=utf-8', + 'cache-control': 'no-store', + }, + }); +} + +function normalizedBaseUrl(baseUrl: string): string { + return baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`; +} + +function formatError(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + +function escapeHtml(value: string): string { + return value + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); +} diff --git a/03-frontend/app/globals.css b/03-frontend/app/globals.css index 3e70b2a1..bec14673 100644 --- a/03-frontend/app/globals.css +++ b/03-frontend/app/globals.css @@ -596,6 +596,66 @@ html[data-resolved-theme="huly_dark"] border-radius: 7px !important; } +.arch-module-home .arch-concept-studio-home { + background: var(--arch-bg); + color: var(--arch-text); +} + +.arch-module-home .arch-concept-studio-home :where(h1, h2, h3, h4, h5, h6) { + letter-spacing: inherit; +} + +.arch-module-home .arch-concept-studio-home :where(h2) { + font-size: var(--text-micro); + line-height: var(--text-micro--line-height); +} + +.arch-module-home .arch-concept-studio-home :where(h3) { + font-size: var(--text-h4); + line-height: 1.4; +} + +.arch-module-home .arch-concept-create-flow :where(.max-w-landing) { + max-width: none !important; +} + +.arch-module-home .arch-concept-create-flow :where(.bg-fg-0) { + background: var(--arch-bg) !important; +} + +.arch-module-home .arch-concept-create-flow :where(.bg-fg-1) { + background: var(--arch-surface) !important; +} + +.arch-module-home .arch-concept-create-flow :where(.bg-fg-2) { + background: var(--arch-surface-muted) !important; +} + +.arch-module-home .arch-concept-create-flow :where(.border-fg-2) { + border-color: var(--arch-border) !important; +} + +.arch-module-home .arch-concept-create-flow :where(.text-fg-8, .text-fg-9, .text-fg-0) { + color: var(--arch-text) !important; +} + +.arch-module-home .arch-concept-create-flow :where(.text-fg-3, .text-fg-4, .text-fg-5) { + color: var(--arch-text-muted) !important; +} + +.arch-module-home .arch-concept-create-flow :where(.border-accent-lime) { + border-color: var(--module-accent) !important; +} + +.arch-module-home .arch-concept-create-flow :where(.bg-accent-lime) { + background: var(--module-accent) !important; + color: var(--module-accent-foreground) !important; +} + +.arch-module-home .arch-concept-create-flow :where(.text-accent-lime) { + color: var(--module-accent) !important; +} + .arch-module-eyebrow { font-size: var(--arch-type-caption); line-height: 1.35; @@ -917,9 +977,9 @@ html[data-resolved-theme="huly_dark"] .arch-huly-capture-shell { display: grid; - grid-template-columns: minmax(0, 1fr) 286px; + grid-template-columns: minmax(0, 1fr); gap: 0.75rem; - width: min(100%, 1260px); + width: 100%; padding: 0.75rem 0.75rem 0.5rem; } @@ -935,6 +995,123 @@ html[data-resolved-theme="huly_dark"] padding: 0; } +.arch-huly-workflow-head, +.arch-huly-selected-panel, +.arch-huly-step-card { + scroll-margin-top: 0.75rem; +} + +.arch-huly-workflow-head { + padding-bottom: 0.8rem; +} + +.arch-huly-selected-panel { + min-width: 0; +} + +.arch-huly-step-card { + border-top: 1px solid var(--arch-border); + padding-top: 0.9rem; +} + +.arch-huly-entry-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 0.75rem; + margin: 0.1rem 0 1rem; +} + +.arch-planning-entry-grid { + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: 0.75rem; + min-width: 0; + max-width: 100%; + overflow: hidden; +} + +.arch-huly-entry-card { + display: grid; + grid-template-columns: 38px minmax(0, 1fr); + gap: 0.75rem; + align-items: start; + min-height: 104px; + border: 1px solid var(--arch-border); + border-radius: 7px; + background: var(--arch-surface); + color: var(--arch-text); + padding: 0.85rem; + text-align: left; + transition: + border-color 0.16s ease, + background 0.16s ease, + transform 0.16s ease; +} + +.arch-huly-entry-card:hover { + border-color: var(--arch-primary); + background: var(--arch-primary-soft); + transform: translateY(-1px); +} + +.arch-huly-entry-card.is-active { + border-color: var(--arch-rich-orange); + background: var(--arch-rich-orange-soft); +} + +.arch-huly-entry-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + border: 1px solid var(--arch-border); + border-radius: 7px; + background: var(--arch-surface-muted); + color: var(--arch-primary); +} + +.arch-huly-entry-card.is-active .arch-huly-entry-icon { + border-color: rgba(244, 81, 30, 0.28); + background: rgba(244, 81, 30, 0.12); + color: var(--arch-rich-orange); +} + +.arch-huly-entry-text { + display: grid; + min-width: 0; + gap: 0.25rem; +} + +.arch-huly-entry-text strong { + color: var(--arch-text); + font-size: 13px; + font-weight: 600; + line-height: 1.35; +} + +.arch-huly-entry-text small { + color: var(--arch-text-muted); + font-size: 11px; + line-height: 1.45; +} + +.arch-huly-reference-upload { + min-width: 0; +} + +.arch-huly-empty-step { + display: flex; + min-height: 180px; + align-items: center; + justify-content: space-between; + gap: 1rem; + border: 1px dashed var(--arch-border); + border-radius: 7px; + background: var(--arch-surface); + padding: 1rem; +} + .arch-huly-section-head { display: flex; align-items: flex-start; @@ -1148,12 +1325,1056 @@ html[data-resolved-theme="huly_dark"] grid-template-columns: 1fr; } + .arch-huly-entry-grid, + .arch-planning-entry-grid { + grid-template-columns: 1fr; + } + .arch-huly-form-actions { align-items: stretch; flex-direction: column; } } +@media (min-width: 1600px) { + .arch-planning-entry-grid { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } +} + +.feichuan-engine { + display: flex; + height: calc(100vh - 9rem); + min-height: calc(100vh - 9rem); + width: min(100%, calc(100vw - 330px)); + min-width: 0; + flex-direction: column; + overflow: hidden; + background: #fff; + color: #0f172a; +} + +.feichuan-engine-toolbar, +.feichuan-engine-switch { + display: flex; + min-width: 0; + align-items: center; + justify-content: space-between; + gap: 1rem; + border-bottom: 1px solid #e5e7eb; + background: #fff; +} + +.feichuan-engine-toolbar { + min-height: 50px; + padding: 0.45rem 0.35rem; +} + +.feichuan-engine-switch { + min-height: 34px; + padding: 0 0.35rem; +} + +.feichuan-engine-title, +.feichuan-engine-actions { + display: flex; + min-width: 0; + align-items: center; + gap: 0.7rem; + white-space: nowrap; +} + +.feichuan-engine-title strong { + font-size: 14px; + font-weight: 600; +} + +.feichuan-engine-title span, +.feichuan-engine-actions span { + color: #64748b; + font-size: 12px; +} + +.feichuan-mode-tabs, +.feichuan-scale-group { + display: inline-flex; + flex: 0 0 auto; + align-items: center; + border-radius: 4px; + background: #f2f4f7; + padding: 2px; +} + +.feichuan-mode-tabs button, +.feichuan-scale-group button { + height: 24px; + min-width: 38px; + border: 0; + border-radius: 3px; + background: transparent; + color: #0f172a; + cursor: pointer; + font-size: 12px; + line-height: 24px; + padding: 0 0.75rem; + white-space: nowrap; +} + +.feichuan-mode-tabs button { + min-width: 68px; +} + +.feichuan-mode-tabs button:hover, +.feichuan-scale-group button:hover { + color: #1677ff; +} + +.feichuan-mode-tabs button.is-active, +.feichuan-scale-group button.is-active { + background: #2f7df6; + color: #fff; + box-shadow: 0 1px 2px rgba(47, 125, 246, 0.22); +} + +.feichuan-scale-group.is-compact button { + min-width: 34px; + padding: 0 0.55rem; +} + +.feichuan-date-range { + display: inline-flex; + height: 26px; + min-width: 250px; + align-items: center; + justify-content: center; + gap: 0.55rem; + border: 1px solid #2f7df6; + border-radius: 2px; + background: #fff; + color: #0f172a; + font-size: 12px; + padding: 0 0.65rem; +} + +.feichuan-date-range input { + width: 96px; + border: 0; + background: transparent; + color: #0f172a; + font-size: 12px; + outline: 0; +} + +.feichuan-date-range i { + width: 14px; + height: 1px; + background: #94a3b8; +} + +.feichuan-inline-editor { + display: grid; + grid-template-columns: auto minmax(220px, 1fr) 104px 104px minmax(160px, 220px) 96px repeat(5, auto); + gap: 0.5rem; + align-items: center; + border-bottom: 1px solid #e5e7eb; + background: #fbfdff; + padding: 0.45rem 0.5rem; +} + +.feichuan-control-strip { + display: flex; + min-height: 34px; + align-items: center; + gap: 0.45rem; + overflow-x: auto; + border-bottom: 1px solid #e5e7eb; + background: #fff; + color: #0f172a; + padding: 0.35rem 0.5rem; + white-space: nowrap; +} + +.feichuan-control-strip span { + display: inline-flex; + min-height: 24px; + align-items: center; + gap: 0.35rem; + border: 1px solid #d7dce2; + border-radius: 4px; + background: #f8fafc; + color: #334155; + font-size: 12px; + padding: 0 0.55rem; +} + +.feichuan-control-strip span.is-wide { + min-width: 280px; +} + +.feichuan-control-strip b { + color: #64748b; + font-weight: 600; +} + +.feichuan-control-strip span.is-warning { + border-color: #f59e0b; + background: #fffbeb; + color: #92400e; +} + +.feichuan-control-strip span.is-danger { + border-color: #ef4444; + background: #fff1f2; + color: #b91c1c; +} + +.feichuan-inline-editor strong { + color: #0f172a; + font-size: 12px; + font-weight: 600; + white-space: nowrap; +} + +.feichuan-inline-editor input, +.feichuan-inline-editor select { + min-width: 0; + height: 28px; + border: 1px solid #d7dce2; + border-radius: 4px; + background: #fff; + color: #0f172a; + font-size: 12px; + outline: 0; + padding: 0 0.45rem; +} + +.feichuan-inline-editor label { + display: grid; + min-width: 0; + grid-template-columns: auto minmax(90px, 1fr); + gap: 0.4rem; + align-items: center; + color: #64748b; + font-size: 11px; +} + +.feichuan-inline-editor label input { + padding: 0; +} + +.feichuan-inline-editor button { + height: 28px; + border: 1px solid #d7dce2; + border-radius: 4px; + background: #fff; + color: #0f172a; + cursor: pointer; + font-size: 12px; + padding: 0 0.55rem; + white-space: nowrap; +} + +.feichuan-inline-editor button:hover { + border-color: #2f7df6; + color: #2f7df6; +} + +.feichuan-inline-editor button:disabled { + cursor: not-allowed; + opacity: 0.45; +} + +.feichuan-gantt, +.feichuan-network { + display: grid; + min-height: 0; + flex: 1; + grid-template-columns: 476px minmax(0, 1fr); + overflow: hidden; + background: #fff; +} + +.feichuan-task-pane { + display: flex; + min-height: 0; + flex-direction: column; + border-right: 1px solid #d7dce2; + background: #f5f8fb; +} + +.feichuan-task-header { + display: grid; + height: 58px; + grid-template-columns: 1fr 96px 108px; + align-items: center; + border-bottom: 1px solid #d7dce2; + background: #fff; + color: #0f172a; + font-size: 13px; + font-weight: 600; +} + +.feichuan-task-header span { + padding: 0 1.25rem; +} + +.feichuan-task-list { + min-height: 0; + flex: 1; + overflow-y: auto; + padding: 0.55rem 0.55rem 0.9rem; +} + +.feichuan-task-row { + display: grid; + width: 100%; + min-height: 45px; + grid-template-columns: 1fr 96px 108px; + align-items: center; + border: 1px solid #e8edf3; + border-radius: 4px; + background: #fff; + box-shadow: 0 1px 5px rgba(15, 23, 42, 0.08); + color: #0f172a; + font-size: 12px; + text-align: left; +} + +.feichuan-task-row + .feichuan-task-row { + margin-top: 0.55rem; +} + +.feichuan-task-row:hover, +.feichuan-task-row.is-selected { + border-color: #2f7df6; + box-shadow: 0 0 0 1px rgba(47, 125, 246, 0.18); +} + +.feichuan-task-row > span { + min-width: 0; + overflow: hidden; + padding: 0 0.8rem; + text-overflow: ellipsis; + white-space: nowrap; +} + +.feichuan-task-row > span:first-child { + display: flex; + align-items: center; + gap: 0.45rem; + color: #0f172a; +} + +.feichuan-task-row .anticon-down { + color: #0f172a; + font-size: 10px; + transition: transform 0.16s ease; +} + +.feichuan-task-row .anticon-down.is-collapsed { + transform: rotate(-90deg); +} + +.feichuan-task-row i { + display: inline-block; + width: 10px; + height: 10px; + border-radius: 50%; + background: #2f7df6; + transform: scale(0.45); +} + +.feichuan-task-row .is-complete { + color: #ff4d4f; +} + +.feichuan-task-footer { + display: flex; + gap: 0.7rem; + border-top: 1px solid #d7dce2; + background: #fff; + padding: 0.65rem; +} + +.feichuan-task-editor { + display: grid; + flex: 0 0 auto; + gap: 0.55rem; + border-top: 1px solid #d7dce2; + background: #fff; + padding: 0.7rem; +} + +.feichuan-task-editor.is-empty { + color: #94a3b8; + font-size: 12px; +} + +.feichuan-editor-title { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; +} + +.feichuan-editor-title strong { + color: #0f172a; + font-size: 13px; + font-weight: 600; +} + +.feichuan-editor-title span { + color: #94a3b8; + font-size: 11px; +} + +.feichuan-task-editor label { + display: grid; + min-width: 0; + gap: 0.25rem; + color: #64748b; + font-size: 11px; +} + +.feichuan-task-editor input, +.feichuan-task-editor select { + min-width: 0; + height: 28px; + border: 1px solid #d7dce2; + border-radius: 4px; + background: #fff; + color: #0f172a; + font-size: 12px; + outline: 0; + padding: 0 0.45rem; +} + +.feichuan-task-editor input[type="range"] { + padding: 0; +} + +.feichuan-editor-grid { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + gap: 0.5rem; +} + +.feichuan-editor-actions { + display: flex; + gap: 0.45rem; +} + +.feichuan-editor-actions button, +.feichuan-diagram-toolbar button { + height: 28px; + border: 1px solid #d7dce2; + border-radius: 4px; + background: #fff; + color: #0f172a; + cursor: pointer; + font-size: 12px; + padding: 0 0.65rem; +} + +.feichuan-editor-actions button:hover, +.feichuan-diagram-toolbar button:hover { + border-color: #2f7df6; + color: #2f7df6; +} + +.feichuan-editor-actions button:disabled { + cursor: not-allowed; + opacity: 0.45; +} + +.feichuan-stage, +.feichuan-network-stage { + position: relative; + display: flex; + min-width: 0; + min-height: 0; + flex-direction: column; + overflow: hidden; + background: #fff; +} + +.feichuan-stage-scroll { + position: relative; + min-width: 0; + min-height: 0; + flex: 1; + overflow: auto; +} + +.feichuan-stage-inner { + position: relative; + min-width: 100%; + background: #fff; +} + +.feichuan-timeline-header { + position: sticky; + top: 0; + z-index: 20; + height: 58px; + flex: 0 0 auto; + border-bottom: 1px solid #d7dce2; + background: #fff; + color: #7c8794; +} + +.feichuan-timeline-header > div { + position: relative; + height: 29px; +} + +.feichuan-timeline-header span { + position: absolute; + display: flex; + height: 29px; + align-items: center; + justify-content: center; + border-left: 1px solid #d7dce2; + font-size: 12px; +} + +.feichuan-timeline-header small { + margin-left: 0.25rem; + color: #94a3b8; + font-size: 10px; +} + +.feichuan-grid-column { + position: absolute; + top: 0; + bottom: 0; + border-left: 1px solid #d7dce2; +} + +.feichuan-grid-column.is-muted { + background: rgba(224, 232, 240, 0.34); +} + +.feichuan-link-layer { + position: absolute; + inset: 0; + overflow: visible; + pointer-events: none; +} + +.feichuan-network-svg { + position: absolute; + inset: 0; + overflow: visible; + pointer-events: auto; +} + +.feichuan-network-node { + cursor: pointer; +} + +.feichuan-network-hitbox { + pointer-events: all; +} + +.feichuan-network-node:hover .feichuan-svg-label, +.feichuan-network-node.is-active .feichuan-svg-label { + fill: #1677ff; +} + +.feichuan-network-node.is-active circle { + stroke: #2f7df6; + stroke-width: 2; +} + +.feichuan-network-node.is-critical circle { + stroke: #ef4444; + stroke-width: 2; +} + +.feichuan-diagram-stage { + display: flex; + min-width: 0; + min-height: 0; + flex-direction: column; + overflow: hidden; + background: + radial-gradient(circle, rgba(148, 163, 184, 0.22) 1px, transparent 1px) 0 0 / 18px 18px, + #f8fafc; +} + +.feichuan-diagram-toolbar { + display: flex; + min-height: 42px; + align-items: center; + gap: 0.65rem; + border-bottom: 1px solid #d7dce2; + background: rgba(255, 255, 255, 0.92); + color: #0f172a; + font-size: 12px; + padding: 0 0.8rem; +} + +.feichuan-diagram-toolbar strong { + font-size: 13px; + font-weight: 600; +} + +.feichuan-diagram-toolbar span { + color: #64748b; + margin-right: auto; +} + +.feichuan-diagram-svg { + display: block; + min-width: 100%; +} + +.feichuan-diagram-svg path { + stroke: #8b95a3; + stroke-width: 1.5; +} + +.feichuan-diagram-svg path.is-dependency { + stroke-dasharray: 5 5; + stroke: #f59e0b; +} + +.feichuan-diagram-node { + display: grid; + width: 100%; + height: 100%; + align-content: center; + gap: 0.15rem; + border: 1px solid #7bb0ff; + border-left: 5px solid #2f7df6; + border-radius: 7px; + background: #fff; + box-shadow: 0 4px 12px rgba(15, 23, 42, 0.1); + color: #0f172a; + cursor: pointer; + padding: 0.45rem 0.6rem; + text-align: left; +} + +.feichuan-diagram-node:hover { + border-color: #1677ff; + box-shadow: 0 0 0 2px rgba(47, 125, 246, 0.14), 0 8px 18px rgba(15, 23, 42, 0.13); +} + +.feichuan-diagram-node.is-mindmap { + border-radius: 999px; + padding-left: 0.85rem; +} + +.feichuan-diagram-node.is-active { + border-color: #2f7df6; + box-shadow: 0 0 0 2px rgba(47, 125, 246, 0.18), 0 8px 18px rgba(15, 23, 42, 0.16); +} + +.feichuan-diagram-node.is-ahead { + border-left-color: #12c86b; +} + +.feichuan-diagram-node.is-warning { + border-left-color: #ff9f2e; +} + +.feichuan-diagram-node.is-delayed { + border-left-color: #ef4444; +} + +.feichuan-diagram-node.is-future { + border-left-color: #9cccf5; +} + +.feichuan-diagram-node strong { + overflow: hidden; + font-size: 12px; + font-weight: 600; + text-overflow: ellipsis; + white-space: nowrap; +} + +.feichuan-diagram-node small, +.feichuan-diagram-node span { + overflow: hidden; + color: #64748b; + font-size: 10px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.feichuan-bar-row { + position: absolute; + left: 0; + width: 100%; + height: 30px; + pointer-events: none; +} + +.feichuan-bar-row.is-active { + background: rgba(15, 23, 42, 0.025); +} + +.feichuan-task-bar { + position: absolute; + top: 1px; + display: block; + min-width: 20px; + height: 26px; + border: 0; + border-radius: 0; + background: #c8e2fb; + cursor: ew-resize; + overflow: visible; + pointer-events: auto; + text-align: left; + touch-action: none; + user-select: none; +} + +.feichuan-task-bar:active { + cursor: grabbing; +} + +.feichuan-task-bar.is-ahead { + background: #bdf6cb; +} + +.feichuan-task-bar.is-warning { + background: #fdecc6; +} + +.feichuan-task-bar.is-delayed { + background: #ffd9d1; +} + +.feichuan-task-bar.is-future { + background: #c8e2fb; + opacity: 0.76; +} + +.feichuan-task-bar.is-critical { + box-shadow: inset 0 -2px 0 #ef4444; +} + +.feichuan-bar-progress { + position: absolute; + inset: 5px auto 5px 0; + background: #2f7df6; +} + +.feichuan-bar-handle { + position: absolute; + top: 1px; + z-index: 4; + width: 10px; + height: 24px; + border: 2px solid #fff; + border-radius: 999px; + background: #64748b; + box-shadow: 0 1px 5px rgba(15, 23, 42, 0.24); + transform: translateX(-50%); +} + +.feichuan-task-bar.is-ahead .feichuan-bar-progress { + background: #12c86b; +} + +.feichuan-task-bar.is-warning .feichuan-bar-progress { + background: #ff9f2e; +} + +.feichuan-task-bar.is-delayed .feichuan-bar-progress { + background: #ef4444; +} + +.feichuan-bar-hatch { + position: absolute; + inset: 5px auto 5px 20%; + width: min(170px, 45%); + background: repeating-linear-gradient( + 135deg, + rgba(255, 255, 255, 0.65) 0, + rgba(255, 255, 255, 0.65) 8px, + rgba(226, 232, 240, 0.75) 8px, + rgba(226, 232, 240, 0.75) 14px + ); +} + +.feichuan-task-bar strong { + position: absolute; + right: 8px; + top: 5px; + z-index: 1; + color: #fff; + font-size: 10px; + font-weight: 500; +} + +.feichuan-task-bar em { + position: absolute; + left: calc(100% + 10px); + top: 5px; + width: 180px; + color: #1f2937; + font-size: 12px; + font-style: normal; + white-space: nowrap; +} + +.feichuan-tooltip { + position: absolute; + left: 50%; + top: -118px; + z-index: 30; + display: none; + width: 270px; + border-radius: 12px; + background: #fff; + box-shadow: 0 8px 24px rgba(15, 23, 42, 0.22); + overflow: hidden; +} + +.feichuan-task-bar:hover .feichuan-tooltip { + display: grid; +} + +.feichuan-tooltip b { + background: #ff9f2e; + color: #fff; + font-size: 13px; + padding: 0.7rem 0.9rem; +} + +.feichuan-tooltip small { + color: #94a3b8; + font-size: 12px; + padding: 0.15rem 0.9rem; +} + +.feichuan-tooltip small:last-child { + padding-bottom: 0.75rem; +} + +.feichuan-graph-editor { + position: fixed; + z-index: 1200; + display: grid; + width: 286px; + gap: 0.55rem; + border: 1px solid #d7dce2; + border-radius: 8px; + background: #fff; + box-shadow: 0 14px 36px rgba(15, 23, 42, 0.22); + color: #0f172a; + padding: 0.7rem; +} + +.feichuan-graph-editor.is-progress { + border-color: #2f7df6; +} + +.feichuan-graph-editor-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; +} + +.feichuan-graph-editor-head strong { + font-size: 13px; + font-weight: 600; +} + +.feichuan-graph-editor-head button { + display: inline-flex; + width: 24px; + height: 24px; + align-items: center; + justify-content: center; + border: 0; + border-radius: 4px; + background: #f1f5f9; + color: #64748b; + cursor: pointer; + font-size: 16px; + line-height: 1; +} + +.feichuan-graph-editor-head button:hover { + background: #e2e8f0; + color: #0f172a; +} + +.feichuan-graph-editor label { + display: grid; + min-width: 0; + gap: 0.25rem; + color: #64748b; + font-size: 11px; +} + +.feichuan-graph-editor label.is-primary span { + color: #1677ff; + font-weight: 600; +} + +.feichuan-graph-editor input, +.feichuan-graph-editor select { + min-width: 0; + height: 30px; + border: 1px solid #d7dce2; + border-radius: 4px; + background: #fff; + color: #0f172a; + font-size: 12px; + outline: 0; + padding: 0 0.45rem; +} + +.feichuan-graph-editor input:focus, +.feichuan-graph-editor select:focus { + border-color: #2f7df6; + box-shadow: 0 0 0 2px rgba(47, 125, 246, 0.14); +} + +.feichuan-graph-editor-grid { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + gap: 0.45rem; +} + +.feichuan-graph-editor-actions { + display: flex; + justify-content: flex-end; +} + +.feichuan-graph-editor-actions button { + height: 28px; + border: 1px solid #2f7df6; + border-radius: 4px; + background: #2f7df6; + color: #fff; + cursor: pointer; + font-size: 12px; + padding: 0 0.8rem; +} + +.feichuan-date-marker { + position: absolute; + top: 0; + bottom: 0; + z-index: 15; + width: 0; + border-left: 2px solid #1677ff; + pointer-events: none; +} + +.feichuan-date-marker.is-data { + border-left: 1px dashed #00bcd4; +} + +.feichuan-date-marker span { + position: absolute; + top: 0; + left: -1px; + transform: translateX(-50%); + border-radius: 2px; + background: #1677ff; + color: #fff; + font-size: 10px; + line-height: 18px; + padding: 0 0.35rem; + white-space: nowrap; +} + +.feichuan-date-marker.is-data span { + top: 18px; + background: #27c7d8; +} + +.feichuan-svg-label { + fill: #5f6875; + font-size: 12px; + font-weight: 600; +} + +.feichuan-svg-index { + fill: #0f172a; + font-size: 12px; +} + +.feichuan-pert-node { + display: grid; + width: 140px; + border-radius: 12px; + background: #fff; + box-shadow: 0 8px 20px rgba(15, 23, 42, 0.14); + color: #0f172a; + overflow: hidden; +} + +.feichuan-pert-node div { + display: grid; + grid-template-columns: repeat(3, 1fr); + border-bottom: 1px solid #d7dce2; +} + +.feichuan-pert-node div:last-child { + border-bottom: 0; + border-top: 1px solid #d7dce2; +} + +.feichuan-pert-node span, +.feichuan-pert-node strong { + display: flex; + min-height: 28px; + align-items: center; + justify-content: center; + font-size: 11px; +} + +.feichuan-pert-node strong { + font-size: 12px; +} + +.feichuan-pert-legend { + position: absolute; + top: 80px; + right: 22px; + z-index: 12; + display: grid; + width: 280px; + border-radius: 12px; + background: #fff; + box-shadow: 0 8px 24px rgba(15, 23, 42, 0.18); + overflow: hidden; +} + +.feichuan-pert-legend div { + display: grid; + grid-template-columns: repeat(3, 1fr); +} + +.feichuan-pert-legend span, +.feichuan-pert-legend strong { + min-height: 46px; + border: 1px solid #e5e7eb; + color: #0f172a; + display: flex; + align-items: center; + justify-content: center; + text-align: center; + font-size: 12px; +} + +.feichuan-pert-legend strong { + border-left: 1px solid #e5e7eb; + border-right: 1px solid #e5e7eb; +} + .arch-drawer { width: min(var(--arch-drawer-width), calc(100vw - 1rem)); border-color: var(--arch-border); diff --git a/03-frontend/app/studio/page.tsx b/03-frontend/app/studio/page.tsx index c4855168..e42023f5 100644 --- a/03-frontend/app/studio/page.tsx +++ b/03-frontend/app/studio/page.tsx @@ -1,130 +1,8 @@ -"use client"; +// app/studio/page.tsx - retired Studio entry redirect +// License: Apache-2.0 -import { useEffect, useMemo, useState } from "react"; -import { AnimatePresence, motion } from "motion/react"; -import { useTranslations } from "next-intl"; -import type { Floorplan } from "@/lib/insome/floorplan"; -import { ProposalGeneratorScope, ScriptedProposalGenerator } from "@/lib/proposal"; -import { stepSlide, viewSwitch } from "@/lib/motion-presets"; -import { useStudioCreateStore } from "@/stores/studio-create.store"; -import { useStudioEditorStore } from "@/stores/studio-editor.store"; -import { useStudioViewStore } from "@/stores/studio-view.store"; -import { StudioTopNav } from "@/components/studio/shared/studio-top-nav"; -import { ToastProvider } from "@/components/studio/shared/toast-provider"; -import { StudioWorkspaceHome } from "@/components/studio/workspace-page/workspace-home"; -import { PageThemeMount } from "@/components/shared/page-theme-mount"; -import { UnifiedNav } from "@/components/shared/unified-nav"; -import { CreateStep1Entry } from "@/components/studio/create/step-1-entry"; -import { CreateStep2Form } from "@/components/studio/create/step-2-form"; -import { CreateGeneratingView } from "@/components/studio/create/generating-view"; -import { ProposalsView } from "@/components/studio/create/proposals-view"; -import { StudioEditorShell } from "@/components/studio/editor/editor-shell"; -import { MiddlePanelRouter } from "@/components/studio/editor/middle/panel-router"; -import { CanvasTabs } from "@/components/studio/editor/canvas/canvas-tabs"; -import { StudioPropertiesPanel } from "@/components/studio/editor/properties/properties-panel"; -import { ExportDialog } from "@/components/studio/editor/export-dialog"; -import { ConstraintWarningBadge } from "@/components/studio/editor/constraint-warning"; -import { ClaimButton } from "@/components/studio/editor/claim-button"; -// LEGACY(phase-4.0.3): AskAiButton (top-bar dialog) replaced by docked AiChatSection in left pane. -// Kept on disk for archival; no longer imported here. -// import { AskAiButton } from "@/components/studio/editor/ask-ai-button"; +import { redirect } from 'next/navigation'; export default function StudioPage() { - const t = useTranslations(); - const tExport = useTranslations("studio.export"); - const { view, activeProjectId, activeProposalId } = useStudioViewStore(); - const resetEditor = useStudioEditorStore((s) => s.resetEditor); - const setProposals = useStudioCreateStore((s) => s.setProposals); - const [_stash, setStash] = useState>([]); - - const provider = useMemo( - () => - new ScriptedProposalGenerator({ - sleepMs: 2400, - onFloorplansGenerated: (plans) => { - setStash(plans); - if (typeof window !== "undefined") { - window.__insomeStudioFloorplans = plans; - } - }, - }), - [], - ); - - // TODO(phase-4): switch to project-level state; for now editor resets on each entry - useEffect(() => { - if (view === "editor") resetEditor(); - }, [view, activeProjectId, activeProposalId, resetEditor]); - - // Housekeeping: when leaving proposals/editor, drop stashed generator output. - useEffect(() => { - if (view === "projects") { - setProposals([]); - if (typeof window !== "undefined") { - window.__insomeStudioFloorplans = []; - } - } - }, [view, setProposals]); - - const projectName = - view === "editor" - ? t("studio.nav.fallbackName") + - (activeProposalId ? ` · Proposal ${activeProposalId.slice(-1).toUpperCase()}` : "") - : undefined; - - const rightSlot = - view === "editor" ? ( -
    - - - - - -
    - ) : null; - - return ( - - -
    - {view === "projects" ? ( - - ) : ( - - )} -
    - - - {renderView(view)} - - -
    - -
    -
    - ); -} - -function renderView(view: ReturnType["view"]) { - if (view === "projects") return ; - if (view === "create-step-1") return ; - if (view === "create-step-2") return ; - if (view === "create-generating") return ; - if (view === "create-proposals") return ; - return ( - } - canvas={} - properties={} - /> - ); + redirect('/app/modules/concept_design'); } diff --git a/03-frontend/app/studio/works/[workId]/page.tsx b/03-frontend/app/studio/works/[workId]/page.tsx index d9927c72..358eea8b 100644 --- a/03-frontend/app/studio/works/[workId]/page.tsx +++ b/03-frontend/app/studio/works/[workId]/page.tsx @@ -37,7 +37,7 @@ export default function StudioWorkPage({ params }: PageProps) {

    {tNotFound("description")}

    ← {tNotFound("back")} diff --git a/03-frontend/bun.lock b/03-frontend/bun.lock index dbc4f664..217c97a3 100644 --- a/03-frontend/bun.lock +++ b/03-frontend/bun.lock @@ -5,6 +5,7 @@ "": { "name": "@architoken/frontend", "dependencies": { + "3d-tiles-renderer": "^0.4.24", "@ant-design/charts": "^2.6.7", "@ant-design/colors": "^8.0.1", "@ant-design/cssinjs-utils": "^2.1.2", @@ -16,6 +17,9 @@ "@antv/g6": "5.1.1", "@antv/x6": "3.1.7", "@hookform/resolvers": "5.2.2", + "@ifc-lite/geometry": "^1.18.5", + "@ifc-lite/parser": "^2.4.1", + "@ifc-lite/renderer": "^1.20.1", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-dropdown-menu": "2.1.16", "@radix-ui/react-popover": "1.1.15", @@ -91,6 +95,8 @@ }, }, "packages": { + "3d-tiles-renderer": ["3d-tiles-renderer@0.4.24", "", { "peerDependencies": { "@babylonjs/core": ">=8.0.0", "@babylonjs/loaders": ">=8.0.0", "@react-three/fiber": "^8.17.9 || ^9.0.0", "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0", "three": ">=0.167.0" }, "optionalPeers": ["@babylonjs/core", "@babylonjs/loaders", "@react-three/fiber", "react", "react-dom", "three"] }, "sha512-1n21vaWoV5e+N6rTfK1nv0Bsgu9ptbY6d9ICHnT6W1llloIhUGmGzx5/JH+B7t9XggjL0aJASn0Z5sFOXXvYjw=="], + "@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="], "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], @@ -393,6 +399,26 @@ "@iconify/utils": ["@iconify/utils@3.1.3", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "import-meta-resolve": "^4.2.0" } }, "sha512-LPKOXPn/zV+zis1oOfGWogaXVpqUybF3ZS6SCZIsz8vg0ivVp9+fVqyYB7xq0aiST/VhUQYGO1qo6uoYSiEJqw=="], + "@ifc-lite/data": ["@ifc-lite/data@1.17.0", "", {}, "sha512-ypDZHPko7UHuSQB7L8bEuYeVUE4VBriOKty4vxTZFrndnfpSbtuhuoLucBg8dNWqfyVycz3k2RYav+JnvtFN2A=="], + + "@ifc-lite/encoding": ["@ifc-lite/encoding@1.14.6", "", {}, "sha512-B3zTnUm8mQBa6kXNy7g2ad+WSvIChk8VVj87oFtmuMnipj6Kd9iwUGNVL3R/WEYhWgKSUkkXYTUvaDehI+eFqQ=="], + + "@ifc-lite/geometry": ["@ifc-lite/geometry@1.18.5", "", { "dependencies": { "@ifc-lite/data": "^1.17.0", "@ifc-lite/wasm": "^1.16.10" }, "optionalDependencies": { "@tauri-apps/api": "^2.0.0" }, "peerDependencies": { "@ifc-lite/wasm-threaded": "*" }, "optionalPeers": ["@ifc-lite/wasm-threaded"] }, "sha512-LVZ2D2gsLipyANCD2jBtBc3fSHmQ9KnTTdO4oW3Ws5o49oDP9cqFWOkANr2SekKpgE0hNfTQ1+o6zhKfIHI5Rw=="], + + "@ifc-lite/ifcx": ["@ifc-lite/ifcx@2.1.1", "", { "dependencies": { "@ifc-lite/data": "^1.17.0", "@ifc-lite/mutations": "^1.15.0", "@ifc-lite/pointcloud": "^0.3.0" } }, "sha512-aFpLhYYoblOQU73+0XcJ+7Qe1uBHLgiyUym11LJmvlkeYyvkLGCCtZguuV16l2Ap9ICiyNEnIPMjIkSKdshVoQ=="], + + "@ifc-lite/mutations": ["@ifc-lite/mutations@1.15.0", "", { "dependencies": { "@ifc-lite/data": "^1.15.2" } }, "sha512-WB9jz9zcSt7+jQ/XCHwGBsQTdHwV6Tg58Ps3W6bZ21XMlwbxS2JHr3qcpVfcPV8O7SQPl1mCg/dFdI3kgPHXzA=="], + + "@ifc-lite/parser": ["@ifc-lite/parser@2.4.1", "", { "dependencies": { "@ifc-lite/data": "^1.17.0", "@ifc-lite/encoding": "^1.14.6", "@ifc-lite/ifcx": "^2.1.1", "@ifc-lite/wasm": "^1.16.10" } }, "sha512-1kM+V5AxipP4rIJxp/ql32cNo0AT9ZqE+K9xSO9YTeKjubXSwGERj4Vhw9rjTViLxsaYsq3kbAcBnFohIhW3Tg=="], + + "@ifc-lite/pointcloud": ["@ifc-lite/pointcloud@0.3.1", "", { "dependencies": { "laz-perf": "^0.0.6" } }, "sha512-wMLXmJFUfZPT5SrDDHsw0r3w2vk2+owKDDD28+WTaOecimpmOTcYW6G7HfRnLEdp99/UeFLIQz0gSM42VLURgA=="], + + "@ifc-lite/renderer": ["@ifc-lite/renderer@1.20.1", "", { "dependencies": { "@ifc-lite/geometry": "^1.18.5", "@ifc-lite/spatial": "^1.14.5", "@ifc-lite/wasm": "^1.16.10" } }, "sha512-IlAAyrAGiqIAUePnY2vqZOGPb/Tmh8z+fMLQl3d7oSkLifj4Rwa2oaJr/rB3rV5hoWIr7TQ+mbBOwFByh4SOGQ=="], + + "@ifc-lite/spatial": ["@ifc-lite/spatial@1.14.5", "", { "dependencies": { "@ifc-lite/geometry": "^1.16.2" } }, "sha512-vFsGSiwXsFcb+aBx4p3rTAWqs4Vd0k7P0OkIdH1ZhyQJ9hXa5muJFls3FI3JI+RcPbth2HyyrmUkReLYBn4G6w=="], + + "@ifc-lite/wasm": ["@ifc-lite/wasm@1.16.10", "", {}, "sha512-wK2Ts522X28uEGcDJC/Qw9gXAllz3DBmKLU5XcIDJc47lHJZb65IMPLHP0Kyyh/HBqxWAU+qh1mqlkBD5VhtdQ=="], + "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], @@ -781,6 +807,8 @@ "@tanstack/react-query": ["@tanstack/react-query@5.99.2", "", { "dependencies": { "@tanstack/query-core": "5.99.2" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-vM91UEe45QUS9ED6OklsVL15i8qKcRqNwpWzPTVWvRPRSEgDudDgHpvyTjcdlwHcrKNa80T+xXYcchT2noPnZA=="], + "@tauri-apps/api": ["@tauri-apps/api@2.11.0", "", {}, "sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA=="], + "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="], "@testing-library/jest-dom": ["@testing-library/jest-dom@6.9.1", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "picocolors": "^1.1.1", "redent": "^3.0.0" } }, "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA=="], @@ -1651,6 +1679,8 @@ "layout-base": ["layout-base@1.0.2", "", {}, "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="], + "laz-perf": ["laz-perf@0.0.6", "", {}, "sha512-ZBqC+BBlofznDIY3SfjXDBVdIhYfz7bq8HAHztlw4XOnu++nHiWtCGPgzpdeAhPkByc68DaKNy3E3rY4XrdRtQ=="], + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], "license-checker": ["license-checker@25.0.1", "", { "dependencies": { "chalk": "^2.4.1", "debug": "^3.1.0", "mkdirp": "^0.5.1", "nopt": "^4.0.1", "read-installed": "~4.0.3", "semver": "^5.5.0", "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0", "spdx-satisfies": "^4.0.0", "treeify": "^1.1.0" }, "bin": { "license-checker": "./bin/license-checker" } }, "sha512-mET5AIwl7MR2IAKYYoVBBpV0OnkKQ1xGj2IMMeEFIs42QAkEVjRtFZGWmQ28WeU7MP779iAgOaOy93Mn44mn6g=="], diff --git a/03-frontend/components/AICenterManagementPanels.tsx b/03-frontend/components/AICenterManagementPanels.tsx new file mode 100644 index 00000000..e45f5060 --- /dev/null +++ b/03-frontend/components/AICenterManagementPanels.tsx @@ -0,0 +1,557 @@ +// components/AICenterManagementPanels.tsx +// License: Apache-2.0 +'use client'; + +import { useEffect, useMemo, useState, type ReactNode } from 'react'; +import { + Activity, + AlertCircle, + BarChart3, + CheckCircle2, + Database, + FileJson, + Gauge, + Lock, + Network, + Plug, + RefreshCw, + ShieldCheck, + Table, + Workflow, +} from 'lucide-react'; +import { + api, + type AiCenterDatabaseBinding, + type AiCenterInterfaceContract, + type AiCenterManagementResponse, + type AiCenterManagementStatus, + type AiCenterVisualizationPanel, +} from '@/lib/api'; +import { createModuleAuditEvent } from '@/lib/module-actions'; +import type { ModuleAuditEvent } from '@/lib/module-file-system'; + +type ManagementPanelId = 'interfaces' | 'databases' | 'visualization'; + +const STATUS_META: Record = { + configured: { + label: '已接入', + className: 'border-emerald-200 bg-emerald-50 text-emerald-700', + }, + approved: { + label: '已批准', + className: 'border-blue-200 bg-blue-50 text-blue-700', + }, + review: { + label: '待审批', + className: 'border-amber-200 bg-amber-50 text-amber-700', + }, + draft: { + label: '待配置', + className: 'border-slate-200 bg-slate-50 text-slate-600', + }, + disabled: { + label: '已停用', + className: 'border-rose-200 bg-rose-50 text-rose-700', + }, +}; + +const MANAGEMENT_PANELS: { + id: ManagementPanelId; + label: string; + description: string; + icon: ReactNode; +}[] = [ + { + id: 'interfaces', + label: '接口管理', + description: '读取并更新后端 ai_center_interface_contracts。', + icon: , + }, + { + id: 'databases', + label: '数据库管理', + description: '读取并更新后端 ai_center_database_bindings。', + icon: , + }, + { + id: 'visualization', + label: '可视化面板', + description: '读取并更新后端 ai_center_visualization_panels。', + icon: , + }, +]; + +export function AICenterManagementPanels({ + compact = false, + onAudit, +}: { + compact?: boolean; + onAudit?: (event: ModuleAuditEvent) => void; +}) { + const [activePanel, setActivePanel] = useState('interfaces'); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(false); + const [savingKey, setSavingKey] = useState(null); + const [error, setError] = useState(null); + + const emitAudit = (action: string, detail: string) => { + onAudit?.(createModuleAuditEvent(action, 'AICenterManagementPanels', detail)); + }; + + const loadManagement = async () => { + setLoading(true); + setError(null); + try { + const payload = await api.aiCenter.management(); + setData(payload); + emitAudit( + 'ai-center-management-refresh', + `AI 中心管理数据已从 Gateway 刷新: ${payload.interfaceContracts.length}/${payload.databaseBindings.length}/${payload.visualizationPanels.length}`, + ); + } catch (err) { + setError(apiErrorMessage(err, 'AI 中心管理接口不可用')); + setData(null); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + const timer = window.setTimeout(() => { + void loadManagement(); + }, 0); + return () => window.clearTimeout(timer); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const stats = useMemo( + () => [ + { + label: '接口合同', + value: String(data?.interfaceContracts.length ?? 0), + detail: `${countStatus(data?.interfaceContracts ?? [], 'configured')} 已接入`, + icon: , + }, + { + label: '数据对象', + value: String(data?.databaseBindings.length ?? 0), + detail: `${countStatus(data?.databaseBindings ?? [], 'review')} 待审批`, + icon: , + }, + { + label: '运行视图', + value: String(data?.visualizationPanels.length ?? 0), + detail: `${countStatus(data?.visualizationPanels ?? [], 'approved')} 已批准`, + icon: , + }, + ], + [data], + ); + + const updateInterfaceStatus = async (contractKey: string, status: AiCenterManagementStatus) => { + setSavingKey(`interface:${contractKey}`); + setError(null); + try { + const updated = await api.aiCenter.updateInterfaceContract(contractKey, { + status, + metadata: { updatedFrom: 'ai_center_workbench' }, + }); + setData((current) => + current + ? { + ...current, + interfaceContracts: current.interfaceContracts.map((item) => + item.contractKey === updated.contractKey ? updated : item, + ), + } + : current, + ); + emitAudit('ai-interface-contract-update', `接口合同状态已写回数据库: ${updated.contractKey} -> ${updated.status}`); + } catch (err) { + setError(apiErrorMessage(err, '接口合同状态写回失败')); + } finally { + setSavingKey(null); + } + }; + + const updateDatabaseStatus = async (bindingKey: string, status: AiCenterManagementStatus) => { + setSavingKey(`database:${bindingKey}`); + setError(null); + try { + const updated = await api.aiCenter.updateDatabaseBinding(bindingKey, { + status, + metadata: { updatedFrom: 'ai_center_workbench' }, + }); + setData((current) => + current + ? { + ...current, + databaseBindings: current.databaseBindings.map((item) => + item.bindingKey === updated.bindingKey ? updated : item, + ), + } + : current, + ); + emitAudit('ai-database-binding-update', `数据库绑定状态已写回数据库: ${updated.bindingKey} -> ${updated.status}`); + } catch (err) { + setError(apiErrorMessage(err, '数据库绑定状态写回失败')); + } finally { + setSavingKey(null); + } + }; + + const updateVisualizationStatus = async (panelKey: string, status: AiCenterManagementStatus) => { + setSavingKey(`visualization:${panelKey}`); + setError(null); + try { + const updated = await api.aiCenter.updateVisualizationPanel(panelKey, { + status, + metadata: { updatedFrom: 'ai_center_workbench' }, + }); + setData((current) => + current + ? { + ...current, + visualizationPanels: current.visualizationPanels.map((item) => + item.panelKey === updated.panelKey ? updated : item, + ), + } + : current, + ); + emitAudit('ai-visualization-panel-update', `可视化面板状态已写回数据库: ${updated.panelKey} -> ${updated.status}`); + } catch (err) { + setError(apiErrorMessage(err, '可视化面板状态写回失败')); + } finally { + setSavingKey(null); + } + }; + + return ( +
    +
    +
    +

    AI Ops Registry

    +

    接口、数据库与可视化治理

    +

    + 数据来自 Gateway 与 PostgreSQL 管理表;状态按钮会写回数据库,不使用前端静态卡片。 +

    +
    + +
    + + {error ? ( +
    + + {error} +
    + ) : null} + +
    + {stats.map((item) => ( +
    +
    + + {item.icon} + {item.label} + + {item.value} +
    +

    {item.detail}

    +
    + ))} +
    + +
    + {MANAGEMENT_PANELS.map((panel) => { + const selected = activePanel === panel.id; + return ( + + ); + })} +
    + +
    + {activePanel === 'interfaces' ? ( + + ) : null} + {activePanel === 'databases' ? ( + + ) : null} + {activePanel === 'visualization' ? ( + + ) : null} +
    +
    + ); +} + +function InterfaceManagementPanel({ + items, + savingKey, + onUpdateStatus, +}: { + items: AiCenterInterfaceContract[]; + savingKey: string | null; + onUpdateStatus: (contractKey: string, status: AiCenterManagementStatus) => Promise; +}) { + return ( +
    +
    +
    + 方法 + 接口合同 + 边界 + 数据对象 + 状态 +
    + {items.map((item) => ( +
    + {item.method} +
    +

    {item.name}

    +

    {item.path}

    +

    + + {item.authPolicy} +

    +
    +

    {item.boundary}

    + {item.dataObject} +
    + + onUpdateStatus(item.contractKey, 'review')} + onApprove={() => onUpdateStatus(item.contractKey, 'approved')} + /> +
    +
    + ))} + {items.length === 0 ? : null} +
    +
    + ); +} + +function DatabaseManagementPanel({ + items, + savingKey, + onUpdateStatus, +}: { + items: AiCenterDatabaseBinding[]; + savingKey: string | null; + onUpdateStatus: (bindingKey: string, status: AiCenterManagementStatus) => Promise; +}) { + return ( +
    + {items.map((item) => ( +
    +
    +
    +

    {item.name}

    +

    {item.objectName}

    +
    + +
    +
    + + + + +
    + onUpdateStatus(item.bindingKey, 'review')} + onApprove={() => onUpdateStatus(item.bindingKey, 'approved')} + /> +
    + ))} + {items.length === 0 ? : null} +
    + ); +} + +function VisualizationPanel({ + items, + savingKey, + onUpdateStatus, +}: { + items: AiCenterVisualizationPanel[]; + savingKey: string | null; + onUpdateStatus: (panelKey: string, status: AiCenterManagementStatus) => Promise; +}) { + return ( +
    +
    +
    +
    +

    AI 运行视图注册表

    +

    视图记录来自数据库,发布审查会写回状态。

    +
    + +
    +
    + {items.map((panel) => ( +
    +
    +
    +

    {panel.name}

    +

    {panel.dataset}

    +
    + +
    +
    + 视图: {panel.viewMode} + 刷新: {panel.refreshPolicy} + 准备度: {panel.readiness}% +
    +
    +
    +
    + onUpdateStatus(panel.panelKey, 'review')} + onApprove={() => onUpdateStatus(panel.panelKey, 'approved')} + /> +
    + ))} + {items.length === 0 ? : null} +
    +
    +
    +

    发布门禁

    +
      +
    • + + 视图必须声明数据库数据集和租户隔离策略。 +
    • +
    • + + AI 调用链必须保留 Planner 到 Approver 审计上下文。 +
    • +
    • + + 运行指标只能从真实后端事件或已声明配置对象读取。 +
    • +
    +
    +
    + ); +} + +function ActionButtons({ + id, + savingKey, + onReview, + onApprove, +}: { + id: string; + savingKey: string | null; + onReview: () => Promise; + onApprove: () => Promise; +}) { + const saving = savingKey === id; + return ( +
    + + +
    + ); +} + +function StatusBadge({ status }: { status: AiCenterManagementStatus }) { + const meta = STATUS_META[status]; + return ( + + {meta.label} + + ); +} + +function KeyValue({ label, value }: { label: string; value: string }) { + return ( +
    +
    {label}
    +
    {value}
    +
    + ); +} + +function EmptyState({ label }: { label: string }) { + return
    {label}
    ; +} + +function countStatus(items: T[], status: AiCenterManagementStatus) { + return items.filter((item) => item.status === status).length; +} + +function apiErrorMessage(err: unknown, fallback: string) { + if (err instanceof Error) { + return err.message; + } + if (typeof err === 'object' && err && 'error' in err) { + const error = (err as { error?: unknown }).error; + if (typeof error === 'string') { + return error; + } + } + return fallback; +} diff --git a/03-frontend/components/AICenterWorkbench.tsx b/03-frontend/components/AICenterWorkbench.tsx index d0269bb4..252f32b4 100644 --- a/03-frontend/components/AICenterWorkbench.tsx +++ b/03-frontend/components/AICenterWorkbench.tsx @@ -8,8 +8,10 @@ import { useLLMConfig, type ProviderId } from '@/lib/llm-provider'; import { getOllamaModels, getHfModels } from '@/lib/local-models-action'; import type { ModuleAuditEvent } from '@/lib/module-file-system'; import { createModuleAuditEvent } from '@/lib/module-actions'; +import { AICenterManagementPanels } from '@/components/AICenterManagementPanels'; const PROVIDERS: { id: ProviderId; name: string; icon: ReactNode; type: 'local' | 'cloud' }[] = [ + { id: 'openclaw', name: 'OpenClaw', icon: , type: 'local' }, { id: 'ollama', name: 'Ollama', icon: , type: 'local' }, { id: 'vllm', name: 'vLLM', icon: , type: 'local' }, { id: 'huggingface', name: 'Hugging Face', icon: , type: 'local' }, @@ -29,6 +31,7 @@ const ROLE_ALIAS_MODELS = [ ]; const CLOUD_ALIAS_MODELS: Record = { + openclaw: ['openclaw/default', 'architoken-openclaw-router', ...ROLE_ALIAS_MODELS], ollama: [], vllm: [], huggingface: [], @@ -42,6 +45,7 @@ const CLOUD_ALIAS_MODELS: Record = { }; const PROVIDER_ENDPOINTS: Record = { + openclaw: { apiBaseUrl: 'http://127.0.0.1:7561', consoleUrl: 'https://github.com/openclaw/openclaw' }, ollama: { apiBaseUrl: 'http://192.168.1.100:11434' }, vllm: { apiBaseUrl: 'http://192.168.1.100:8000' }, huggingface: { apiBaseUrl: 'https://api-inference.huggingface.co', consoleUrl: 'https://huggingface.co/models' }, @@ -84,7 +88,7 @@ function modelCatalogUrl(provider: ProviderId, baseUrl?: string): string | null return `${apiBaseUrl}/models?output_modalities=all`; } - if (['vllm', 'lmstudio', 'unsloth'].includes(provider)) { + if (['openclaw', 'vllm', 'lmstudio', 'unsloth'].includes(provider)) { return apiBaseUrl.endsWith('/v1') ? `${apiBaseUrl}/models` : `${apiBaseUrl}/v1/models`; } @@ -139,7 +143,7 @@ export function AICenterWorkbench({ models = await getOllamaModels(); } else if (provider === 'huggingface') { models = await getHfModels(); - } else if (['vllm', 'lmstudio', 'unsloth'].includes(provider)) { + } else if (['openclaw', 'vllm', 'lmstudio', 'unsloth'].includes(provider)) { const url = modelCatalogUrl(provider, baseUrl); if (url) { const res = await fetch(url).catch(() => null); @@ -324,6 +328,8 @@ export function AICenterWorkbench({
    + + ); } diff --git a/03-frontend/components/ArchivePackageViewer.tsx b/03-frontend/components/ArchivePackageViewer.tsx index c2dcf1ca..9a91525a 100644 --- a/03-frontend/components/ArchivePackageViewer.tsx +++ b/03-frontend/components/ArchivePackageViewer.tsx @@ -851,7 +851,7 @@ function classifyZipEntry(name: string, directory: boolean): ZipArchiveEntry['ki const extension = zipEntryExtension(name); if (['.zip', '.zipx', '.7z', '.rar', '.tar', '.gz', '.bz2', '.xz', '.zst', '.tgz', '.tbz2', '.tar.gz', '.tar.bz2', '.tar.xz', '.ifczip', '.bcfzip', '.jar', '.war', '.ear', '.apk', '.ipa', '.asar'].includes(extension)) return 'archive'; if (['.ifc', '.ifczip', '.ids', '.bcf', '.bcfzip', '.idm'].includes(extension)) return 'bim'; - if (['.dxf', '.dwg', '.step', '.stp', '.iges', '.igs', '.brep', '.stl', '.obj', '.ply', '.3dm', '.skp'].includes(extension)) return 'cad'; + if (['.dxf', '.dwg', '.step', '.stp', '.iges', '.igs', '.brep', '.stl', '.ply', '.3dm', '.skp', '.usd', '.usda', '.usdc', '.usdz', '.gltf', '.glb', '.b3dm', '.i3dm', '.pnts', '.cmpt'].includes(extension)) return 'cad'; if (['.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.odt', '.ods', '.odp', '.rtf'].includes(extension)) return 'office'; if (['.pdf', '.txt', '.md', '.html', '.htm'].includes(extension)) return 'document'; if (['.png', '.jpg', '.jpeg', '.webp', '.gif', '.svg', '.heic'].includes(extension)) return 'image'; diff --git a/03-frontend/components/BIMViewer.tsx b/03-frontend/components/BIMViewer.tsx index f9bc11c6..141996ea 100644 --- a/03-frontend/components/BIMViewer.tsx +++ b/03-frontend/components/BIMViewer.tsx @@ -7,7 +7,10 @@ import { Suspense, useEffect, useState, + type Dispatch, + type KeyboardEvent, type ReactNode, + type SetStateAction, } from "react"; import { Canvas, useLoader } from "@react-three/fiber"; import { @@ -19,14 +22,14 @@ import { OrbitControls, useGLTF, } from "@react-three/drei"; +import { TilesRenderer as Tiles3dRenderer } from "3d-tiles-renderer/r3f"; import { STLLoader } from "three/examples/jsm/loaders/STLLoader.js"; -import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader.js"; import { PLYLoader } from "three/examples/jsm/loaders/PLYLoader.js"; -import { FBXLoader } from "three/examples/jsm/loaders/FBXLoader.js"; import { ColladaLoader } from "three/examples/jsm/loaders/ColladaLoader.js"; export interface BIMViewerProps { sourceUrl?: string | null; + materialUrl?: string | null; ifcData?: string | null; fileName?: string; mimeType?: string; @@ -51,22 +54,26 @@ function isGltfSource(fileName?: string, mimeType?: string): boolean { ); } -function isStlSource(fileName?: string, mimeType?: string): boolean { - const ext = fileName ? extensionOf(fileName) : ""; +function isTiles3dSource(fileName?: string, mimeType?: string): boolean { + const normalizedFileName = fileName?.toLowerCase() ?? ""; const normalizedMimeType = mimeType?.toLowerCase() ?? ""; return ( - ext === ".stl" || - normalizedMimeType === "model/stl" || - normalizedMimeType === "application/sla" + normalizedFileName.endsWith("tileset.json") || + normalizedMimeType === "application/vnd.3dtiles+json" || + normalizedMimeType === "model/vnd.3dtiles" ); } -function isObjSource(fileName?: string, mimeType?: string): boolean { +function isStlSource(fileName?: string, mimeType?: string): boolean { const ext = fileName ? extensionOf(fileName) : ""; const normalizedMimeType = mimeType?.toLowerCase() ?? ""; - return ext === ".obj" || normalizedMimeType === "model/obj"; + return ( + ext === ".stl" || + normalizedMimeType === "model/stl" || + normalizedMimeType === "application/sla" + ); } function isPlySource(fileName?: string, mimeType?: string): boolean { @@ -76,13 +83,6 @@ function isPlySource(fileName?: string, mimeType?: string): boolean { return ext === ".ply" || normalizedMimeType === "model/ply"; } -function isFbxSource(fileName?: string, mimeType?: string): boolean { - const ext = fileName ? extensionOf(fileName) : ""; - const normalizedMimeType = mimeType?.toLowerCase() ?? ""; - - return ext === ".fbx" || normalizedMimeType.includes("fbx"); -} - function isColladaSource(fileName?: string, mimeType?: string): boolean { const ext = fileName ? extensionOf(fileName) : ""; const normalizedMimeType = mimeType?.toLowerCase() ?? ""; @@ -114,6 +114,10 @@ function GltfModel({ url }: { url: string }) { return ; } +function Tiles3dModel({ url }: { url: string }) { + return ; +} + function StlModel({ url }: { url: string }) { const geometry = useLoader(STLLoader, url); geometry.computeVertexNormals(); @@ -124,11 +128,6 @@ function StlModel({ url }: { url: string }) { ); } -function ObjModel({ url }: { url: string }) { - const object = useLoader(OBJLoader, url); - return ; -} - function PlyModel({ url }: { url: string }) { const geometry = useLoader(PLYLoader, url); geometry.computeVertexNormals(); @@ -139,11 +138,6 @@ function PlyModel({ url }: { url: string }) { ); } -function FbxModel({ url }: { url: string }) { - const object = useLoader(FBXLoader, url); - return ; -} - function ColladaModel({ url }: { url: string }) { const collada = useLoader(ColladaLoader, url); if (!collada) { @@ -175,8 +169,8 @@ function EmptyEngineeringScene({

    {detail ?? (canParseIfc - ? "IFC 源文件已接入。当前前端未安装 IFC WASM 解析器,已显示 IFC 源码预览;后续可由后端 Worker 转换为 GLB/3D Tiles。" - : "该工程格式需要后端解析管线生成可视化 derivative,例如 GLB、glTF 或 3D Tiles。")} + ? "模型源文件已接入。当前浏览器仅显示源码预览,完整查看需等待 Prengine 完成轻量化处理。" + : "该工程格式需要 Prengine 生成可视化模型后查看。")}

    @@ -192,6 +186,109 @@ interface GltfValidationState { reason?: string; } +type ModelAxisAction = + | "left" + | "right" + | "up" + | "down" + | "front" + | "back" + | "reset"; + +interface BimViewTransform { + offsetX: number; + offsetY: number; + offsetZ: number; +} + +const defaultBimViewTransform: BimViewTransform = { + offsetX: 0, + offsetY: 0, + offsetZ: 0, +}; + +function BimSixAxisControlPanel({ + onAction, +}: { + onAction: (action: ModelAxisAction) => void; +}) { + const buttons: Array<{ + action: ModelAxisAction; + label: string; + title: string; + className: string; + }> = [ + { action: "up", label: "上", title: "沿上轴移动", className: "col-start-2" }, + { action: "front", label: "前", title: "沿前轴移动", className: "col-start-4" }, + { action: "left", label: "左", title: "沿左轴移动", className: "col-start-1 row-start-2" }, + { action: "reset", label: "中", title: "重置六轴位置", className: "col-start-2 row-start-2" }, + { action: "right", label: "右", title: "沿右轴移动", className: "col-start-3 row-start-2" }, + { action: "back", label: "后", title: "沿后轴移动", className: "col-start-4 row-start-2" }, + { action: "down", label: "下", title: "沿下轴移动", className: "col-start-2 row-start-3" }, + ]; + + return ( +
    + {buttons.map((button) => ( + + ))} +
    + ); +} + +function applyBimAxisAction( + action: ModelAxisAction, + setViewTransform: Dispatch>, + step = 1, +) { + setViewTransform((current) => { + if (action === "reset") return defaultBimViewTransform; + if (action === "left") return { ...current, offsetX: current.offsetX - step }; + if (action === "right") return { ...current, offsetX: current.offsetX + step }; + if (action === "up") return { ...current, offsetZ: current.offsetZ + step }; + if (action === "down") return { ...current, offsetZ: current.offsetZ - step }; + if (action === "front") return { ...current, offsetY: current.offsetY - step }; + return { ...current, offsetY: current.offsetY + step }; + }); +} + +function handleBimKeyDown( + event: KeyboardEvent, + setViewTransform: Dispatch>, +) { + const step = event.shiftKey ? 4 : 1; + const key = event.key.toLowerCase(); + const handlers: Record void> = { + arrowup: () => applyBimAxisAction("up", setViewTransform, step), + arrowdown: () => applyBimAxisAction("down", setViewTransform, step), + arrowleft: () => applyBimAxisAction("left", setViewTransform, step), + arrowright: () => applyBimAxisAction("right", setViewTransform, step), + pageup: () => applyBimAxisAction("front", setViewTransform, step), + pagedown: () => applyBimAxisAction("back", setViewTransform, step), + w: () => applyBimAxisAction("front", setViewTransform, step), + s: () => applyBimAxisAction("back", setViewTransform, step), + a: () => applyBimAxisAction("left", setViewTransform, step), + d: () => applyBimAxisAction("right", setViewTransform, step), + r: () => applyBimAxisAction("reset", setViewTransform, step), + }; + const handler = handlers[key]; + if (!handler) return; + event.preventDefault(); + handler(); +} + function isLikelyValidGltfPayload( buffer: ArrayBuffer, fileName?: string, @@ -284,13 +381,16 @@ export function BIMViewer({ key: "", status: "idle", }); + const [viewTransform, setViewTransform] = + useState(defaultBimViewTransform); const effectiveIfcData = ifcData ?? loadedIfcData; const canRenderGltf = Boolean(sourceUrl && isGltfSource(fileName, mimeType)); + const canRenderTiles3d = Boolean( + sourceUrl && isTiles3dSource(fileName, mimeType), + ); const canRenderStl = Boolean(sourceUrl && isStlSource(fileName, mimeType)); - const canRenderObj = Boolean(sourceUrl && isObjSource(fileName, mimeType)); const canRenderPly = Boolean(sourceUrl && isPlySource(fileName, mimeType)); - const canRenderFbx = Boolean(sourceUrl && isFbxSource(fileName, mimeType)); const canRenderCollada = Boolean( sourceUrl && isColladaSource(fileName, mimeType), ); @@ -302,29 +402,27 @@ export function BIMViewer({ ); const gltfValidationKey = `${sourceUrl ?? ""}:${fileName}:${mimeType ?? ""}`; - const status = canRenderObj - ? "OBJ mesh 实时渲染" + const status = canRenderTiles3d + ? "Prengine 模型流式查看" : canRenderPly - ? "PLY mesh 实时渲染" - : canRenderFbx - ? "FBX scene 实时渲染" - : canRenderCollada - ? "Collada scene 实时渲染" - : canRenderStl - ? "STL mesh 实时渲染" - : canRenderGltf - ? gltfValidation.key === gltfValidationKey && - gltfValidation.status === "invalid" - ? "GLB/glTF derivative 校验失败" - : gltfValidation.key === gltfValidationKey && - gltfValidation.status === "checking" - ? "GLB/glTF derivative 校验中" - : "GLB/glTF 模型实时渲染" - : canParseIfc - ? effectiveIfcData?.startsWith("ISO-10303-21") - ? "IFC 源文件已接入,源码预览可用" - : "IFC 源文件已接入,正在读取源码" - : "工程文件已接入,等待解析 derivative"; + ? "Prengine 模型实时查看" + : canRenderCollada + ? "Prengine 模型实时查看" + : canRenderStl + ? "Prengine 模型实时查看" + : canRenderGltf + ? gltfValidation.key === gltfValidationKey && + gltfValidation.status === "invalid" + ? "Prengine 模型校验失败" + : gltfValidation.key === gltfValidationKey && + gltfValidation.status === "checking" + ? "Prengine 模型校验中" + : "Prengine 模型实时查看" + : canParseIfc + ? effectiveIfcData?.startsWith("ISO-10303-21") + ? "Prengine 源文件预览可用" + : "Prengine 正在读取源文件" + : "工程文件已接入,等待 Prengine 处理"; useEffect(() => { let cancelled = false; @@ -419,6 +517,8 @@ export function BIMViewer({ className ?? "relative min-h-[calc(100vh-180px)] overflow-hidden rounded-lg border border-slate-800 bg-slate-950" } + tabIndex={0} + onKeyDown={(event) => handleBimKeyDown(event, setViewTransform)} > {showStatusPanel ? (
    @@ -429,6 +529,10 @@ export function BIMViewer({
    ) : null} + applyBimAxisAction(action, setViewTransform)} + /> + @@ -444,71 +548,72 @@ export function BIMViewer({ } > - {canRenderGltf && sourceUrl ? ( - gltfValidation.key === gltfValidationKey && - gltfValidation.status === "valid" ? ( - - } - > - -
    - -
    -
    -
    + + {canRenderTiles3d && sourceUrl ? ( + + ) : canRenderGltf && sourceUrl ? ( + gltfValidation.key === gltfValidationKey && + gltfValidation.status === "valid" ? ( + + } + > + +
    + +
    +
    +
    + ) : ( + + ) + ) : canRenderStl && sourceUrl ? ( + +
    + +
    +
    + ) : canRenderPly && sourceUrl ? ( + +
    + +
    +
    + ) : canRenderCollada && sourceUrl ? ( + +
    + +
    +
    ) : ( - ) - ) : canRenderStl && sourceUrl ? ( - -
    - -
    -
    - ) : canRenderObj && sourceUrl ? ( - -
    - -
    -
    - ) : canRenderPly && sourceUrl ? ( - -
    - -
    -
    - ) : canRenderFbx && sourceUrl ? ( - -
    - -
    -
    - ) : canRenderCollada && sourceUrl ? ( - -
    - -
    -
    - ) : ( - - )} + )} +
    @@ -517,7 +622,7 @@ export function BIMViewer({ {effectiveIfcData?.startsWith("ISO-10303-21") ? (
    - IFC 源码预览 ISO-10303-21 + 源文件预览
                 {effectiveIfcData}
    diff --git a/03-frontend/components/ConceptDesignStudioWorkbench.tsx b/03-frontend/components/ConceptDesignStudioWorkbench.tsx
    new file mode 100644
    index 00000000..2f96a3ae
    --- /dev/null
    +++ b/03-frontend/components/ConceptDesignStudioWorkbench.tsx
    @@ -0,0 +1,381 @@
    +// components/ConceptDesignStudioWorkbench.tsx - Concept design embedded Studio dashboard
    +// License: Apache-2.0
    +'use client';
    +
    +import { useEffect, useMemo, useState } from 'react';
    +import { useTranslations } from 'next-intl';
    +import { BadgeCheck, ClipboardList, FolderOpen, Plus } from 'lucide-react';
    +import { CanvasTabs } from '@/components/studio/editor/canvas/canvas-tabs';
    +import { ClaimButton } from '@/components/studio/editor/claim-button';
    +import { ConstraintWarningBadge } from '@/components/studio/editor/constraint-warning';
    +import { StudioEditorShell } from '@/components/studio/editor/editor-shell';
    +import { ExportDialog } from '@/components/studio/editor/export-dialog';
    +import { MiddlePanelRouter } from '@/components/studio/editor/middle/panel-router';
    +import { StudioPropertiesPanel } from '@/components/studio/editor/properties/properties-panel';
    +import { CreateGeneratingView } from '@/components/studio/create/generating-view';
    +import { ProposalsView } from '@/components/studio/create/proposals-view';
    +import { CreateStep1Entry } from '@/components/studio/create/step-1-entry';
    +import { CreateStep2Form } from '@/components/studio/create/step-2-form';
    +import { ToastProvider } from '@/components/studio/shared/toast-provider';
    +import { WorkCard } from '@/components/shared/work-card';
    +import { WorkDetailDialog } from '@/components/shared/work-detail-dialog';
    +import { DESIGNER_LEVELS } from '@/content/designer-levels';
    +import { mockWorks, type Work } from '@/content/works.mock';
    +import type { StudioView } from '@/lib/insome/types';
    +import { useDesignerStats } from '@/lib/designer-stats';
    +import { createModuleAuditEvent } from '@/lib/module-actions';
    +import type { ModuleAuditEvent } from '@/lib/module-file-system';
    +import { usePublishWork } from '@/lib/publish-work';
    +import { ProposalGeneratorScope, ScriptedProposalGenerator } from '@/lib/proposal';
    +import { useStudioCreateStore } from '@/stores/studio-create.store';
    +import { useStudioEditorStore } from '@/stores/studio-editor.store';
    +import { useStudioViewStore } from '@/stores/studio-view.store';
    +
    +const photoCovers: Record = {
    +  'w-023': '/assets/projects-photo/villa-pool.svg',
    +  'w-024': '/assets/projects-photo/ryokan.svg',
    +  'w-025': '/assets/projects-photo/camp.svg',
    +  'w-027': '/assets/projects-photo/resort.svg',
    +  'w-028': '/assets/projects-photo/alpine.svg',
    +  'w-004': '/assets/projects-photo/ryokan.svg',
    +  'w-005': '/assets/projects-photo/interior.svg',
    +  'w-006': '/assets/projects-photo/alpine.svg',
    +  'w-007': '/assets/projects-photo/villa-pool.svg',
    +};
    +
    +function withPhotoCovers(works: ReadonlyArray): Work[] {
    +  return works.map((work) => ({
    +    ...work,
    +    thumbnail: photoCovers[work.id] ?? work.thumbnail,
    +  }));
    +}
    +
    +export function ConceptDesignStudioWorkbench({
    +  onAudit,
    +}: {
    +  onAudit?: (event: ModuleAuditEvent) => void;
    +}) {
    +  const [lastAction, setLastAction] = useState(null);
    +  const view = useStudioViewStore((state) => state.view);
    +  const activeProjectId = useStudioViewStore((state) => state.activeProjectId);
    +  const activeProposalId = useStudioViewStore((state) => state.activeProposalId);
    +  const startCreate = useStudioViewStore((state) => state.startCreate);
    +  const resetEditor = useStudioEditorStore((state) => state.resetEditor);
    +  const setProposals = useStudioCreateStore((state) => state.setProposals);
    +
    +  const provider = useMemo(
    +    () =>
    +      new ScriptedProposalGenerator({
    +        sleepMs: 2400,
    +      }),
    +    [],
    +  );
    +
    +  useEffect(() => {
    +    if (view === 'editor') resetEditor();
    +  }, [view, activeProjectId, activeProposalId, resetEditor]);
    +
    +  useEffect(() => {
    +    if (view === 'projects') {
    +      setProposals([]);
    +    }
    +  }, [view, setProposals]);
    +
    +  function emitAction(action: string, summary: string) {
    +    setLastAction(summary);
    +    onAudit?.(createModuleAuditEvent(action, 'ConceptDesignStudioWorkbench', summary));
    +  }
    +
    +  function handleNewWork() {
    +    startCreate();
    +    emitAction('concept-design-new-work', '方案设计新建作品入口已打开');
    +  }
    +
    +  return (
    +    
    +      
    + {view === 'projects' ? ( +
    + + + {lastAction ? ( +
    + {lastAction} +
    + ) : null} + +
    + ) : ( + + )} + +
    +
    + ); +} + +function ConceptDesignCreateSurface({ + view, +}: { + view: StudioView; +}) { + const tExport = useTranslations('studio.export'); + const exitToProjects = useStudioViewStore((state) => state.exitToProjects); + const activeProposalId = useStudioViewStore((state) => state.activeProposalId); + const projectName = activeProposalId ? `方案编辑 · Proposal ${activeProposalId.slice(-1).toUpperCase()}` : '方案编辑'; + const showFlowHeader = view !== 'editor'; + + return ( +
    + {showFlowHeader ? ( +
    + + 方案设计 / 新建作品 +
    + ) : null} + +
    + {renderConceptCreateView(view, projectName, tExport('label'))} +
    +
    + ); +} + +function renderConceptCreateView(view: StudioView, projectName: string, exportLabel: string) { + if (view === 'create-step-1') return ; + if (view === 'create-step-2') return ; + if (view === 'create-generating') return ; + if (view === 'create-proposals') return ; + return ( + } + canvas={} + properties={ +
    +
    + + {projectName} + +
    + + + + + +
    +
    +
    + +
    +
    + } + /> + ); +} + +function ConceptDesignerLevelCard() { + const t = useTranslations(); + const tLevel = useTranslations('workspace.studio.level'); + const stats = useDesignerStats(); + const levelMeta = DESIGNER_LEVELS[stats.level - 1]!; + const progress = + stats.nextLevelThreshold !== null + ? Math.min(stats.points / stats.nextLevelThreshold, 1) + : 1; + const remaining = stats.nextLevelThreshold !== null ? stats.nextLevelThreshold - stats.points : 0; + + return ( +
    +
    +
    + + Lv{stats.level} + + + {t(levelMeta.nameKey)} + + + {tLevel('designer')} + +
    + {stats.isVerified ? ( + + {tLevel('verified')} + + ) : null} +
    + +
    + + {stats.nextLevelThreshold !== null ? ( + + ) : null} + +
    + + {stats.nextLevelThreshold !== null ? ( +
    +
    +
    +
    +
    + + {tLevel('points')}: {stats.points.toLocaleString()} + + {stats.nextLevelThreshold.toLocaleString()} +
    +
    + ) : null} +
    + ); +} + +function ConceptDesignerMetric({ + label, + value, + primary = false, +}: { + label: string; + value: string; + primary?: boolean; +}) { + return ( +
    + + {label} + + + {value} + +
    + ); +} + +function ConceptDesignStudioActions({ + onAction, + onNewWork, +}: { + onAction: (action: string, summary: string) => void; + onNewWork: () => void; +}) { + const tActions = useTranslations('workspace.studio.actions'); + const { listPublished } = usePublishWork(); + const stats = useDesignerStats(); + const publishedCount = listPublished().length || stats.publishedWorks; + + return ( +
    + + + +
    + ); +} + +function ConceptDesignPeerFeed() { + const tPeer = useTranslations('workspace.studio.peer'); + const [openId, setOpenId] = useState(null); + + const photoWorks = useMemo(() => withPhotoCovers(mockWorks), []); + const weeklyHot = useMemo( + () => [...photoWorks].sort((a, b) => b.likes - a.likes).slice(0, 8), + [photoWorks], + ); + const newDesigners = useMemo( + () => + photoWorks + .filter((work) => !work.creator.isHomeowner && work.creator.level <= 2) + .slice(0, 6), + [photoWorks], + ); + const work = photoWorks.find((item) => item.id === openId) ?? null; + + return ( +
    + + + !open && setOpenId(null)} + /> +
    + ); +} + +function ConceptDesignFeedRow({ + title, + works, + onOpen, +}: { + title: string; + works: ReadonlyArray; + onOpen: (id: string) => void; +}) { + return ( +
    +

    + {title} +

    +
    + {works.map((work) => ( +
    + +
    + ))} +
    +
    + ); +} diff --git a/03-frontend/components/DetailedDesignPlanFinderWorkbench.tsx b/03-frontend/components/DetailedDesignPlanFinderWorkbench.tsx new file mode 100644 index 00000000..fb51c190 --- /dev/null +++ b/03-frontend/components/DetailedDesignPlanFinderWorkbench.tsx @@ -0,0 +1,2741 @@ +// components/DetailedDesignPlanFinderWorkbench.tsx - AI residential plan studio for detailed design +// License: Apache-2.0 +"use client"; + +import { OrbitControls } from "@react-three/drei"; +import { Canvas, useThree } from "@react-three/fiber"; +import { Button, Input, InputNumber, Select, Switch, Tag, Tooltip } from "antd"; +import { + Armchair, + BoxSelect, + ChevronLeft, + ChevronRight, + DoorOpen, + Grid3X3, + Home, + Layers3, + Library, + PencilRuler, + RefreshCw, + Save, + Sparkles, + Trash2, + Wand2, +} from "lucide-react"; +import type { ReactNode } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { createModuleAuditEvent } from "@/lib/module-actions"; +import type { ModuleAuditEvent } from "@/lib/module-file-system"; +import { moduleFileApiClient } from "@/lib/module-file-api-client"; + +type RoomKey = "主卧" | "主卫" | "次卧" | "卫生间" | "厨房" | "阳台"; +type PublicSplit = "auto" | "lk" | "lk_sep"; +type RoofType = "双坡" | "单坡" | "平"; +type RidgeAxis = "X" | "Y"; +type PlanFinderMode = "generate" | "fit" | "furnish" | "manage"; +type SidePanelTab = "requirements" | "rooms" | "furnish" | "checks"; + +interface RoomDefinition { + key: RoomKey; + count: number; + min: number; + max: number; + short: number; + locked: boolean; + hint: string; +} + +interface RoomRequirement { + count: number; + min: number; + max: number; +} + +interface TemplateConfig { + title: string; + total: number; + floors: 1 | 2; + split: PublicSplit; + rooms: Partial>; +} + +type TemplateRegistry = Record; + +interface StudioIntent { + totalAreaSqm: number; + south: "-Y" | "+Y"; + floors: 1 | 2; + publicSplit: PublicSplit; + roofType: RoofType; + roofRidgeAxis: RidgeAxis; + rooms: Record; +} + +interface Point2D { + x: number; + y: number; +} + +interface PlanBlock { + id: string; + purpose: string; + polygon: Point2D[]; + areaSqm: number; + floor: 1 | 2; + stairKind?: "单跑" | "双跑"; +} + +interface PlanWarning { + room: string; + msg: string; + reason: string; +} + +interface GeneratedPlan { + projectId: string; + projectName: string; + intentLabel: string; + floors: 1 | 2; + blocks: PlanBlock[]; + designNotes: string[]; + warnings: PlanWarning[]; + summary: { + envelope: [number, number]; + envelopeSqm: number; + targetSqm: number; + totalRoomSqm: number; + usableRatioEst: number; + blockCount: number; + floor1Sqm?: number; + floor2Sqm?: number; + }; +} + +interface PlanCandidate { + id: string; + title: string; + command: "Generate" | "Fit" | "Furnish"; + plan: GeneratedPlan; + score: number; + summary: string; +} + +interface FurnitureItem { + id: string; + blockId: string; + label: string; + x0: number; + y0: number; + w: number; + h: number; + floor: 1 | 2; + color: string; +} + +interface BlockRect { + x0: number; + y0: number; + x1: number; + y1: number; + w: number; + h: number; +} + +const MODULUS = 300; +const MAX_SPAN = 4800; +const DEFAULT_USABLE_RATIO = 0.83; + +const roomDefinitions: RoomDefinition[] = [ + { + key: "主卧", + count: 1, + min: 12, + max: 16, + short: 3000, + locked: true, + hint: "南向", + }, + { + key: "主卫", + count: 1, + min: 3, + max: 5, + short: 1500, + locked: false, + hint: "套间", + }, + { + key: "次卧", + count: 2, + min: 10, + max: 13, + short: 2400, + locked: false, + hint: "南向", + }, + { + key: "卫生间", + count: 1, + min: 3, + max: 6, + short: 1500, + locked: false, + hint: "公卫", + }, + { + key: "厨房", + count: 1, + min: 6, + max: 8, + short: 1500, + locked: true, + hint: "", + }, + { + key: "阳台", + count: 0, + min: 3, + max: 6, + short: 1200, + locked: false, + hint: "可选", + }, +]; + +const roomColors: Record = { + 主卧: "#3b82f6", + 次卧: "#60a5fa", + 主卫: "#a78bfa", + 卫生间: "#94a3b8", + 客厅: "#10b981", + 餐厅: "#22c55e", + 客餐厅一体: "#34d399", + 厨房: "#fbbf24", + 阳台: "#fcd34d", + 楼梯: "#a855f7", + 走廊: "#cbd5e1", + 储藏: "#64748b", + 弹性区: "#e2e8f0", + 公共区: "#38bdf8", +}; + +const templates: TemplateRegistry = { + t2: { + title: "两居 75㎡", + total: 75, + floors: 1, + split: "lk", + rooms: { + 主卧: { count: 1, min: 12, max: 14 }, + 主卫: { count: 0, min: 3, max: 5 }, + 次卧: { count: 1, min: 9, max: 11 }, + 卫生间: { count: 1, min: 3, max: 5 }, + 厨房: { count: 1, min: 5, max: 7 }, + 阳台: { count: 0, min: 3, max: 5 }, + }, + }, + t3: { + title: "三居两厅 95㎡", + total: 95, + floors: 1, + split: "lk_sep", + rooms: { + 主卧: { count: 1, min: 13, max: 15 }, + 主卫: { count: 0, min: 3, max: 5 }, + 次卧: { count: 2, min: 10, max: 12 }, + 卫生间: { count: 1, min: 4, max: 6 }, + 厨房: { count: 1, min: 6, max: 8 }, + 阳台: { count: 0, min: 3, max: 5 }, + }, + }, + t3b: { + title: "三居两厅 + 主卫 110㎡", + total: 110, + floors: 2, + split: "lk_sep", + rooms: { + 主卧: { count: 1, min: 14, max: 17 }, + 主卫: { count: 1, min: 3, max: 5 }, + 次卧: { count: 2, min: 10, max: 13 }, + 卫生间: { count: 1, min: 4, max: 6 }, + 厨房: { count: 1, min: 6, max: 8 }, + 阳台: { count: 0, min: 3, max: 5 }, + }, + }, + t4: { + title: "四居两厅双卫 135㎡", + total: 135, + floors: 2, + split: "lk_sep", + rooms: { + 主卧: { count: 1, min: 15, max: 18 }, + 主卫: { count: 1, min: 4, max: 6 }, + 次卧: { count: 3, min: 10, max: 13 }, + 卫生间: { count: 1, min: 4, max: 6 }, + 厨房: { count: 1, min: 7, max: 9 }, + 阳台: { count: 0, min: 3, max: 5 }, + }, + }, +}; + +const paletteDefaults: Record< + string, + { w: number; h: number; stairKind?: "单跑" | "双跑" } +> = { + 主卧: { w: 3600, h: 4500 }, + 次卧: { w: 3000, h: 4200 }, + 主卫: { w: 1500, h: 2400 }, + 卫生间: { w: 1800, h: 2400 }, + 客厅: { w: 3600, h: 4500 }, + 餐厅: { w: 3000, h: 3600 }, + 客餐厅一体: { w: 4800, h: 3600 }, + 厨房: { w: 3000, h: 3000 }, + 阳台: { w: 3000, h: 1500 }, + 楼梯: { w: 2400, h: 3900, stairKind: "双跑" }, + 储藏: { w: 1500, h: 1500 }, + 弹性区: { w: 3000, h: 3000 }, +}; + +const initialIntent: StudioIntent = { + totalAreaSqm: 100, + south: "-Y", + floors: 2, + publicSplit: "auto", + roofType: "平", + roofRidgeAxis: "X", + rooms: Object.fromEntries( + roomDefinitions.map((item) => [ + item.key, + { count: item.count, min: item.min, max: item.max }, + ]), + ) as Record, +}; + +const planFinderModes: Array<{ + id: PlanFinderMode; + label: string; + icon: ReactNode; +}> = [ + { id: "generate", label: "Generate", icon: }, + { id: "fit", label: "Fit", icon: }, + { id: "furnish", label: "Furnish", icon: }, + { id: "manage", label: "Manage", icon: }, +]; + +const sidePanelTabs: Array<{ id: SidePanelTab; label: string }> = [ + { id: "requirements", label: "需求" }, + { id: "rooms", label: "房间" }, + { id: "furnish", label: "家具" }, + { id: "checks", label: "校核" }, +]; + +export function DetailedDesignPlanFinderWorkbench({ + onAudit, +}: { + onAudit?: (event: ModuleAuditEvent) => void; +}) { + const [intent, setIntent] = useState(initialIntent); + const [mode, setMode] = useState("generate"); + const [aiPrompt, setAiPrompt] = useState( + "110 平三室两厅,主卧带卫生间,客厅朝南,大餐厅", + ); + const [candidates, setCandidates] = useState(() => + createPlanCandidates(initialIntent), + ); + const [activeCandidateId, setActiveCandidateId] = useState("generate-a"); + const [sideTab, setSideTab] = useState("requirements"); + const [currentFloor, setCurrentFloor] = useState<1 | 2>(1); + const [selectedBlockId, setSelectedBlockId] = useState(null); + const [editDraft, setEditDraft] = useState(null); + const [built3d, setBuilt3d] = useState(true); + const [showFurniture, setShowFurniture] = useState(false); + const [constructionColumn, setConstructionColumn] = useState(true); + const [saving, setSaving] = useState(false); + const addedRoomSequence = useRef(0); + const [status, setStatus] = useState( + "就绪 · 调整参数后点“生成布局”,3D 预览会同步更新。", + ); + const [plan, setPlan] = useState(() => + generatePlan(initialIntent), + ); + + const liveSummary = useMemo(() => computeLiveSummary(intent), [intent]); + const activeCandidate = + candidates.find((candidate) => candidate.id === activeCandidateId) ?? + candidates[0] ?? + null; + const furniture = useMemo(() => buildFurniture(plan), [plan]); + const furnitureVisible = showFurniture || mode === "furnish"; + const visibleBlocks = plan.blocks.filter( + (block) => plan.floors === 1 || block.floor === currentFloor, + ); + const visibleFurniture = furniture.filter( + (item) => plan.floors === 1 || item.floor === currentFloor, + ); + const selectedBlock = + plan.blocks.find((block) => block.id === selectedBlockId) ?? null; + + function emit(action: string, summary: string) { + setStatus(summary); + onAudit?.( + createModuleAuditEvent(action, "DetailedDesignAiPlanStudio", summary), + ); + } + + function commitPlan(nextPlan: GeneratedPlan) { + setPlan(nextPlan); + setCandidates((current) => + current.map((candidate) => + candidate.id === activeCandidateId + ? { + ...candidate, + plan: nextPlan, + score: scorePlan(nextPlan), + summary: candidateSummary(nextPlan), + } + : candidate, + ), + ); + } + + function activateCandidate(candidate: PlanCandidate) { + const nextMode = candidate.command.toLowerCase() as PlanFinderMode; + setActiveCandidateId(candidate.id); + setPlan(candidate.plan); + setMode(nextMode); + if (nextMode === "furnish") setShowFurniture(true); + setSideTab(nextMode === "furnish" ? "furnish" : "requirements"); + setCurrentFloor(1); + setSelectedBlockId(null); + setEditDraft(null); + setBuilt3d(true); + emit( + "detailed-design-planfinder-candidate-select", + `已切换到 ${candidate.title} · ${candidate.score} 分。`, + ); + } + + function stepCandidate(direction: -1 | 1) { + if (candidates.length === 0) return; + const currentIndex = Math.max( + 0, + candidates.findIndex((candidate) => candidate.id === activeCandidateId), + ); + const nextIndex = + (currentIndex + direction + candidates.length) % candidates.length; + const nextCandidate = candidates[nextIndex]; + if (nextCandidate) activateCandidate(nextCandidate); + } + + function switchMode(nextMode: PlanFinderMode) { + const command = candidateCommandForMode(nextMode); + const nextCandidate = command + ? candidates.find((candidate) => candidate.command === command) + : null; + setMode(nextMode); + if (nextMode === "furnish") setShowFurniture(true); + setSideTab(nextMode === "furnish" ? "furnish" : "requirements"); + if (nextCandidate) { + setActiveCandidateId(nextCandidate.id); + setPlan(nextCandidate.plan); + setCurrentFloor(1); + setSelectedBlockId(null); + setEditDraft(null); + setBuilt3d(true); + } + emit( + "detailed-design-planfinder-mode-change", + nextCandidate + ? `已进入 ${nextModeLabel(nextMode)} 模式 · 当前 ${nextCandidate.title}。` + : `已进入 ${nextModeLabel(nextMode)} 模式。`, + ); + } + + function updateRoom( + key: RoomKey, + field: keyof RoomRequirement, + value: number | null, + ) { + if (value === null || Number.isNaN(value)) return; + setIntent((current) => ({ + ...current, + rooms: { + ...current.rooms, + [key]: { + ...current.rooms[key], + [field]: value, + }, + }, + })); + } + + function updateIntent( + key: K, + value: StudioIntent[K], + ) { + setIntent((current) => ({ ...current, [key]: value })); + } + + function applyTemplate(templateId: string) { + const template = templates[templateId]; + if (!template) return; + const nextIntent: StudioIntent = { + ...intent, + totalAreaSqm: template.total, + floors: template.floors, + publicSplit: template.split, + rooms: roomDefinitions.reduce( + (acc, def) => { + acc[def.key] = template.rooms[def.key] ?? intent.rooms[def.key]; + return acc; + }, + {} as Record, + ), + }; + const nextCandidates = createPlanCandidates(nextIntent); + const fitCandidate = + nextCandidates.find((candidate) => candidate.command === "Fit") ?? + nextCandidates[0]; + setIntent(nextIntent); + setCandidates(nextCandidates); + setActiveCandidateId(fitCandidate?.id ?? nextCandidates[0]?.id ?? ""); + if (fitCandidate) setPlan(fitCandidate.plan); + setMode("fit"); + setSideTab("requirements"); + setCurrentFloor(1); + emit("detailed-design-planfinder-fit", `已套用模板:${template.title}`); + } + + function generateLayout(nextIntent = intent) { + const generatedCandidates = createPlanCandidates(nextIntent); + const firstCandidate = generatedCandidates[0]; + if (!firstCandidate) return; + setCandidates(generatedCandidates); + setActiveCandidateId(firstCandidate.id); + setPlan(firstCandidate.plan); + setMode("generate"); + setSideTab("requirements"); + setCurrentFloor(1); + setSelectedBlockId(null); + setEditDraft(null); + setBuilt3d(true); + emit( + "detailed-design-planfinder-generate", + `已生成 ${generatedCandidates.length} 个户型候选 · 当前 ${firstCandidate.title}。`, + ); + } + + function runAiPrompt() { + const parsed = parsePromptToIntent(aiPrompt, intent); + setIntent(parsed); + generateLayout(parsed); + emit( + "detailed-design-ai-prompt", + "已按自然语言描述生成户型参数和 2D/3D 预览。", + ); + } + + function build3D() { + setBuilt3d(true); + emit( + "detailed-design-ai-plan-build-3d", + `3D 已生成 · 外轮廓 ${plan.summary.envelope[0]}×${plan.summary.envelope[1]}mm · ${plan.floors} 层。`, + ); + } + + function selectBlock(block: PlanBlock) { + setSelectedBlockId(block.id); + setEditDraft(rectFromBlock(block)); + setSideTab("rooms"); + } + + function applyEditDraft() { + if (!selectedBlock || !editDraft) return; + const nextBlocks = plan.blocks.map((block) => + block.id === selectedBlock.id + ? { + ...block, + polygon: rectToPolygon(editDraft), + areaSqm: roundArea((editDraft.w * editDraft.h) / 1e6), + } + : block, + ); + const nextPlan = normalizePlanFromBlocks(plan, nextBlocks); + commitPlan(nextPlan); + setBuilt3d(true); + emit( + "detailed-design-ai-plan-edit", + `已更新 ${selectedBlock.purpose} 尺寸。`, + ); + } + + function deleteSelectedBlock() { + if (!selectedBlock) return; + const nextBlocks = plan.blocks.filter( + (block) => block.id !== selectedBlock.id, + ); + commitPlan(normalizePlanFromBlocks(plan, nextBlocks)); + setSelectedBlockId(null); + setEditDraft(null); + emit( + "detailed-design-ai-plan-delete-room", + `已删除 ${selectedBlock.purpose}。`, + ); + } + + function addPaletteRoom(purpose: string) { + const defaults = paletteDefaults[purpose] ?? { w: 3000, h: 3000 }; + const [envW, envH] = plan.summary.envelope; + const x0 = snap(Math.max(0, envW - defaults.w - 600)); + const y0 = snap(Math.max(0, envH - defaults.h - 600)); + const samePurposeCount = + plan.blocks.filter((block) => block.purpose === purpose).length + 1; + addedRoomSequence.current += 1; + const block: PlanBlock = { + id: `R_${purpose}_${samePurposeCount}_${addedRoomSequence.current}`, + purpose, + polygon: rectToPolygon({ + x0, + y0, + x1: x0 + defaults.w, + y1: y0 + defaults.h, + w: defaults.w, + h: defaults.h, + }), + areaSqm: roundArea((defaults.w * defaults.h) / 1e6), + floor: currentFloor, + ...(defaults.stairKind ? { stairKind: defaults.stairKind } : {}), + }; + commitPlan(normalizePlanFromBlocks(plan, [...plan.blocks, block])); + selectBlock(block); + emit("detailed-design-ai-plan-add-room", `已加入 ${purpose} 色块。`); + } + + async function savePlan() { + setSaving(true); + try { + const payload = { + schema: "architoken.detailed_design.ai_floor_plan_studio.v1", + moduleId: "detailed_design", + source: + "Imported from local AI floor-plan studio reference: frontend/studio.html and intent_to_blocks.py logic", + reviewState: "professional_review_required", + mode, + intent, + plan, + activeCandidate, + candidates, + furniture: furnitureVisible ? furniture : [], + constructionColumn, + createdAt: new Date().toISOString(), + }; + const content = JSON.stringify(payload, null, 2); + await moduleFileApiClient.createModuleFile({ + moduleId: "detailed_design", + parentId: null, + name: `AI户型工作室-${safeFileName(plan.projectName)}-${new Date().toISOString().slice(0, 10)}.json`, + kind: "file", + mimeType: "application/json", + sizeBytes: new TextEncoder().encode(content).byteLength, + owner: "深化设计", + tags: [ + "ai-floor-plan-studio", + "2d-to-3d", + "professional-review-required", + ], + content, + }); + emit( + "detailed-design-ai-plan-save", + "已保存 AI 户型工作室方案到深化设计 CDE。", + ); + } catch (error) { + emit( + "detailed-design-ai-plan-save-failed", + `保存失败:${error instanceof Error ? error.message : "未知错误"}`, + ); + } finally { + setSaving(false); + } + } + + return ( +
    + +
    +
    + +

    + AI 户型工作室 +

    + Generate · Fit · Furnish · Manage +
    +
    + + + + +
    +
    + +
    +
    + {planFinderModes.map((item) => ( + + ))} +
    +
    + {modeDescription(mode)} + {activeCandidate ? ( + = 90 ? "green" : "gold"}> + {activeCandidate.score} 分 + + ) : null} +
    +
    + +
    + + 智能生成 + setAiPrompt(event.target.value)} + placeholder="例:110 平三室两厅,主卧带卫生间,客厅朝南,大餐厅" + className="border-slate-700 bg-slate-800 text-slate-100 placeholder:text-slate-500" + /> + + + ModelRouter / 本地预览 + +
    + +
    + +
    + {candidates.map((candidate) => ( + + ))} +
    + +
    + +
    +
    + 2D 平面图 +
    + + + 画板 + updateIntent("south", value)} + options={[ + { value: "-Y", label: "南 = -Y" }, + { value: "+Y", label: "南 = +Y" }, + ]} + className="w-36" + /> + + + updateIntent("publicSplit", value)} + options={[ + { value: "auto", label: "自动" }, + { value: "lk_sep", label: "客厅 + 餐厅" }, + { value: "lk", label: "客餐厅一体" }, + ]} + className="w-36" + /> + + + ({ + value, + label: item.title, + }), + )} + className="w-40" + /> + +
    +
    + +
    + Room Counts +
    + {roomDefinitions.map((room) => ( +
    +
    + + {room.key} + {room.locked ? ( + 锁定 + ) : null} + + + ≥ {room.short}mm {room.hint ? `· ${room.hint}` : ""} + +
    +
    + + + +
    +
    + ))} +
    +
    + + {mode !== "furnish" ? ( + { + if (checked) { + switchMode("furnish"); + } else { + setShowFurniture(false); + switchMode("generate"); + } + }} + /> + ) : null} +
    + ) : null} + + {sideTab === "rooms" ? ( +
    +
    + Rooms +
    + {visibleBlocks.map((block) => ( + + ))} +
    +
    + +
    + 快速加入 +
    + {[ + "主卧", + "次卧", + "主卫", + "卫生间", + "客厅", + "餐厅", + "客餐厅一体", + "厨房", + "阳台", + "楼梯", + "储藏", + ].map((name) => ( + + ))} +
    +
    + +
    + 门窗 +
    +
    + + 加门窗 +
    +

    + 外墙门窗按房间外墙推定,后续进入门窗深化校核。 +

    +
    +
    +
    + ) : null} + + {sideTab === "furnish" ? ( +
    + { + if (checked) { + switchMode("furnish"); + } else { + setShowFurniture(false); + switchMode("generate"); + } + }} + /> + +
    + Furniture Items +
    + {visibleFurniture.map((item) => ( +
    + {item.label} + + {roundArea((item.w * item.h) / 1e6).toFixed(1)}㎡ + +
    + ))} +
    +
    +
    + ) : null} + + {sideTab === "checks" ? ( +
    +
    + Summary +
    + + + + + +
    +
    + +
    + 面积偏差 +
    + {plan.warnings.length ? ( + plan.warnings.map((warning) => ( +
    + {warning.msg} +
    + {warning.reason} +
    +
    + )) + ) : ( + + 所有房间面积符合 min~max 范围 + + )} +
    +
    + +
    + 设计笔记 +
    + {plan.designNotes.map((note) => ( +
    + • {note} +
    + ))} +
    +
    + +
    + 构建日志 +
    +                    {`[layout_planner] ${plan.projectId}
    +[2D] blocks=${plan.summary.blockCount}, floors=${plan.floors}
    +[3D] construction_column=${constructionColumn ? "on" : "off"}
    +[review] professional_review_required`}
    +                  
    +
    +
    + ) : null} +
    + +
    + +
    + + + + +
    + {status} +
    +
    + + ); +} + +function PanelTitle({ children }: { children: ReactNode }) { + return ( +

    + {children} +

    + ); +} + +function StageLabel({ children }: { children: ReactNode }) { + return ( +
    + {children} +
    + ); +} + +function DarkField({ + label, + children, +}: { + label: string; + children: ReactNode; +}) { + return ( + + ); +} + +function SummaryRow({ + label, + value, + tone, +}: { + label: string; + value: string; + tone?: "ok" | "warn" | "err"; +}) { + const color = + tone === "ok" + ? "text-emerald-300" + : tone === "warn" + ? "text-amber-300" + : tone === "err" + ? "text-red-300" + : "text-slate-100"; + return ( +
    + {label} + {value} +
    + ); +} + +function ModeSidePanel({ + mode, + activeCandidate, + candidates, + templates, + showFurniture, + furnitureCount, + onTemplate, + onCandidate, + onFurnitureChange, +}: { + mode: PlanFinderMode; + activeCandidate: PlanCandidate | null; + candidates: PlanCandidate[]; + templates: TemplateRegistry; + showFurniture: boolean; + furnitureCount: number; + onTemplate: (templateId: string) => void; + onCandidate: (candidate: PlanCandidate) => void; + onFurnitureChange: (checked: boolean) => void; +}) { + if (mode === "fit") { + return ( +
    + Fit 模板库 +
    + {Object.entries(templates).map(([id, template]) => ( + + ))} +
    +
    + ); + } + + if (mode === "furnish") { + return ( +
    + Furnish 家具布置 +
    +
    + 自动家具层 + +
    +
    + 当前层已布置 {furnitureCount}{" "} + 个家具块,按房间类型自动放置床、沙发、餐桌、厨柜和卫浴洁具。 +
    +
    +
    + ); + } + + if (mode === "manage") { + return ( +
    + Manage 方案库 +
    + {candidates.map((candidate) => ( + + ))} +
    +
    + ); + } + + return ( +
    + Generate 候选 +
    +
    + {activeCandidate?.title ?? "未选择候选"} +
    +
    + {activeCandidate?.summary ?? "设置边界和需求后生成多个方案。"} +
    +
    +
    + ); +} + +function FloatingEditPanel({ + block, + draft, + onDraftChange, + onApply, + onDelete, + onClose, +}: { + block: PlanBlock; + draft: BlockRect; + onDraftChange: (draft: BlockRect) => void; + onApply: () => void; + onDelete: () => void; + onClose: () => void; +}) { + function patch(key: "x0" | "y0" | "w" | "h", value: number | null) { + if (value === null || Number.isNaN(value)) return; + const next = { ...draft, [key]: snap(value) }; + if (key === "x0" || key === "w") next.x1 = next.x0 + next.w; + if (key === "y0" || key === "h") next.y1 = next.y0 + next.h; + onDraftChange(next); + } + + return ( +
    +
    + {block.purpose} +
    +
    + X + patch("x0", Number(value ?? 0))} + /> + Y + patch("y0", Number(value ?? 0))} + /> + + patch("w", Number(value ?? 300))} + /> + + patch("h", Number(value ?? 300))} + /> +
    +
    + {roundArea((draft.w * draft.h) / 1e6).toFixed(2)} ㎡ +
    +
    + + + +
    +
    + ); +} + +function PlanSvg({ + plan, + blocks, + furniture, + showFurniture, + selectedBlockId, + onSelect, +}: { + plan: GeneratedPlan; + blocks: PlanBlock[]; + furniture: FurnitureItem[]; + showFurniture: boolean; + selectedBlockId: string | null; + onSelect: (block: PlanBlock) => void; +}) { + const width = 760; + const height = 560; + const margin = 54; + const [envW, envH] = plan.summary.envelope; + const scale = Math.min( + (width - margin * 2) / envW, + (height - margin * 2) / envH, + ); + const x = (value: number) => margin + value * scale; + const y = (value: number) => height - margin - value * scale; + const gridLines = buildGridLines(envW, envH, 1500); + const minorLines = buildGridLines(envW, envH, 300); + + return ( + + + {minorLines.x.map((value) => ( + + ))} + {minorLines.y.map((value) => ( + + ))} + {gridLines.x.map((value) => ( + + + + {value} + + + ))} + {gridLines.y.map((value) => ( + + + + {value} + + + ))} + + {blocks.map((block) => { + const rect = rectFromBlock(block); + const selected = selectedBlockId === block.id; + return ( + onSelect(block)} + onKeyDown={(event) => { + if (event.key === "Enter" || event.key === " ") onSelect(block); + }} + className="cursor-pointer" + > + + + {block.purpose} + + + {block.areaSqm.toFixed(1)}㎡ + + + ); + })} + {showFurniture + ? furniture.map((item) => ( + + + + {item.label} + + + )) + : null} + + ); +} + +function PlanModel3D({ + plan, + furniture, + showFurniture, + constructionColumn, +}: { + plan: GeneratedPlan; + furniture: FurnitureItem[]; + showFurniture: boolean; + constructionColumn: boolean; +}) { + return ( + + + + + + + + + ); +} + +function CameraLookAt() { + const { camera } = useThree(); + useEffect(() => { + camera.lookAt(0, 3, 0); + camera.updateProjectionMatrix(); + }, [camera]); + return null; +} + +function PlanFrame({ + plan, + furniture, + showFurniture, + constructionColumn, +}: { + plan: GeneratedPlan; + furniture: FurnitureItem[]; + showFurniture: boolean; + constructionColumn: boolean; +}) { + const [envW, envH] = plan.summary.envelope; + const w = envW / 1000; + const d = envH / 1000; + const levelH = 3.2; + const gridX = buildAxisPositions(w, 3); + const gridZ = buildAxisPositions(d, 3); + const floors = Array.from({ length: plan.floors }, (_, index) => index + 1); + + return ( + + {floors.map((floor) => { + const yBase = (floor - 1) * levelH; + const yTop = yBase + levelH; + return ( + + + + + + + + + + {plan.blocks + .filter((block) => block.floor === floor) + .map((block) => { + const rect = rectFromBlock(block); + const bw = rect.w / 1000; + const bd = rect.h / 1000; + const bx = rect.x0 / 1000 + bw / 2; + const bz = rect.y0 / 1000 + bd / 2; + return ( + + + + + ); + })} + {gridX.map((x) => + gridZ.map((z) => ( + + + + + )), + )} + {gridX.map((x) => ( + + + + + ))} + {gridZ.map((z) => ( + + + + + ))} + {showFurniture + ? furniture + .filter((item) => item.floor === floor) + .map((item) => ( + + + + + )) + : null} + + ); + })} + + + + + + ); +} + +function createPlanCandidates(intent: StudioIntent): PlanCandidate[] { + const base = generatePlan(intent); + const mirrored = mirrorPlan(base, intent, "x", "Generate B · 镜像采光"); + const compact = scalePlan(base, intent, 0.94, 1.04, "Fit · 紧凑核心"); + const furnishReady = mirrorPlan( + scalePlan(base, intent, 1.03, 0.96, "Furnish · 家具友好").plan, + intent, + "y", + "Furnish · 家具友好", + ); + const candidates: Array> = [ + { + id: "generate-a", + title: "Generate A · 平衡方案", + command: "Generate", + plan: base, + }, + { + id: "generate-b", + title: "Generate B · 镜像采光", + command: "Generate", + plan: mirrored.plan, + }, + { + id: "fit-c", + title: "Fit C · 模板适配", + command: "Fit", + plan: compact.plan, + }, + { + id: "furnish-d", + title: "Furnish D · 家具友好", + command: "Furnish", + plan: furnishReady.plan, + }, + ]; + return candidates.map((candidate) => ({ + ...candidate, + score: scorePlan(candidate.plan), + summary: candidateSummary(candidate.plan), + })); +} + +function mirrorPlan( + plan: GeneratedPlan, + intent: StudioIntent, + axis: "x" | "y", + projectName: string, +) { + const [envW, envH] = plan.summary.envelope; + const blocks = plan.blocks.map((block) => ({ + ...block, + id: `${block.id}_${axis === "x" ? "mx" : "my"}`, + polygon: block.polygon.map((point) => ({ + x: axis === "x" ? snap(envW - point.x) : point.x, + y: axis === "y" ? snap(envH - point.y) : point.y, + })), + })); + return { + plan: finalizePlan({ + projectId: `${plan.projectId}-${axis}-mirror`, + projectName, + intentLabel: plan.intentLabel, + floors: plan.floors, + blocks: blocks.map((block) => ({ + ...block, + areaSqm: roundArea( + (rectFromBlock(block).w * rectFromBlock(block).h) / 1e6, + ), + })), + targetSqm: plan.summary.targetSqm, + designNotes: [ + ...plan.designNotes.slice(0, 2), + axis === "x" + ? "候选变体:按 X 方向镜像,用于快速比较入口和采光侧。" + : "候选变体:按 Y 方向镜像,用于比较南北动线和家具摆放。", + ], + rooms: intent.rooms, + }), + }; +} + +function scalePlan( + plan: GeneratedPlan, + intent: StudioIntent, + scaleX: number, + scaleY: number, + projectName: string, +) { + const blocks = plan.blocks.map((block) => ({ + ...block, + id: `${block.id}_s${Math.round(scaleX * 100)}${Math.round(scaleY * 100)}`, + polygon: block.polygon.map((point) => ({ + x: Math.max(0, snap(point.x * scaleX)), + y: Math.max(0, snap(point.y * scaleY)), + })), + })); + return { + plan: finalizePlan({ + projectId: `${plan.projectId}-scaled-${Math.round(scaleX * 100)}-${Math.round(scaleY * 100)}`, + projectName, + intentLabel: plan.intentLabel, + floors: plan.floors, + blocks: blocks.map((block) => ({ + ...block, + areaSqm: roundArea( + (rectFromBlock(block).w * rectFromBlock(block).h) / 1e6, + ), + })), + targetSqm: plan.summary.targetSqm, + designNotes: [ + ...plan.designNotes.slice(0, 2), + "候选变体:按外轮廓比例重新适配,模拟 PlanFinder Fit 的库方案贴合。", + ], + rooms: intent.rooms, + }), + }; +} + +function candidateSummary(plan: GeneratedPlan) { + return `${plan.summary.envelope[0]}×${plan.summary.envelope[1]}mm · ${plan.summary.blockCount} 房间 · ${plan.warnings.length} 警告`; +} + +function scorePlan(plan: GeneratedPlan) { + const ratioPenalty = Math.abs(plan.summary.usableRatioEst - 0.83) * 28; + const warningPenalty = plan.warnings.length * 4; + return Math.max( + 68, + Math.min(98, Math.round(95 - ratioPenalty - warningPenalty)), + ); +} + +function buildFurniture(plan: GeneratedPlan): FurnitureItem[] { + return plan.blocks.flatMap((block) => furnitureForBlock(block)); +} + +function furnitureForBlock(block: PlanBlock): FurnitureItem[] { + const rect = rectFromBlock(block); + const base = { + blockId: block.id, + floor: block.floor, + }; + const centerX = rect.x0 + rect.w / 2; + const centerY = rect.y0 + rect.h / 2; + if (["主卧", "次卧"].includes(block.purpose)) { + return [ + { + ...base, + id: `${block.id}-bed`, + label: "床", + x0: snap(rect.x0 + 300), + y0: snap(rect.y0 + 300), + w: Math.min(2100, Math.max(1500, rect.w - 900)), + h: 1800, + color: "#bfdbfe", + }, + { + ...base, + id: `${block.id}-wardrobe`, + label: "柜", + x0: snap(rect.x1 - 900), + y0: snap(rect.y0 + 300), + w: 600, + h: Math.min(2400, Math.max(1200, rect.h - 600)), + color: "#dbeafe", + }, + ]; + } + if (["客厅", "公共区", "客餐厅一体"].includes(block.purpose)) { + return [ + { + ...base, + id: `${block.id}-sofa`, + label: "沙发", + x0: snap(centerX - 1200), + y0: snap(centerY - 600), + w: 2400, + h: 900, + color: "#bbf7d0", + }, + { + ...base, + id: `${block.id}-table`, + label: "几", + x0: snap(centerX - 450), + y0: snap(centerY + 600), + w: 900, + h: 600, + color: "#86efac", + }, + ]; + } + if (block.purpose === "餐厅") { + return [ + { + ...base, + id: `${block.id}-dining`, + label: "餐桌", + x0: snap(centerX - 900), + y0: snap(centerY - 600), + w: 1800, + h: 1200, + color: "#bbf7d0", + }, + ]; + } + if (block.purpose === "厨房") { + return [ + { + ...base, + id: `${block.id}-cabinet`, + label: "橱柜", + x0: rect.x0 + 150, + y0: rect.y0 + 150, + w: Math.max(900, rect.w - 300), + h: 600, + color: "#fde68a", + }, + ]; + } + if (["卫生间", "主卫"].includes(block.purpose)) { + return [ + { + ...base, + id: `${block.id}-bath`, + label: "洁具", + x0: snap(centerX - 450), + y0: snap(centerY - 450), + w: 900, + h: 900, + color: "#e0e7ff", + }, + ]; + } + return []; +} + +function modeDescription(mode: PlanFinderMode) { + if (mode === "fit") return "Fit: 从方案库选择相近户型并适配当前边界。"; + if (mode === "furnish") return "Furnish: 自动放置家具并同步 2D / 3D 家具层。"; + if (mode === "manage") return "Manage: 管理候选方案和可复用模板库。"; + return "Generate: 依据外轮廓和房间需求生成多个候选。"; +} + +function nextModeLabel(mode: PlanFinderMode) { + return planFinderModes.find((item) => item.id === mode)?.label ?? mode; +} + +function candidateCommandForMode( + mode: PlanFinderMode, +): PlanCandidate["command"] | null { + if (mode === "generate") return "Generate"; + if (mode === "fit") return "Fit"; + if (mode === "furnish") return "Furnish"; + return null; +} + +function generatePlan(intent: StudioIntent): GeneratedPlan { + return intent.floors === 2 + ? generateTwoFloorPlan(intent) + : generateSingleFloorPlan(intent); +} + +function generateSingleFloorPlan(intent: StudioIntent): GeneratedPlan { + const rooms = intent.rooms; + const master = rooms.主卧; + const masterBath = rooms.主卫; + const secondary = rooms.次卧; + const wc = rooms.卫生间; + const kitchen = rooms.厨房; + const balcony = rooms.阳台; + const bedCount = Math.max(1, master.count) + secondary.count; + const bathCount = masterBath.count + wc.count; + const publicSplit = + intent.publicSplit === "auto" + ? bedCount <= 2 + ? "lk" + : "lk_sep" + : intent.publicSplit; + const [masterW, privateDepth] = pickMasterDims(master.max || 16); + + const southRooms: Array<{ purpose: string; w: number; idx?: number }> = [ + { purpose: "主卧", w: masterW }, + ]; + if (masterBath.count > 0) { + southRooms.push({ + purpose: "主卫", + w: Math.max(1500, snap((masterBath.max * 1e6) / privateDepth)), + }); + } + for (let index = 0; index < secondary.count; index += 1) { + southRooms.push({ + purpose: "次卧", + w: Math.min( + MAX_SPAN, + Math.max(2400, snap((secondary.max * 1e6) / privateDepth)), + ), + idx: index + 1, + }); + } + + let envelopeW = southRooms.reduce((sum, room) => sum + room.w, 0); + envelopeW = Math.max(envelopeW, bedCount <= 1 ? 6000 : 9000); + const targetInnerArea = intent.totalAreaSqm * DEFAULT_USABLE_RATIO * 1e6; + const northDepth = Math.min( + MAX_SPAN, + snap(Math.max(targetInnerArea / envelopeW - privateDepth, 3000)), + ); + const envelopeH = privateDepth + northDepth; + const kitchenWetArea = kitchen.max + (wc.count > 0 ? wc.max : 0); + const wetW = Math.max( + 1500, + Math.min(envelopeW - 3600, snap((kitchenWetArea * 1e6) / northDepth)), + ); + const publicW = envelopeW - wetW; + const blocks: PlanBlock[] = []; + + let cursor = 0; + for (const room of southRooms) { + const id = room.idx ? `R_${room.purpose}_${room.idx}` : `R_${room.purpose}`; + blocks.push( + rectBlock(id, room.purpose, cursor, 0, cursor + room.w, privateDepth, 1), + ); + cursor += room.w; + } + + if (publicSplit === "lk") { + blocks.push( + rectBlock( + "R_客餐厅一体", + "客餐厅一体", + 0, + privateDepth, + publicW, + envelopeH, + 1, + ), + ); + } else { + const livingW = snap(publicW * 0.6); + blocks.push( + rectBlock("R_客厅", "客厅", 0, privateDepth, livingW, envelopeH, 1), + ); + blocks.push( + rectBlock("R_餐厅", "餐厅", livingW, privateDepth, publicW, envelopeH, 1), + ); + } + + if (wc.count > 0) { + const wcH = Math.max( + 1500, + Math.min(northDepth - 1500, snap((wc.max * 1e6) / wetW)), + ); + blocks.push( + rectBlock( + "R_卫生间", + "卫生间", + publicW, + privateDepth, + envelopeW, + privateDepth + wcH, + 1, + ), + ); + blocks.push( + rectBlock( + "R_厨房", + "厨房", + publicW, + privateDepth + wcH, + envelopeW, + envelopeH, + 1, + ), + ); + } else { + blocks.push( + rectBlock( + "R_厨房", + "厨房", + publicW, + privateDepth, + envelopeW, + envelopeH, + 1, + ), + ); + } + + if (balcony.count > 0) { + blocks.push( + rectBlock( + "R_阳台", + "阳台", + 0, + envelopeH, + Math.min(3600, envelopeW), + envelopeH + 1500, + 1, + ), + ); + } + + return finalizePlan({ + projectId: `ai-plan-${bedCount}bed-${bathCount}bath-${Math.round(intent.totalAreaSqm)}sqm`, + projectName: `AI 模板生成:${Math.round(intent.totalAreaSqm)}㎡ ${bedCount}卧${bathCount}卫`, + intentLabel: `${bedCount}居${publicSplit === "lk" ? "一厅" : "两厅"} ${bathCount}卫`, + floors: 1, + blocks, + targetSqm: intent.totalAreaSqm, + designNotes: [ + `南向(Y 小):主卧${masterBath.count ? "+主卫" : ""} + 次卧×${secondary.count}`, + `北向(Y 大):${publicSplit === "lk" ? "客餐厅一体" : "客厅+餐厅"} + 厨房${wc.count ? "+公卫" : ""}`, + "模板化布局:南卧 + 北公共 + 厨卫角,生成后进入专业复核。", + ], + rooms, + }); +} + +function generateTwoFloorPlan(intent: StudioIntent): GeneratedPlan { + const rooms = intent.rooms; + const bedCount = Math.max(1, rooms.主卧.count) + rooms.次卧.count; + const bathCount = rooms.主卫.count + rooms.卫生间.count; + const footprintTarget = Math.max(100, intent.totalAreaSqm * 1.08); + const envelopeW = snap( + Math.max(12000, Math.sqrt(footprintTarget * 1e6 * 1.33)), + ); + const envelopeH = snap(Math.max(9000, (footprintTarget * 1e6) / envelopeW)); + const c1 = snap(envelopeW * 0.31); + const c2 = snap(envelopeW * 0.55); + const c3 = snap(envelopeW * 0.75); + const r1 = snap(envelopeH * 0.33); + const r2 = snap(envelopeH * 0.62); + const blocks: PlanBlock[] = [ + rectBlock("R_1F_公共区", "公共区", 0, 0, c1, r1, 1), + rectBlock("R_1F_厨房", "厨房", c1, 0, c2, r1, 1), + rectBlock("R_1F_卫生间", "卫生间", c2, 0, c3, r1, 1), + rectBlock("R_1F_楼梯", "楼梯", c3, 0, envelopeW, r1, 1, "双跑"), + rectBlock("R_1F_客厅", "客厅", 0, r1, c2, r2, 1), + rectBlock("R_1F_餐厅", "餐厅", c2, r1, c3, r2, 1), + rectBlock("R_1F_弹性区_A", "弹性区", c3, r1, envelopeW, r2, 1), + rectBlock("R_1F_弹性区_B", "弹性区", 0, r2, c1, envelopeH, 1), + rectBlock("R_1F_弹性区_C", "弹性区", c1, r2, c2, envelopeH, 1), + rectBlock("R_1F_弹性区_D", "弹性区", c2, r2, c3, envelopeH, 1), + rectBlock("R_1F_弹性区_E", "弹性区", c3, r2, envelopeW, envelopeH, 1), + rectBlock("R_2F_主卧", "主卧", 0, 0, c2, r1, 2), + rectBlock("R_2F_主卫", "主卫", c2, 0, c3, r1, 2), + rectBlock("R_2F_楼梯", "楼梯", c3, 0, envelopeW, r1, 2, "双跑"), + rectBlock("R_2F_次卧_1", "次卧", 0, r1, c1, r2, 2), + rectBlock("R_2F_卫生间", "卫生间", c1, r1, c2, r2, 2), + rectBlock("R_2F_次卧_2", "次卧", c2, r1, c3, r2, 2), + rectBlock("R_2F_储藏", "储藏", c3, r1, envelopeW, r2, 2), + rectBlock("R_2F_弹性区_A", "弹性区", 0, r2, c1, envelopeH, 2), + rectBlock("R_2F_弹性区_B", "弹性区", c1, r2, c2, envelopeH, 2), + rectBlock("R_2F_弹性区_C", "弹性区", c2, r2, c3, envelopeH, 2), + rectBlock("R_2F_弹性区_D", "弹性区", c3, r2, envelopeW, envelopeH, 2), + ]; + + for (let index = 2; index < rooms.次卧.count; index += 1) { + const x0 = snap(((index - 2) % 2) * (envelopeW / 2)); + const y0 = snap(envelopeH - 3000 - Math.floor((index - 2) / 2) * 3000); + blocks.push( + rectBlock( + `R_2F_次卧_${index + 1}`, + "次卧", + x0, + y0, + x0 + 3000, + y0 + 3000, + 2, + ), + ); + } + + return finalizePlan({ + projectId: `ai-plan-two-floor-${bedCount}bed-${bathCount}bath-${Math.round(intent.totalAreaSqm)}sqm`, + projectName: `AI 两层户型:${Math.round(intent.totalAreaSqm)}㎡ ${bedCount}卧${bathCount}卫`, + intentLabel: `${bedCount}居两厅 ${bathCount}卫 · 2 层`, + floors: 2, + blocks: + rooms.主卫.count > 0 + ? blocks + : blocks.filter((block) => block.purpose !== "主卫"), + targetSqm: intent.totalAreaSqm, + designNotes: [ + "1F:公共区 + 厨房 + 卫生间 + 楼梯,弹性区等待深化分配。", + "2F:主卧 + 次卧 + 公卫 + 楼梯,上下层楼梯位置完全对齐。", + "3D:按外轮廓生成层板、柱网、梁网和房间底色,后续可进入构件深化。", + ], + rooms, + }); +} + +function finalizePlan({ + projectId, + projectName, + intentLabel, + floors, + blocks, + targetSqm, + designNotes, + rooms, +}: { + projectId: string; + projectName: string; + intentLabel: string; + floors: 1 | 2; + blocks: PlanBlock[]; + targetSqm: number; + designNotes: string[]; + rooms: Record; +}): GeneratedPlan { + const envelope = computeEnvelope(blocks); + const totalRoomSqm = roundArea( + blocks.reduce((sum, block) => sum + block.areaSqm, 0), + ); + const warnings = collectWarnings(blocks, rooms); + const floor1Sqm = roundArea( + blocks + .filter((block) => block.floor === 1) + .reduce((sum, block) => sum + block.areaSqm, 0), + ); + const floor2Sqm = roundArea( + blocks + .filter((block) => block.floor === 2) + .reduce((sum, block) => sum + block.areaSqm, 0), + ); + return { + projectId, + projectName, + intentLabel, + floors, + blocks, + designNotes, + warnings, + summary: { + envelope, + envelopeSqm: roundArea((envelope[0] * envelope[1]) / 1e6), + targetSqm, + totalRoomSqm, + usableRatioEst: targetSqm ? roundArea(totalRoomSqm / targetSqm) : 0, + blockCount: blocks.length, + ...(floors === 2 ? { floor1Sqm, floor2Sqm } : {}), + }, + }; +} + +function normalizePlanFromBlocks( + plan: GeneratedPlan, + blocks: PlanBlock[], +): GeneratedPlan { + return finalizePlan({ + projectId: plan.projectId, + projectName: plan.projectName, + intentLabel: plan.intentLabel, + floors: plan.floors, + blocks, + targetSqm: plan.summary.targetSqm, + designNotes: plan.designNotes, + rooms: initialIntent.rooms, + }); +} + +function collectWarnings( + blocks: PlanBlock[], + rooms: Record, +): PlanWarning[] { + const warnings: PlanWarning[] = []; + for (const block of blocks) { + if (!isRoomKey(block.purpose)) continue; + const cfg = rooms[block.purpose]; + if (!cfg || cfg.count === 0) continue; + if (block.areaSqm > cfg.max * 1.1) { + warnings.push({ + room: block.id, + msg: `${block.id} 实际 ${block.areaSqm.toFixed(1)}㎡ 超过目标 max ${cfg.max}㎡`, + reason: "模数 snap 和短边约束导致,需人工复核。", + }); + } else if (block.areaSqm < cfg.min * 0.9) { + warnings.push({ + room: block.id, + msg: `${block.id} 实际 ${block.areaSqm.toFixed(1)}㎡ 低于目标 min ${cfg.min}㎡`, + reason: "目标面积偏紧或房间被压缩,需人工复核。", + }); + } + } + return warnings; +} + +function computeLiveSummary(intent: StudioIntent) { + const privateKeys: RoomKey[] = [ + "主卧", + "主卫", + "次卧", + "卫生间", + "厨房", + "阳台", + ]; + let privateMin = 0; + let privateMax = 0; + let privateCount = 0; + for (const key of privateKeys) { + const item = intent.rooms[key]; + privateCount += item.count; + privateMin += item.count * item.min; + privateMax += item.count * item.max; + } + const usable = intent.totalAreaSqm * DEFAULT_USABLE_RATIO; + const publicMin = Math.max(0, usable - privateMax); + const publicMax = Math.max(0, usable - privateMin); + let check = "参数合理"; + let tone: "ok" | "warn" | "err" = "ok"; + if (privateMax > usable) { + check = "私密区超总面积"; + tone = "err"; + } else if (publicMax < 14) { + check = "公共区不足"; + tone = "warn"; + } + return { + privateRange: `${privateMin.toFixed(1)} ~ ${privateMax.toFixed(1)} ㎡`, + privateCount, + publicRange: `${publicMin.toFixed(1)} ~ ${publicMax.toFixed(1)} ㎡`, + check, + tone, + }; +} + +function parsePromptToIntent(prompt: string, base: StudioIntent): StudioIntent { + const text = prompt.trim(); + const areaMatch = text.match(/(\d+(?:\.\d+)?)\s*(?:平|㎡|m2|m²)/i); + const bedMatch = text.match(/([一二两三四五六七八九]|\d+)\s*(?:室|居|房)/); + const bathCount = /双卫|两卫|2卫/.test(text) + ? 2 + : /主卧带卫生间|主卫|套卫/.test(text) + ? 2 + : 1; + const hasMasterBath = /主卧带卫生间|主卫|套卫/.test(text); + const split: PublicSplit = /一体|一厅/.test(text) + ? "lk" + : /两厅|大餐厅|餐厅/.test(text) + ? "lk_sep" + : base.publicSplit; + const bedCount = bedMatch + ? parseChineseNumber(bedMatch[1] ?? "3") + : base.rooms.主卧.count + base.rooms.次卧.count; + const totalAreaSqm = areaMatch ? Number(areaMatch[1]) : base.totalAreaSqm; + const floors: 1 | 2 = + /两层|2层|二层|楼梯|复式/.test(text) || totalAreaSqm >= 105 ? 2 : 1; + return { + ...base, + totalAreaSqm, + floors, + publicSplit: split, + rooms: { + ...base.rooms, + 主卧: { + ...base.rooms.主卧, + count: 1, + max: totalAreaSqm >= 120 ? 18 : 16, + }, + 主卫: { + ...base.rooms.主卫, + count: hasMasterBath ? 1 : Math.max(0, bathCount - 1), + }, + 次卧: { ...base.rooms.次卧, count: Math.max(0, bedCount - 1) }, + 卫生间: { ...base.rooms.卫生间, count: 1 }, + 厨房: { ...base.rooms.厨房, count: 1 }, + }, + }; +} + +function parseChineseNumber(value: string) { + const map: Record = { + 一: 1, + 二: 2, + 两: 2, + 三: 3, + 四: 4, + 五: 5, + 六: 6, + 七: 7, + 八: 8, + 九: 9, + }; + return Number(value) || map[value] || 3; +} + +function pickMasterDims( + areaTarget: number, + minShort = 3000, + ratio = 1.2, +): [number, number] { + const targetSide = Math.sqrt((areaTarget * 1e6) / ratio); + const w = Math.max(minShort, Math.min(snap(targetSide), MAX_SPAN)); + const h = Math.max( + minShort, + Math.min(snap((areaTarget * 1e6) / w), MAX_SPAN), + ); + return [w, h]; +} + +function rectBlock( + id: string, + purpose: string, + x0: number, + y0: number, + x1: number, + y1: number, + floor: 1 | 2, + stairKind?: "单跑" | "双跑", +): PlanBlock { + const w = Math.max(MODULUS, x1 - x0); + const h = Math.max(MODULUS, y1 - y0); + return { + id, + purpose, + polygon: rectToPolygon({ x0, y0, x1: x0 + w, y1: y0 + h, w, h }), + areaSqm: roundArea((w * h) / 1e6), + floor, + ...(stairKind ? { stairKind } : {}), + }; +} + +function rectFromBlock(block: PlanBlock): BlockRect { + const xs = block.polygon.map((point) => point.x); + const ys = block.polygon.map((point) => point.y); + const x0 = Math.min(...xs); + const x1 = Math.max(...xs); + const y0 = Math.min(...ys); + const y1 = Math.max(...ys); + return { x0, y0, x1, y1, w: x1 - x0, h: y1 - y0 }; +} + +function rectToPolygon(rect: BlockRect): Point2D[] { + return [ + { x: rect.x0, y: rect.y0 }, + { x: rect.x1, y: rect.y0 }, + { x: rect.x1, y: rect.y1 }, + { x: rect.x0, y: rect.y1 }, + ]; +} + +function computeEnvelope(blocks: PlanBlock[]): [number, number] { + const xs = blocks.flatMap((block) => block.polygon.map((point) => point.x)); + const ys = blocks.flatMap((block) => block.polygon.map((point) => point.y)); + return [snap(Math.max(...xs, 1)), snap(Math.max(...ys, 1))]; +} + +function buildGridLines(w: number, h: number, step: number) { + const xs: number[] = []; + const ys: number[] = []; + for (let value = 0; value <= w; value += step) xs.push(value); + for (let value = 0; value <= h; value += step) ys.push(value); + return { x: xs, y: ys }; +} + +function buildAxisPositions(lengthM: number, stepM: number) { + const values: number[] = []; + for (let value = 0; value <= lengthM + 0.001; value += stepM) + values.push(value); + if (values[values.length - 1] !== lengthM) values.push(lengthM); + return values; +} + +function isRoomKey(value: string): value is RoomKey { + return ["主卧", "主卫", "次卧", "卫生间", "厨房", "阳台"].includes(value); +} + +function snap(value: number) { + return Math.round(value / MODULUS) * MODULUS; +} + +function roundArea(value: number) { + return Math.round(value * 100) / 100; +} + +function safeFileName(value: string) { + return value.replace(/[^\p{L}\p{N}-]+/gu, "-").replace(/^-+|-+$/g, ""); +} diff --git a/03-frontend/components/DigitalTwinOperationsPanel.tsx b/03-frontend/components/DigitalTwinOperationsPanel.tsx index 591ba6e2..7026eb7c 100644 --- a/03-frontend/components/DigitalTwinOperationsPanel.tsx +++ b/03-frontend/components/DigitalTwinOperationsPanel.tsx @@ -5,25 +5,38 @@ import { AimOutlined, ApartmentOutlined, + ArrowDownOutlined, + ArrowLeftOutlined, + ArrowRightOutlined, + ArrowUpOutlined, BarChartOutlined, CheckCircleOutlined, ClusterOutlined, CodeSandboxOutlined, CompassOutlined, + DeleteOutlined, DeploymentUnitOutlined, ExperimentOutlined, EyeOutlined, HeatMapOutlined, + LineChartOutlined, + PauseCircleOutlined, PlayCircleOutlined, + RadarChartOutlined, + ReloadOutlined, SafetyCertificateOutlined, ThunderboltOutlined, + ToolOutlined, + VerticalAlignBottomOutlined, + VerticalAlignTopOutlined, WarningOutlined, } from '@ant-design/icons'; import { Canvas, useFrame } from '@react-three/fiber'; -import { OrbitControls, PerspectiveCamera } from '@react-three/drei'; +import { ContactShadows, Html, OrbitControls, PerspectiveCamera } from '@react-three/drei'; import { Button, Progress, Segmented, Switch, Tag, Tooltip } from 'antd'; import { useMemo, useRef, useState, type ReactNode } from 'react'; import * as THREE from 'three'; +import { DigitalTwinWebGPUViewport } from '@/components/DigitalTwinWebGPUViewport'; import { createModuleAuditEvent } from '@/lib/module-actions'; import type { ModuleAuditEvent } from '@/lib/module-file-system'; import { @@ -33,6 +46,7 @@ import { steelMembers, steelProcessMetrics, steelQualityGates, + steelTwinRuntimeCapabilities, steelSensors, steelSimulationThreads, steelTwinLayers, @@ -40,9 +54,12 @@ import { steelTwinVisualizationReferences, type SteelMember, type SteelMemberStatus, + type SteelMemberTwinGeometry, type SteelSensorPoint, type SteelTwinLayerId, type SteelTwinViewportModeId, + getSteelMemberTwinGeometry, + getSteelSensorTwinPosition, } from '@/lib/digital-twin'; interface DigitalTwinOperationsPanelProps { @@ -52,7 +69,24 @@ interface DigitalTwinOperationsPanelProps { const defaultModeId: SteelTwinViewportModeId = steelTwinViewportModes[0]?.id ?? 'cde_model'; const defaultMemberId = steelMembers[0]?.id ?? ''; -const defaultLayerIds: SteelTwinLayerId[] = ['semantic_ifc', 'iot_scada', 'risk']; +const defaultLayerIds: SteelTwinLayerId[] = [ + 'semantic_ifc', + 'reality_splat', + 'iot_scada', + 'simulation', + 'process', + 'risk', +]; + +interface SteelFrameElement { + id: string; + kind: 'column' | 'beam' | 'brace' | 'deck' | 'truss' | 'outrigger' | 'crane'; + position: [number, number, number]; + size: [number, number, number]; + rotation?: [number, number, number]; + color: string; + opacity?: number; +} export function DigitalTwinOperationsPanel({ onAudit, @@ -64,17 +98,26 @@ export function DigitalTwinOperationsPanel({ ); const [selectedMemberId, setSelectedMemberId] = useState(defaultMemberId); const [progressPlaying, setProgressPlaying] = useState(false); + const [memberGeometryOverrides, setMemberGeometryOverrides] = useState< + Partial> + >({}); + const [hiddenMemberIds, setHiddenMemberIds] = useState>(() => new Set()); const activeMode = steelTwinViewportModes.find((mode) => mode.id === activeModeId) ?? steelTwinViewportModes[0]; + const visibleMembers = steelMembers.filter((member) => !hiddenMemberIds.has(member.id)); const selectedMember = - steelMembers.find((member) => member.id === selectedMemberId) ?? + visibleMembers.find((member) => member.id === selectedMemberId) ?? + visibleMembers[0] ?? steelMembers[0]; + const selectedMemberGeometry = selectedMember + ? getSteelMemberTwinGeometry(selectedMember, memberGeometryOverrides) + : null; const activeLayerCount = activeLayerIds.size; const readinessScore = getSteelTwinReadinessScore(); const blockingIssues = getSteelTwinBlockingIssues(); - const bundledReferences = steelTwinVisualizationReferences.filter((reference) => reference.bundledRuntime); + const runtimeReadyCount = steelTwinRuntimeCapabilities.filter((capability) => capability.status !== 'planned').length; function emit(summary: string) { onAudit?.(createModuleAuditEvent('digital-twin-ops', 'DigitalTwinOperationsPanel', summary)); @@ -105,10 +148,58 @@ export function DigitalTwinOperationsPanel({ function selectMember(memberId: string) { const member = steelMembers.find((item) => item.id === memberId); + if (!member || hiddenMemberIds.has(memberId)) return; setSelectedMemberId(memberId); emit(`数字孪生: 选择构件 ${member?.memberMark ?? memberId}`); } + function moveSelectedMember(axis: 'x' | 'y' | 'z', delta: number) { + if (!selectedMember) return; + setMemberGeometryOverrides((current) => { + const base = current[selectedMember.id] ?? getSteelMemberTwinGeometry(selectedMember); + const position: [number, number, number] = [...base.position]; + const axisIndex = axis === 'x' ? 0 : axis === 'y' ? 1 : 2; + position[axisIndex] = Math.round((position[axisIndex] + delta) * 100) / 100; + return { + ...current, + [selectedMember.id]: { + ...base, + position, + }, + }; + }); + emit(`数字孪生: 移动构件 ${selectedMember.memberMark} ${axis}${delta > 0 ? '+' : ''}${delta}m`); + } + + function resetSelectedMemberGeometry() { + if (!selectedMember) return; + setMemberGeometryOverrides((current) => { + const next = { ...current }; + delete next[selectedMember.id]; + return next; + }); + emit(`数字孪生: 重置构件 ${selectedMember.memberMark} 位姿覆盖`); + } + + function softDeleteSelectedMember() { + if (!selectedMember) return; + setHiddenMemberIds((current) => { + const next = new Set(current); + next.add(selectedMember.id); + return next; + }); + const nextMember = visibleMembers.find((member) => member.id !== selectedMember.id); + if (nextMember) { + setSelectedMemberId(nextMember.id); + } + emit(`数字孪生: 软删除/隐藏构件 ${selectedMember.memberMark}, 待写入 IFC/BCF 变更审批`); + } + + function restoreHiddenMembers() { + setHiddenMemberIds(new Set()); + emit('数字孪生: 恢复本会话隐藏构件'); + } + function togglePlayback() { setProgressPlaying((current) => { const next = !current; @@ -123,130 +214,172 @@ export function DigitalTwinOperationsPanel({ if (variant === 'main') { return ( -
    -
    -
    -
    -

    - DIGITAL TWIN OPS -

    -

    - 重钢结构数字孪生运行面板 -

    -

    - Three.js fallback + IFC/GLB derivative,主视口、图层、构件和传感数据统一在模块主窗口显示。 -

    +
    +
    +
    + +
    + + } + /> +
    + +
    +
    +
    +
    + DIGITAL TWIN OPS + LIVE
    -
    - } /> - } /> - } /> - } /> +
    + + +
    - ({ - label: mode.name, - value: mode.id, - }))} - onChange={(value) => selectMode(value as SteelTwinViewportModeId)} - /> -
    -
    -
    -
    -
    -

    三维主视口

    -

    - {selectedMember.memberMark} · {selectedMember.assembly} -

    -
    - +
    +

    + WebGPU / IFC4.3 / 3DGS / IoT / FEA Runtime +

    +

    + 材料数字化工厂孪生系统 +

    +
    + +
    +
    + {activeMode.name} + {runtimeReadyCount}/{steelTwinRuntimeCapabilities.length} 技术栈
    -
    - - - +
    + + +
    +
    - + + + +
    +
    +
    + {steelTwinViewportModes.map((mode) => ( + + ))} + +
    +
    ); @@ -265,7 +398,7 @@ export function DigitalTwinOperationsPanel({
    - Three.js fallback + WebGPU preferred
    @@ -273,7 +406,7 @@ export function DigitalTwinOperationsPanel({ } /> } /> } /> - } /> + } />
    @@ -325,21 +458,26 @@ export function DigitalTwinOperationsPanel({ -
    - - - -
    + } + />
    @@ -356,7 +494,17 @@ export function DigitalTwinOperationsPanel({ + {selectedMemberGeometry ? ( + + ) : null}
    +
    @@ -365,7 +513,7 @@ export function DigitalTwinOperationsPanel({

    构件树 / 选择

    - {steelMembers.slice(0, 7).map((member) => ( + {visibleMembers.slice(0, 7).map((member) => ( ))} + {hiddenMemberIds.size > 0 ? ( + + ) : null}
    @@ -499,12 +652,30 @@ export function DigitalTwinOperationsPanel({
    -

    参考栈 / 运行时

    +

    孪生技术栈

    +
    +
    + {steelTwinRuntimeCapabilities.map((capability) => ( + + + {capability.name} + + + ))}
    - {steelTwinVisualizationReferences.map((reference) => ( + {steelTwinVisualizationReferences.slice(0, 5).map((reference) => ( - + {reference.name} @@ -537,18 +708,69 @@ export function DigitalTwinOperationsPanel({ ); } +function ThreeTwinFallbackViewport({ + activeLayerIds, + selectedMemberId, + geometryOverrides, + hiddenMemberIds, + progressPlaying, + onSelectMember, + className = '', +}: { + activeLayerIds: Set; + selectedMemberId: string; + geometryOverrides: Partial>; + hiddenMemberIds: ReadonlySet; + progressPlaying: boolean; + onSelectMember: (memberId: string) => void; + className?: string; +}) { + return ( +
    + + + +
    + ); +} + function TwinScene({ activeLayerIds, selectedMemberId, + geometryOverrides, + hiddenMemberIds, progressPlaying, onSelectMember, }: { activeLayerIds: Set; selectedMemberId: string; + geometryOverrides: Partial>; + hiddenMemberIds: ReadonlySet; progressPlaying: boolean; onSelectMember: (memberId: string) => void; }) { const groupRef = useRef(null); + const fixtureElements = useMemo(() => buildFactoryFixtureElements(), []); + const visibleMembers = useMemo( + () => steelMembers.filter((member) => !hiddenMemberIds.has(member.id)), + [hiddenMemberIds], + ); + const visibleSensors = useMemo( + () => steelSensors.filter((sensor) => !hiddenMemberIds.has(sensor.memberId)), + [hiddenMemberIds], + ); const processRoute = useMemo( () => new THREE.CatmullRomCurve3([ @@ -563,115 +785,475 @@ function TwinScene({ useFrame(({ clock }) => { if (groupRef.current) { - groupRef.current.rotation.y = Math.sin(clock.getElapsedTime() * 0.14) * 0.05; if (progressPlaying) { - groupRef.current.position.y = Math.sin(clock.getElapsedTime() * 1.2) * 0.04; + groupRef.current.position.y = Math.sin(clock.getElapsedTime() * 1.2) * 0.025; } } }); return ( <> - - - - - - - - - + + + + + + + + + + + + + + + + - {activeLayerIds.has('semantic_ifc') - ? steelMembers.map((member) => ( + {activeLayerIds.has('semantic_ifc') ? ( + <> + {visibleMembers.map((member) => ( - )) - : null} + ))} + + ) : null} {activeLayerIds.has('risk') - ? steelMembers + ? visibleMembers .filter((member) => member.risk !== 'low') - .map((member) => ) + .map((member) => ( + + )) : null} + {activeLayerIds.has('simulation') ? : null} + {activeLayerIds.has('iot_scada') - ? steelSensors.map((sensor) => ) + ? visibleSensors.map((sensor) => ( + + )) : null} - {activeLayerIds.has('reality_splat') ? : null} + member.id === selectedMemberId) ?? visibleMembers[0]} + geometryOverrides={geometryOverrides} + sensors={visibleSensors.filter((sensor) => sensor.status !== 'normal').slice(0, 3)} + /> - {activeLayerIds.has('process') ? ( - - - - + {activeLayerIds.has('reality_splat') ? ( + <> + + + ) : null} + + {activeLayerIds.has('process') ? : null} - + ); } +function SiteBase() { + return ( + + + + + + + + + + {[-4, -2, 0, 2, 4].map((x) => ( + + + + + ))} + {[-3, -1, 1, 3].map((z) => ( + + + + + ))} + + + + + + ); +} + +function FactoryShell() { + const roofLines = useMemo(() => Array.from({ length: 9 }, (_, index) => -4 + index), []); + const wallRibs = useMemo(() => Array.from({ length: 10 }, (_, index) => -7.2 + index * 1.6), []); + + return ( + + + + + + + + + + + + + + {wallRibs.map((x) => ( + + + + + ))} + {roofLines.map((z) => ( + + + + + ))} + {[-2.4, -1.2, 0, 1.2, 2.4].map((x) => ( + + + + + ))} + + ); +} + +function ProductionTwinLine({ progressPlaying }: { progressPlaying: boolean }) { + const shuttleRef = useRef(null); + + useFrame(({ clock }) => { + if (!shuttleRef.current) return; + const phase = progressPlaying ? clock.getElapsedTime() : 0.8; + shuttleRef.current.position.x = -3.1 + ((Math.sin(phase * 0.72) + 1) / 2) * 6.2; + }); + + return ( + + + + + + + + + + + + + + + + + + + + + {[-4.6, 4.6].map((x) => ( + + + + + + + ))} + {[-3.6, -2.4, -1.2, 0, 1.2, 2.4, 3.6].map((x) => ( + + + + + ))} + + + + + + + + + + + ); +} + +function FactoryMachine({ + position, + accent, +}: { + position: [number, number, number]; + accent: string; +}) { + return ( + + + + + + + + + + + + + + + ); +} + +function RobotArm({ + position, + color, +}: { + position: [number, number, number]; + color: string; +}) { + return ( + + + + + + + + + + + + + + + + + + + ); +} + +function WarehouseRackLayer() { + const bays = useMemo(() => Array.from({ length: 6 }, (_, index) => -3 + index * 1.2), []); + + return ( + + {bays.map((x) => ( + + {[0.8, 1.6, 2.4].map((y) => ( + + + + + ))} + {[-0.48, 0.48].map((sx) => + [-0.54, 0.54].map((sz) => ( + + + + + )), + )} + + ))} + + ); +} + +function FactoryFixtureModel({ elements }: { elements: SteelFrameElement[] }) { + return ( + + {elements.map((element) => ( + + ))} + + ); +} + +function FactoryFixtureElementMesh({ element }: { element: SteelFrameElement }) { + const rotation = element.rotation ?? [0, 0, 0]; + const isDeck = element.kind === 'deck'; + return ( + + + + + + {element.kind === 'crane' || element.kind === 'outrigger' ? ( + + + + + ) : null} + + ); +} + +function SimulationOverlay({ progressPlaying }: { progressPlaying: boolean }) { + const pulseRef = useRef(null); + + useFrame(({ clock }) => { + if (!pulseRef.current) return; + const phase = progressPlaying ? clock.getElapsedTime() : 0.7; + pulseRef.current.children.forEach((child, index) => { + child.scale.setScalar(1 + Math.sin(phase * 2.2 + index * 0.6) * 0.05); + }); + }); + + return ( + + + + + + + + + + + + + + + ); +} + function SteelMemberMesh({ member, + geometryOverrides, selected, onSelect, }: { member: SteelMember; + geometryOverrides: Partial>; selected: boolean; onSelect: (memberId: string) => void; }) { - const rotation = member.rotation ?? [0, 0, 0]; + const geometry = getSteelMemberTwinGeometry(member, geometryOverrides); + const rotation = geometry.rotation ?? [0, 0, 0]; + const bodyOpacity = selected ? 0.96 : memberBodyOpacity(member); return ( - + { event.stopPropagation(); onSelect(member.id); }} > - + {selected ? ( - - - - + <> + + + + + + + + + ) : null} ); } -function RiskEnvelope({ member }: { member: SteelMember }) { - const rotation = member.rotation ?? [0, 0, 0]; +function RiskEnvelope({ + member, + geometryOverrides, +}: { + member: SteelMember; + geometryOverrides: Partial>; +}) { + const geometry = getSteelMemberTwinGeometry(member, geometryOverrides); const color = member.risk === 'high' ? '#ff4d4f' : '#faad14'; + const top: [number, number, number] = [ + geometry.position[0], + geometry.position[1] + geometry.size[1] / 2 + 0.12, + geometry.position[2], + ]; return ( - - - - + + + + + + + + + + ); } -function SensorBeacon({ sensor }: { sensor: SteelSensorPoint }) { +function SensorBeacon({ + sensor, + geometryOverrides, +}: { + sensor: SteelSensorPoint; + geometryOverrides: Partial>; +}) { const color = sensor.status === 'critical' ? '#ff4d4f' @@ -680,26 +1262,92 @@ function SensorBeacon({ sensor }: { sensor: SteelSensorPoint }) { : '#07c160'; return ( - - - - + + + + + + + + + + ); } +function SceneTwinLabels({ + selectedMember, + geometryOverrides, + sensors, +}: { + selectedMember: SteelMember | undefined; + geometryOverrides: Partial>; + sensors: SteelSensorPoint[]; +}) { + return ( + + {selectedMember ? ( + + ) : null} + {sensors.map((sensor) => ( + + ))} + + ); +} + +function FloatingTwinLabel({ + position, + title, + value, + tone, +}: { + position: [number, number, number]; + title: string; + value: string; + tone: TwinHudTone; +}) { + return ( + +
    +

    {title}

    +

    {value}

    +
    + + ); +} + +function liftLabelPosition( + [x, y, z]: [number, number, number], + offset: number, +): [number, number, number] { + return [x, y + offset, z]; +} + function RealitySplatLayer() { const splats = useMemo( () => - Array.from({ length: 64 }, (_, index) => { + Array.from({ length: 180 }, (_, index) => { const angle = index * 0.57; - const radius = 1.2 + (index % 9) * 0.18; + const radius = 1.2 + (index % 13) * 0.2; return { position: [ - Math.cos(angle) * radius, - 1.2 + Math.sin(index * 0.91) * 0.36, - Math.sin(angle) * radius, + Math.cos(angle) * radius + Math.sin(index * 0.17) * 0.4, + 1.05 + Math.sin(index * 0.91) * 0.46 + (index % 5) * 0.12, + Math.sin(angle) * radius + Math.cos(index * 0.23) * 0.28, ] as [number, number, number], - scale: 0.026 + (index % 5) * 0.012, + scale: 0.018 + (index % 5) * 0.009, color: index % 4 === 0 ? '#07c160' : index % 4 === 1 ? '#8fe8b3' : index % 4 === 2 ? '#faad14' : '#9aa4b2', }; }), @@ -711,13 +1359,337 @@ function RealitySplatLayer() { {splats.map((splat, index) => ( - + ))}
    ); } +function PointCloudResidualLayer() { + const points = useMemo( + () => + Array.from({ length: 72 }, (_, index) => { + const x = -4.7 + (index % 12) * 0.82; + const z = -3.1 + Math.floor(index / 12) * 1.12; + return { + position: [ + x + Math.sin(index * 1.7) * 0.08, + 0.06 + Math.abs(Math.sin(index * 0.83)) * 0.22, + z + Math.cos(index * 1.31) * 0.08, + ] as [number, number, number], + critical: index % 17 === 0 || index % 23 === 0, + }; + }), + [], + ); + + return ( + + {points.map((point, index) => ( + + + + + ))} + + ); +} + +function ProcessRouteLayer({ points }: { points: THREE.Vector3[] }) { + return ( + + + + + + {points + .filter((_, index) => index % 8 === 0) + .map((point, index) => ( + + + + + ))} + + ); +} + +function buildFactoryFixtureElements(): SteelFrameElement[] { + const elements: SteelFrameElement[] = []; + + [-4.8, 4.8].forEach((x) => { + elements.push({ + id: `gantry-post-front-${x}`, + kind: 'column', + position: [x, 1.85, -3.35], + size: [0.12, 3.7, 0.12], + color: '#5f7681', + opacity: 0.58, + }); + elements.push({ + id: `gantry-post-back-${x}`, + kind: 'column', + position: [x, 1.85, 3.35], + size: [0.12, 3.7, 0.12], + color: '#5f7681', + opacity: 0.42, + }); + }); + + elements.push( + { + id: 'gantry-rail-front', + kind: 'outrigger', + position: [0, 3.72, -3.35], + size: [10.1, 0.12, 0.16], + color: '#6e8792', + opacity: 0.62, + }, + { + id: 'gantry-rail-back', + kind: 'outrigger', + position: [0, 3.72, 3.35], + size: [10.1, 0.12, 0.16], + color: '#6e8792', + opacity: 0.48, + }, + { + id: 'assembly-table-a', + kind: 'deck', + position: [-2.8, 0.24, 1.55], + size: [2.25, 0.12, 1.1], + color: '#dce8ec', + opacity: 0.5, + }, + { + id: 'assembly-table-b', + kind: 'deck', + position: [0, 0.24, 1.55], + size: [2.25, 0.12, 1.1], + color: '#dce8ec', + opacity: 0.46, + }, + { + id: 'assembly-table-c', + kind: 'deck', + position: [2.8, 0.24, 1.55], + size: [2.25, 0.12, 1.1], + color: '#dce8ec', + opacity: 0.42, + }, + { + id: 'inspection-portal', + kind: 'outrigger', + position: [5.35, 1.35, 0.2], + size: [0.12, 2.7, 1.8], + color: '#1fd6ff', + opacity: 0.36, + }, + { + id: 'crane-boom', + kind: 'crane', + position: [5.55, 2.15, -2.65], + size: [0.11, 4.25, 0.11], + rotation: [0.18, 0, -0.52], + color: '#d98b00', + opacity: 0.82, + }, + ); + + return elements; +} + +type TwinHudTone = 'cyan' | 'green' | 'amber' | 'red'; + +function TwinHudPanel({ + icon, + title, + badge, + children, +}: { + icon: ReactNode; + title: string; + badge: string; + children: ReactNode; +}) { + return ( +
    +
    +
    + {icon} +

    {title}

    +
    + + {badge} + +
    + {children} +
    + ); +} + +function TwinHudStat({ + label, + value, + tone, +}: { + label: string; + value: string; + tone: TwinHudTone; +}) { + return ( +
    +

    {label}

    +

    {value}

    +
    + ); +} + +function TwinHudMetric({ metric }: { metric: (typeof steelProcessMetrics)[number] }) { + return ( +
    +
    +

    {metric.name}

    + +
    +

    + {metric.value} + {metric.unit ? {metric.unit} : null} +

    +
    + ); +} + +function MicroBarChart({ + values, + tone, +}: { + values: number[]; + tone: TwinHudTone; +}) { + return ( +
    + {values.map((value, index) => ( + + ))} +
    + ); +} + +function TrendBars({ values }: { values: number[] }) { + return ( +
    + {values.map((value, index) => ( + + ))} +
    + ); +} + +function TwinHudSensorRow({ sensor }: { sensor: SteelSensorPoint }) { + const tone: TwinHudTone = + sensor.status === 'critical' ? 'red' : sensor.status === 'warning' ? 'amber' : 'green'; + return ( +
    +
    +

    {sensor.name}

    + + {sensor.value} + +
    +
    + {sensor.limit} + {sensor.trend === 'up' ? 'UP' : sensor.trend === 'down' ? 'DOWN' : 'STABLE'} +
    +
    + ); +} + +function TwinHudInfo({ label, value }: { label: string; value: string }) { + return ( +
    + {label} + {value} +
    + ); +} + +function LayerToggleHud({ + layer, + checked, + onChange, +}: { + layer: (typeof steelTwinLayers)[number]; + checked: boolean; + onChange: (checked: boolean) => void; +}) { + return ( +
    +
    +
    +

    {layer.name}

    +

    {layer.standard}

    +
    + +
    +
    + +
    +
    + ); +} + +function hudToneText(tone: TwinHudTone | (typeof steelProcessMetrics)[number]['tone']) { + if (tone === 'red') return 'text-red-300'; + if (tone === 'amber') return 'text-amber-300'; + if (tone === 'green') return 'text-emerald-300'; + return 'text-cyan-300'; +} + +function hudToneDot(tone: TwinHudTone | (typeof steelProcessMetrics)[number]['tone']) { + if (tone === 'red') return 'bg-red-300 shadow-[0_0_10px_rgba(248,113,113,0.7)]'; + if (tone === 'amber') return 'bg-amber-300 shadow-[0_0_10px_rgba(252,211,77,0.7)]'; + if (tone === 'green') return 'bg-emerald-300 shadow-[0_0_10px_rgba(110,231,183,0.7)]'; + return 'bg-cyan-300 shadow-[0_0_10px_rgba(103,232,249,0.7)]'; +} + +function hudToneBar(tone: TwinHudTone) { + if (tone === 'red') return 'bg-[linear-gradient(180deg,#ff8080,#ef4444)] shadow-[0_0_10px_rgba(248,113,113,0.34)]'; + if (tone === 'amber') return 'bg-[linear-gradient(180deg,#ffe082,#f59e0b)] shadow-[0_0_10px_rgba(245,158,11,0.34)]'; + if (tone === 'green') return 'bg-[linear-gradient(180deg,#6ee7b7,#10b981)] shadow-[0_0_10px_rgba(16,185,129,0.34)]'; + return 'bg-[linear-gradient(180deg,#67e8f9,#0ea5e9)] shadow-[0_0_10px_rgba(14,165,233,0.34)]'; +} + +function hudToneBadge(tone: TwinHudTone) { + if (tone === 'red') return 'border-red-300/35 bg-red-500/18 text-red-200'; + if (tone === 'amber') return 'border-amber-300/35 bg-amber-500/18 text-amber-200'; + if (tone === 'green') return 'border-emerald-300/35 bg-emerald-500/18 text-emerald-200'; + return 'border-cyan-300/35 bg-cyan-500/18 text-cyan-100'; +} + function MetricTile({ label, value, @@ -747,6 +1719,59 @@ function InfoPill({ label, value }: { label: string; value: string }) { ); } +function MemberEditControls({ + tone = 'light', + hiddenCount, + onMove, + onReset, + onDelete, + onRestore, +}: { + tone?: 'light' | 'hud'; + hiddenCount: number; + onMove: (axis: 'x' | 'y' | 'z', delta: number) => void; + onReset: () => void; + onDelete: () => void; + onRestore: () => void; +}) { + const panelClass = + tone === 'hud' + ? 'mt-3 rounded border border-cyan-300/15 bg-[#061927]/72 p-2' + : 'arch-card-muted mt-3 rounded-md p-2'; + const titleClass = tone === 'hud' ? 'text-[11px] font-medium text-cyan-200' : 'arch-primary-text text-[11px] font-medium'; + const noteClass = tone === 'hud' ? 'mt-2 text-[10px] leading-4 text-cyan-100/55' : 'arch-muted mt-2 text-[10px] leading-4'; + const hudButtonClass = + tone === 'hud' + ? 'border-cyan-300/25 bg-cyan-950/30 text-cyan-50 hover:!border-cyan-200 hover:!text-white' + : ''; + + return ( +
    +

    构件编辑

    +
    + + + + + + +
    +
    + + +
    + {hiddenCount > 0 ? ( + + ) : null} +

    + 当前为会话内位姿覆盖和软删除,写回 IFC/BCF 前仍需审批。 +

    +
    + ); +} + function SensorRow({ sensor }: { sensor: SteelSensorPoint }) { return (
    @@ -761,6 +1786,10 @@ function SensorRow({ sensor }: { sensor: SteelSensorPoint }) { ); } +function formatPosition([x, y, z]: [number, number, number]) { + return `${x.toFixed(2)}, ${y.toFixed(2)}, ${z.toFixed(2)} m`; +} + function RiskDot({ risk }: { risk: SteelMember['risk'] }) { const className = risk === 'high' @@ -781,11 +1810,18 @@ function memberStatusLabel(status: SteelMemberStatus) { } function memberColor(member: SteelMember) { - if (member.risk === 'high') return '#ff7875'; - if (member.risk === 'medium') return '#faad14'; + if (member.status === 'hold') return '#9c4d4d'; + if (member.status === 'erecting') return '#d89700'; + if (member.risk === 'medium') return '#a9802c'; if (member.status === 'installed') return '#07c160'; - if (member.status === 'in_transit') return '#0fa36b'; - return '#63d69b'; + if (member.status === 'in_transit') return '#5f78a8'; + return '#6f8790'; +} + +function memberBodyOpacity(member: SteelMember) { + if (member.risk === 'high') return 0.18; + if (member.risk === 'medium') return 0.16; + return 0.14; } function metricToneClass(tone: (typeof steelProcessMetrics)[number]['tone']) { diff --git a/03-frontend/components/DigitalTwinWebGPUViewport.tsx b/03-frontend/components/DigitalTwinWebGPUViewport.tsx new file mode 100644 index 00000000..3c09a12a --- /dev/null +++ b/03-frontend/components/DigitalTwinWebGPUViewport.tsx @@ -0,0 +1,439 @@ +// components/DigitalTwinWebGPUViewport.tsx - WebGPU-first heavy steel twin viewport +// License: Apache-2.0 +'use client'; + +import { + ApiOutlined, + DeploymentUnitOutlined, + ThunderboltOutlined, + WarningOutlined, +} from '@ant-design/icons'; +import { Tag, Tooltip } from 'antd'; +import { useEffect, useMemo, useRef, useState, type PointerEvent, type ReactNode } from 'react'; +import { + buildSteelTwinWebGpuScene, + steelTwinWebGpuAdapterManifest, + type SteelMemberTwinGeometry, + type SteelTwinLayerId, + type SteelTwinWebGpuPickTarget, +} from '@/lib/digital-twin'; + +interface DigitalTwinWebGPUViewportProps { + activeLayerIds: ReadonlySet; + selectedMemberId: string; + geometryOverrides?: Partial>; + hiddenMemberIds?: ReadonlySet; + progressPlaying: boolean; + onSelectMember: (memberId: string) => void; + className?: string; + fallback?: ReactNode; +} + +type RuntimeState = + | { mode: 'initializing'; message: string } + | { + mode: 'webgpu'; + message: string; + adapterLabel: string; + featureCount: number; + limitText: string; + vertexCount: number; + } + | { mode: 'fallback'; message: string }; + +interface AdapterInfoLike { + vendor?: string; + architecture?: string; + device?: string; + description?: string; +} + +const vertexStride = 6 * Float32Array.BYTES_PER_ELEMENT; +const gpuBufferUsageVertex = 0x0020; +const gpuBufferUsageCopyDst = 0x0008; + +const steelTwinShader = /* wgsl */ ` +struct VertexOut { + @builtin(position) position: vec4, + @location(0) color: vec4, +}; + +@vertex +fn vertexMain( + @location(0) position: vec2, + @location(1) color: vec4, +) -> VertexOut { + var out: VertexOut; + out.position = vec4(position, 0.0, 1.0); + out.color = color; + return out; +} + +@fragment +fn fragmentMain(@location(0) color: vec4) -> @location(0) vec4 { + return color; +} +`; + +export function DigitalTwinWebGPUViewport({ + activeLayerIds, + selectedMemberId, + geometryOverrides = {}, + hiddenMemberIds = new Set(), + progressPlaying, + onSelectMember, + className = '', + fallback, +}: DigitalTwinWebGPUViewportProps) { + const canvasRef = useRef(null); + const pickTargetsRef = useRef([]); + const activeLayerList = useMemo( + () => Array.from(activeLayerIds).sort(), + [activeLayerIds], + ); + const hasVisualFallback = Boolean(fallback); + const activeLayerKey = activeLayerList.join('|'); + const hiddenMemberKey = useMemo( + () => Array.from(hiddenMemberIds).sort().join('|'), + [hiddenMemberIds], + ); + const geometryOverrideKey = useMemo( + () => + Object.entries(geometryOverrides) + .filter((entry): entry is [string, SteelMemberTwinGeometry] => Boolean(entry[1])) + .sort(([left], [right]) => left.localeCompare(right)) + .map(([id, geometry]) => `${id}:${geometry.position.join(',')}:${geometry.size.join(',')}:${geometry.rotation?.join(',') ?? ''}`) + .join('|'), + [geometryOverrides], + ); + const [runtime, setRuntime] = useState({ + mode: 'initializing', + message: '正在请求 WebGPU Adapter...', + }); + + useEffect(() => { + let cancelled = false; + let animationFrame = 0; + let device: GPUDevice | null = null; + let vertexBuffer: GPUBuffer | null = null; + let vertexBufferByteLength = 0; + + async function start() { + const canvas = canvasRef.current; + const gpu = (navigator as Navigator & { gpu?: GPU }).gpu; + if (!canvas) { + animationFrame = requestAnimationFrame(() => { + void start(); + }); + return; + } + if (!gpu) { + setRuntime({ + mode: 'fallback', + message: `${getWebGpuUnavailableReason()} 已切换到 Three.js 兼容视口。`, + }); + return; + } + const currentCanvas = canvas; + + try { + setRuntime({ + mode: 'initializing', + message: '正在创建 WebGPU device 和 WGSL 渲染管线...', + }); + const adapter = await gpu.requestAdapter({ powerPreference: 'high-performance' }); + if (!adapter) { + throw new Error('WebGPU adapter 不可用。'); + } + + const adapterInfo = await readAdapterInfo(adapter); + const adapterLabel = formatAdapterInfo(adapterInfo); + device = await adapter.requestDevice(); + const currentDevice = device; + const context = currentCanvas.getContext('webgpu') as GPUCanvasContext | null; + if (!context) { + throw new Error('Canvas 无法创建 webgpu context。'); + } + + const format = gpu.getPreferredCanvasFormat(); + context.configure({ + device: currentDevice, + format, + alphaMode: 'premultiplied', + }); + const gpuContext = context; + + const pipeline = currentDevice.createRenderPipeline({ + label: 'ArchIToken steel twin WebGPU pipeline', + layout: 'auto', + vertex: { + module: currentDevice.createShaderModule({ + label: 'ArchIToken steel twin WGSL shader', + code: steelTwinShader, + }), + entryPoint: 'vertexMain', + buffers: [ + { + arrayStride: vertexStride, + attributes: [ + { shaderLocation: 0, offset: 0, format: 'float32x2' }, + { shaderLocation: 1, offset: 2 * Float32Array.BYTES_PER_ELEMENT, format: 'float32x4' }, + ], + }, + ], + }, + fragment: { + module: currentDevice.createShaderModule({ + label: 'ArchIToken steel twin WGSL fragment shader', + code: steelTwinShader, + }), + entryPoint: 'fragmentMain', + targets: [{ format }], + }, + primitive: { + topology: 'triangle-list', + }, + }); + + const featureCount = adapter.features.size; + const limitText = `texture ${adapter.limits.maxTextureDimension2D}px / bind groups ${adapter.limits.maxBindGroups}`; + let metricsReported = false; + + currentDevice.lost + .then((info) => { + if (!cancelled) { + setRuntime({ + mode: 'fallback', + message: `WebGPU device lost: ${info.message || info.reason}`, + }); + } + }) + .catch(() => undefined); + + function renderFrame(now: number) { + if (cancelled || !device) return; + resizeCanvas(currentCanvas); + const scene = buildSteelTwinWebGpuScene({ + activeLayerIds: activeLayerList, + selectedMemberId, + geometryOverrides, + hiddenMemberIds: Array.from(hiddenMemberIds), + progressPhase: progressPlaying ? now / 1000 : 0, + }); + pickTargetsRef.current = scene.pickTargets; + + if (scene.vertices.byteLength > vertexBufferByteLength) { + vertexBuffer?.destroy(); + vertexBufferByteLength = Math.max(scene.vertices.byteLength, vertexStride); + vertexBuffer = device.createBuffer({ + label: 'ArchIToken steel twin vertex buffer', + size: vertexBufferByteLength, + usage: gpuBufferUsageVertex | gpuBufferUsageCopyDst, + }); + } + + if (scene.vertexCount > 0 && vertexBuffer) { + device.queue.writeBuffer(vertexBuffer, 0, scene.vertices); + } + + const encoder = device.createCommandEncoder({ + label: 'ArchIToken steel twin command encoder', + }); + const pass = encoder.beginRenderPass({ + label: 'ArchIToken steel twin render pass', + colorAttachments: [ + { + view: gpuContext.getCurrentTexture().createView(), + clearValue: { r: 0.024, g: 0.071, b: 0.122, a: hasVisualFallback ? 0 : 1 }, + loadOp: 'clear', + storeOp: 'store', + }, + ], + }); + pass.setPipeline(pipeline); + if (scene.vertexCount > 0 && vertexBuffer) { + pass.setVertexBuffer(0, vertexBuffer); + pass.draw(scene.vertexCount); + } + pass.end(); + device.queue.submit([encoder.finish()]); + + if (!metricsReported) { + metricsReported = true; + setRuntime({ + mode: 'webgpu', + message: 'WebGPU 渲染管线运行中', + adapterLabel, + featureCount, + limitText, + vertexCount: scene.vertexCount, + }); + } + + if (progressPlaying) { + animationFrame = requestAnimationFrame(renderFrame); + } + } + + animationFrame = requestAnimationFrame(renderFrame); + } catch (error) { + if (!cancelled) { + setRuntime({ + mode: 'fallback', + message: `${error instanceof Error ? error.message : String(error)} ${getWebGpuUnavailableReason()} 已切换到 Three.js 兼容视口。`, + }); + } + } + } + + void start(); + + return () => { + cancelled = true; + if (animationFrame) cancelAnimationFrame(animationFrame); + vertexBuffer?.destroy(); + device?.destroy(); + pickTargetsRef.current = []; + }; + }, [activeLayerKey, activeLayerList, geometryOverrideKey, geometryOverrides, hasVisualFallback, hiddenMemberIds, hiddenMemberKey, progressPlaying, selectedMemberId]); + + function handlePointerDown(event: PointerEvent) { + if (runtime.mode !== 'webgpu') return; + const rect = event.currentTarget.getBoundingClientRect(); + const x = ((event.clientX - rect.left) / rect.width) * 2 - 1; + const y = -(((event.clientY - rect.top) / rect.height) * 2 - 1); + const target = findPickTarget(pickTargetsRef.current, x, y); + if (target) { + onSelectMember(target.memberId); + } + } + + if (runtime.mode === 'fallback' && fallback) { + return ( +
    + {fallback} + +
    + ); + } + + return ( +
    + {fallback ?
    {fallback}
    : null} + event.preventDefault()} + /> + +
    + {activeLayerList.map((layerId) => ( + + {layerId} + + ))} +
    +
    + ); +} + +function RuntimeBadge({ runtime }: { runtime: RuntimeState }) { + if (runtime.mode === 'webgpu') { + return ( +
    +
    + + WebGPU + + + + + {runtime.featureCount} features · {runtime.vertexCount.toLocaleString()} vertices + + +
    +
    + ); + } + + const icon = runtime.mode === 'fallback' ? : ; + const color = runtime.mode === 'fallback' ? 'warning' : 'processing'; + + return ( +
    + + {icon} {runtime.mode === 'fallback' ? 'Three fallback' : 'WebGPU init'} + +

    + {runtime.message} +

    +

    + {steelTwinWebGpuAdapterManifest.rendererId} · {steelTwinWebGpuAdapterManifest.shaderLanguage.toUpperCase()} +

    +
    + ); +} + +function resizeCanvas(canvas: HTMLCanvasElement) { + const ratio = Math.min(window.devicePixelRatio || 1, 2); + const width = Math.max(1, Math.floor(canvas.clientWidth * ratio)); + const height = Math.max(1, Math.floor(canvas.clientHeight * ratio)); + if (canvas.width !== width || canvas.height !== height) { + canvas.width = width; + canvas.height = height; + } +} + +function getWebGpuUnavailableReason(): string { + const host = window.location.hostname; + const localhost = host === 'localhost' || host === '127.0.0.1' || host === '[::1]'; + const insecureLan = !window.isSecureContext && !localhost; + const firefox = /firefox/i.test(navigator.userAgent); + + if (insecureLan) { + return `当前地址 ${window.location.origin} 不是浏览器认可的 WebGPU 安全来源;请用 localhost、HTTPS 或把该地址加入安全来源白名单。`; + } + if (firefox) { + return '当前 Firefox 未暴露 navigator.gpu;需要启用 WebGPU 配置或改用 Chromium/Edge 的安全来源。'; + } + return '当前浏览器未暴露 navigator.gpu。'; +} + +async function readAdapterInfo(adapter: GPUAdapter): Promise { + const adapterWithInfo = adapter as GPUAdapter & { + info?: AdapterInfoLike; + requestAdapterInfo?: () => Promise; + }; + if (adapterWithInfo.info) return adapterWithInfo.info; + if (adapterWithInfo.requestAdapterInfo) { + return adapterWithInfo.requestAdapterInfo().catch(() => ({})); + } + return {}; +} + +function formatAdapterInfo(info: AdapterInfoLike): string { + return [info.vendor, info.architecture, info.device, info.description] + .filter((item): item is string => Boolean(item)) + .join(' / ') || 'WebGPU adapter'; +} + +function findPickTarget( + targets: readonly SteelTwinWebGpuPickTarget[], + x: number, + y: number, +): SteelTwinWebGpuPickTarget | null { + let best: { target: SteelTwinWebGpuPickTarget; score: number } | null = null; + for (const target of targets) { + const dx = Math.abs(x - target.center[0]); + const dy = Math.abs(y - target.center[1]); + const inside = dx <= target.size[0] / 2 && dy <= target.size[1] / 2; + if (!inside) continue; + const score = dx + dy + target.size[0] * target.size[1] * 0.05; + if (!best || score < best.score) { + best = { target, score }; + } + } + return best === null ? null : best.target; +} diff --git a/03-frontend/components/FeichuanPlanningWorkbench.tsx b/03-frontend/components/FeichuanPlanningWorkbench.tsx new file mode 100644 index 00000000..88f17210 --- /dev/null +++ b/03-frontend/components/FeichuanPlanningWorkbench.tsx @@ -0,0 +1,1697 @@ +// components/FeichuanPlanningWorkbench.tsx - Engineering schedule planning engine +// License: Apache-2.0 +'use client'; + +import { + ArrowLeftOutlined, + BranchesOutlined, + CloudDownloadOutlined, + CloudUploadOutlined, + DownOutlined, + PlayCircleFilled, + PlusOutlined, + SaveOutlined, + SettingOutlined, +} from '@ant-design/icons'; +import { Button } from 'antd'; +import type { MouseEvent as ReactMouseEvent, PointerEvent as ReactPointerEvent } from 'react'; +import { useMemo, useState } from 'react'; +import { createModuleAuditEvent } from '@/lib/module-actions'; +import type { ModuleAuditEvent } from '@/lib/module-file-system'; +import { + applyPlanningScheduleAdjustment, + createDefaultProjectPlanningModel, + createPlanningVersion, + deriveEarnedValueMetrics, + deriveNetworkSchedule, + derivePlanningAnalytics, + derivePlanningStandardsCoverage, + derivePlanningSummary, + deriveResourceLoadAnalysis, + deriveScheduleAlerts, + deriveTaskPlannedProgress, + deriveWorkingCalendarMetrics, + type PlanningTask, + type PlanningTaskStatus, + type ProjectPlanningModel, +} from '@/lib/project-planning-studio'; + +type ScheduleView = 'gantt' | 'time-network' | 'adm' | 'pert' | 'flowchart' | 'mindmap'; +type NetworkView = 'time-network' | 'adm' | 'pert'; +type DiagramView = 'flowchart' | 'mindmap'; +type ScheduleScale = 'day' | 'week' | 'month'; +type ScheduleStatus = 'normal' | 'ahead' | 'warning' | 'delayed' | 'future'; +type AddTaskMode = 'child' | 'after'; +type GraphEditMode = 'progress' | 'task'; + +interface GraphEditState { + taskId: string; + mode: GraphEditMode; + x: number; + y: number; +} + +interface ScheduleTask { + id: string; + code: string; + parentId: string | null; + name: string; + owner: string; + level: number; + start: string; + end: string; + duration: number; + progress: number; + dependencies: string[]; + status: ScheduleStatus; + expanded?: boolean; + earlyStart?: number; + earlyFinish?: number; + lateStart?: number; + lateFinish?: number; + totalFloat?: number; + freeFloat?: number; + expectedDuration?: number; + critical?: boolean; + budgetAmount?: number; + actualCostAmount?: number; +} + +interface VisibleTask extends ScheduleTask { + rowIndex: number; +} + +interface TimelineUnit { + key: string; + label: string; + subLabel: string; + start: Date; + end: Date; + x: number; + width: number; + muted: boolean; +} + +const timelineHeaderHeight = 58; +const taskRowHeight = 56; +const defaultScheduleStart = '2026-05-01'; +const defaultScheduleEnd = '2026-12-31'; +const todayDate = parseDate('2026-05-21'); + +const scaleColumnWidth: Record = { + day: 37, + week: 146, + month: 262, +}; + +const viewLabels: Record = { + gantt: '甘特图', + 'time-network': '时标网络图', + adm: '双代号', + pert: 'PERT图', + flowchart: '流程图', + mindmap: '思维导图', +}; + +const scaleLabels: Record = { + day: '日', + week: '周', + month: '月', +}; + +const statusLabels: Record = { + normal: '正常', + ahead: '提前', + warning: '预警', + delayed: '滞后', + future: '未开始', +}; + +const initialPlanningModel = createDefaultProjectPlanningModel(); + +export function FeichuanPlanningWorkbench({ + onAudit, +}: { + onAudit?: (event: ModuleAuditEvent) => void; +}) { + const [planModel, setPlanModel] = useState(() => initialPlanningModel); + const [view, setView] = useState('gantt'); + const [scale, setScale] = useState('month'); + const [selectedTaskId, setSelectedTaskId] = useState('task-5'); + const [planRange, setPlanRange] = useState({ start: defaultScheduleStart, end: defaultScheduleEnd }); + const [graphEdit, setGraphEdit] = useState(null); + const networkSchedule = useMemo(() => deriveNetworkSchedule(planModel.tasks), [planModel.tasks]); + const summary = useMemo(() => derivePlanningSummary(planModel), [planModel]); + const analytics = useMemo(() => derivePlanningAnalytics(planModel), [planModel]); + const alerts = useMemo(() => deriveScheduleAlerts(planModel), [planModel]); + const coverage = useMemo(() => derivePlanningStandardsCoverage(planModel), [planModel]); + const earnedValue = useMemo(() => deriveEarnedValueMetrics(planModel), [planModel]); + const resourceLoad = useMemo(() => deriveResourceLoadAnalysis(planModel), [planModel]); + const calendarMetrics = useMemo(() => deriveWorkingCalendarMetrics(planModel), [planModel]); + const tasks = useMemo(() => planningModelToScheduleTasks(planModel, networkSchedule), [networkSchedule, planModel]); + const controlDate = useMemo(() => parseDate(planModel.dataDate), [planModel.dataDate]); + const visibleTasks = useMemo(() => deriveVisibleTasks(tasks), [tasks]); + const timeline = useMemo(() => createTimeline(scale, planRange.start, planRange.end), [scale, planRange.end, planRange.start]); + const selectedTask = tasks.find((task) => task.id === selectedTaskId) ?? visibleTasks[0] ?? null; + const graphEditTask = graphEdit ? tasks.find((task) => task.id === graphEdit.taskId) ?? null : null; + const criticalPathLabel = networkSchedule.criticalPathTaskIds + .map((taskId) => tasks.find((task) => task.id === taskId)?.code ?? taskId) + .slice(0, 8) + .join(' -> '); + + function audit(summary: string) { + onAudit?.(createModuleAuditEvent('planning-feichuan-engine', 'FeichuanPlanningWorkbench', summary)); + } + + function toggleTask(taskId: string) { + setPlanModel((current) => ({ + ...current, + tasks: current.tasks.map((task) => (task.id === taskId ? { ...task, isExpanded: !task.isExpanded } : task)), + })); + } + + function updatePlanRange(field: 'start' | 'end', value: string) { + if (!value) return; + setPlanRange((current) => ({ ...current, [field]: value })); + } + + function updateTask(taskId: string, patch: Partial) { + const planningPatch = schedulePatchToPlanningPatch(patch); + const progressOnly = Object.keys(patch).length === 1 && patch.progress !== undefined; + setPlanModel((current) => ({ + ...current, + tasks: current.tasks.map((task) => (task.id === taskId ? { ...task, ...planningPatch } : task)), + auditTrail: progressOnly ? current.auditTrail : [ + { + id: `feichuan-task-edit-${Date.now()}`, + at: new Date().toISOString(), + actor: 'FeichuanPlanningWorkbench', + summary: `图上/表单编辑任务 ${taskId}`, + }, + ...current.auditTrail, + ], + })); + if (!progressOnly) { + audit(`更新进度任务: ${tasks.find((task) => task.id === taskId)?.name ?? taskId}`); + } + } + + function openGraphEditor(taskId: string, event: ReactMouseEvent, mode: GraphEditMode = 'task') { + event.preventDefault(); + event.stopPropagation(); + setSelectedTaskId(taskId); + setGraphEdit({ + taskId, + mode, + x: clampNumber(event.clientX + 10, 12, window.innerWidth - 300), + y: clampNumber(event.clientY + 10, 12, window.innerHeight - 330), + }); + } + + function addTask(mode: AddTaskMode = 'after') { + const selected = selectedTask ?? tasks[0]; + const nextIndex = planModel.tasks.length + 1; + const nextId = `task-${nextIndex}`; + const start = mode === 'child' ? selected?.start ?? planRange.start : shiftDate(selected?.end ?? planRange.start, 1); + const end = shiftDate(start, mode === 'child' ? 14 : 21); + const next: PlanningTask = { + id: nextId, + code: `T-${String(nextIndex).padStart(3, '0')}`, + title: mode === 'child' ? `新增子任务 ${nextIndex}` : `新增后续任务 ${nextIndex}`, + wbsId: planModel.tasks.find((task) => task.id === selected?.id)?.wbsId ?? planModel.wbs[0]?.id ?? 'wbs-1', + owner: selected?.owner ?? '计划工程师', + start, + end, + progress: 0, + dependencies: mode === 'after' && selected ? [selected.id] : [], + dependencyRules: mode === 'after' && selected ? [{ predecessorId: selected.id, type: 'FS', lagDays: 0 }] : [], + parentTaskId: mode === 'child' ? selected?.id ?? 'task-1' : selected?.parentId ?? 'task-1', + outlineLevel: mode === 'child' ? Math.min((selected?.level ?? 1) + 1, 4) : selected?.level ?? 2, + isExpanded: false, + baselineStart: start, + baselineEnd: end, + durationOptimistic: Math.max(1, calculateDuration(start, end) - 3), + durationMostLikely: calculateDuration(start, end), + durationPessimistic: calculateDuration(start, end) + 5, + calendarId: selected ? planModel.tasks.find((task) => task.id === selected.id)?.calendarId ?? planModel.calendars[0]?.id ?? 'cal-johor-site' : planModel.calendars[0]?.id ?? 'cal-johor-site', + resourceDemand: 1, + budgetAmount: Math.max(0, calculateDuration(start, end) * 5200), + actualCostAmount: 0, + approvalRequired: false, + status: 'todo', + resourceId: planModel.resources[0]?.id ?? 'res-pm', + riskId: planModel.risks[0]?.id ?? 'risk-interface', + }; + setPlanModel((current) => ({ + ...current, + tasks: current.tasks.map((task) => ( + mode === 'child' && selected && task.id === selected.id ? { ...task, isExpanded: true } : task + )).concat(next), + auditTrail: [ + { id: `feichuan-task-add-${Date.now()}`, at: new Date().toISOString(), actor: 'FeichuanPlanningWorkbench', summary: `新增任务 ${next.code}` }, + ...current.auditTrail, + ], + })); + setSelectedTaskId(nextId); + audit(`新增柔佛进度任务: ${next.title}`); + } + + function deleteSelectedTask() { + if (!selectedTask || selectedTask.parentId === null) return; + const deletedIds = collectDescendantIds(tasks, selectedTask.id); + const fallback = tasks.find((task) => !deletedIds.has(task.id) && task.id !== selectedTask.id)?.id ?? 'task-1'; + setPlanModel((current) => ({ + ...current, + tasks: current.tasks + .filter((task) => !deletedIds.has(task.id)) + .map((task) => { + const dependencyRules = task.dependencyRules?.filter((dependency) => !deletedIds.has(dependency.predecessorId)); + return { + ...task, + dependencies: task.dependencies.filter((dependency) => !deletedIds.has(dependency)), + ...(dependencyRules ? { dependencyRules } : {}), + }; + }), + milestones: current.milestones.map((milestone) => ({ + ...milestone, + linkedTaskIds: milestone.linkedTaskIds.filter((taskId) => !deletedIds.has(taskId)), + })), + auditTrail: [ + { id: `feichuan-task-delete-${Date.now()}`, at: new Date().toISOString(), actor: 'FeichuanPlanningWorkbench', summary: `删除任务 ${selectedTask.name}` }, + ...current.auditTrail, + ], + })); + setSelectedTaskId(fallback); + setGraphEdit(null); + audit(`删除进度任务: ${selectedTask.name}`); + } + + function savePlanningVersion() { + setPlanModel((current) => createPlanningVersion(current, 'FeichuanPlanningWorkbench', '飞椽计划图表与网络参数在线保存')); + audit('保存飞椽进度计划版本'); + } + + function applySelectedTaskAdjustment(shiftDays: number) { + if (!selectedTask) return; + setPlanModel((current) => applyPlanningScheduleAdjustment(current, { + taskIds: [selectedTask.id], + shiftDays, + reason: shiftDays > 0 ? '图上计划顺延调整。' : '图上计划赶工调整。', + actor: '计划工程师', + includeSuccessors: true, + })); + audit(`${selectedTask.name} ${shiftDays > 0 ? '顺延' : '赶工'} ${Math.abs(shiftDays)} 天并影响后续任务`); + } + + function changeView(next: ScheduleView) { + setView(next); + audit(`切换工程进度视图: ${viewLabels[next]}`); + } + + return ( +
    +
    +
    +
    +
    + 缩放 + + 时间选择 +
    + updatePlanRange('start', event.target.value)} + /> + + updatePlanRange('end', event.target.value)} + /> +
    + + + + +
    +
    + +
    +
    + {(Object.keys(viewLabels) as ScheduleView[]).map((item) => ( + + ))} +
    + +
    + + + + alert.severity === 'high' || alert.severity === 'critical').length} + criticalPathLabel={criticalPathLabel} + coverageGapCount={coverage.filter((item) => item.status === 'gap').length} + dependencyWarnings={networkSchedule.dependencyWarnings.length} + /> + + {view === 'gantt' ? ( + + ) : isNetworkView(view) ? ( + task.level >= 3)} + timeline={timeline} + selectedTask={selectedTask} + onSelectTask={setSelectedTaskId} + onAddTask={addTask} + onUpdateTask={updateTask} + onOpenGraphEditor={openGraphEditor} + onDeleteTask={deleteSelectedTask} + /> + ) : ( + + )} + setGraphEdit(null)} + onUpdateTask={updateTask} + /> +
    + ); +} + +function ScaleButtons({ + scale, + onChange, + compact = false, +}: { + scale: ScheduleScale; + onChange: (scale: ScheduleScale) => void; + compact?: boolean; +}) { + return ( +
    + {(Object.keys(scaleLabels) as ScheduleScale[]).map((item) => ( + + ))} +
    + ); +} + +function PlanningControlStrip({ + summary, + analytics, + earnedValue, + resourceLoad, + calendarMetrics, + alertCount, + highAlertCount, + criticalPathLabel, + coverageGapCount, + dependencyWarnings, +}: { + summary: ReturnType; + analytics: ReturnType; + earnedValue: ReturnType; + resourceLoad: ReturnType; + calendarMetrics: ReturnType; + alertCount: number; + highAlertCount: number; + criticalPathLabel: string; + coverageGapCount: number; + dependencyWarnings: number; +}) { + return ( +
    + SPI{analytics.schedulePerformanceIndex} + CPI{earnedValue.costPerformanceIndex} + 计划应达{summary.plannedProgress}% + 实际均值{summary.averageProgress}% + PV/EV{formatCompactMoney(earnedValue.plannedValue)} / {formatCompactMoney(earnedValue.earnedValue)} + 0 ? 'is-warning' : ''}>预警{alertCount} 条 + 0 ? 'is-danger' : ''}>高风险{highAlertCount} 条 + 预测完成{analytics.forecastFinish} + 0 ? 'is-warning' : ''}>资源峰值{resourceLoad.peakResourceName} {resourceLoad.peakUtilizationPercent}% + 工作日历{calendarMetrics.workingDayCount} 工日 + 0 ? 'is-warning' : ''}>标准缺口{coverageGapCount} 项 + 0 ? 'is-warning' : ''}>网络校核{dependencyWarnings} 条 + 关键路径{criticalPathLabel || '未识别'} +
    + ); +} + +function GanttPlanner({ + tasks, + visibleTasks, + timeline, + dataDate, + selectedTaskId, + selectedTask, + onAddTask, + onSelectTask, + onToggleTask, + onUpdateTask, + onOpenGraphEditor, + onDeleteTask, +}: { + tasks: ScheduleTask[]; + visibleTasks: VisibleTask[]; + timeline: TimelineUnit[]; + dataDate: Date; + selectedTaskId: string; + selectedTask: ScheduleTask | null; + onAddTask: (mode?: AddTaskMode) => void; + onSelectTask: (taskId: string) => void; + onToggleTask: (taskId: string) => void; + onUpdateTask: (taskId: string, patch: Partial) => void; + onOpenGraphEditor: (taskId: string, event: ReactMouseEvent, mode: GraphEditMode) => void; + onDeleteTask: () => void; +}) { + const layout = createGanttLayout(visibleTasks, timeline); + const activeTask = visibleTasks.find((task) => task.id === selectedTaskId) ?? visibleTasks[0]; + + function updateProgressFromPointer(element: HTMLElement, clientX: number, taskId: string) { + const rect = element.getBoundingClientRect(); + const progress = clampNumber(Math.round(((clientX - rect.left) / Math.max(1, rect.width)) * 100), 0, 100); + onUpdateTask(taskId, { progress }); + } + + function handleBarPointerDown(event: ReactPointerEvent, task: ScheduleTask) { + if (event.button !== 0 || event.detail > 1) return; + event.preventDefault(); + event.stopPropagation(); + onSelectTask(task.id); + + const element = event.currentTarget; + element.setPointerCapture(event.pointerId); + + const handlePointerMove = (moveEvent: PointerEvent) => { + updateProgressFromPointer(element, moveEvent.clientX, task.id); + }; + const handlePointerDone = () => { + window.removeEventListener('pointermove', handlePointerMove); + window.removeEventListener('pointerup', handlePointerDone); + window.removeEventListener('pointercancel', handlePointerDone); + try { + element.releasePointerCapture(event.pointerId); + } catch { + // The pointer may already be released by the browser. + } + }; + + window.addEventListener('pointermove', handlePointerMove); + window.addEventListener('pointerup', handlePointerDone, { once: true }); + window.addEventListener('pointercancel', handlePointerDone, { once: true }); + } + + return ( +
    + + +
    + +
    +
    + + + + + + + + {layout.links.map((link) => ( + + ))} + + + + {layout.bars.map((bar) => ( +
    + +
    + ))} +
    +
    +
    +
    + ); +} + +function NetworkPlanner({ + view, + tasks, + timeline, + selectedTask, + onSelectTask, + onAddTask, + onUpdateTask, + onOpenGraphEditor, + onDeleteTask, +}: { + view: NetworkView; + tasks: ScheduleTask[]; + timeline: TimelineUnit[]; + selectedTask: ScheduleTask | null; + onSelectTask: (taskId: string) => void; + onAddTask: (mode?: AddTaskMode) => void; + onUpdateTask: (taskId: string, patch: Partial) => void; + onOpenGraphEditor: (taskId: string, event: ReactMouseEvent, mode: GraphEditMode) => void; + onDeleteTask: () => void; +}) { + const layout = createNetworkLayout(tasks, timeline, view); + + return ( +
    + +
    + +
    +
    + + + + + + + + + + + {layout.edges.map((edge) => ( + + ))} + {layout.nodes.map((node, index) => { + const hitbox = networkNodeHitbox(node, view); + return ( + onSelectTask(node.task.id)} + onDoubleClick={(event) => onOpenGraphEditor(node.task.id, event, 'task')} + > + {view === 'time-network' ? ( + + ) : view === 'adm' ? ( + + ) : ( + + )} + { + event.stopPropagation(); + onSelectTask(node.task.id); + }} + onDoubleClick={(event) => onOpenGraphEditor(node.task.id, event, 'task')} + /> + + ); + })} + + {view === 'pert' ? ( +
    +
    最早开始时间
    (ES)
    工期 (DU)最早完成时间
    (EF)
    + 活动名称 +
    最迟开始时间
    (LS)
    总浮动时间
    (TF)
    最迟完成时间
    (LF)
    +
    + ) : null} +
    +
    +
    +
    + ); +} + +function InlineTaskEditor({ + task, + onAddTask, + onDeleteTask, + onUpdateTask, + onAdjustTask, +}: { + task: ScheduleTask | null; + onAddTask: (mode?: AddTaskMode) => void; + onDeleteTask: () => void; + onUpdateTask: (taskId: string, patch: Partial) => void; + onAdjustTask: (shiftDays: number) => void; +}) { + if (!task) return null; + + return ( +
    + 当前编辑 + onUpdateTask(task.id, { name: event.target.value })} + /> + onUpdateTask(task.id, { start: event.target.value })} + /> + onUpdateTask(task.id, { end: event.target.value })} + /> + + + + + + + +
    + ); +} + +function TaskEditor({ + task, + onAddTask, + onDeleteTask, + onUpdateTask, +}: { + task: ScheduleTask | null; + onAddTask: (mode?: AddTaskMode) => void; + onDeleteTask: () => void; + onUpdateTask: (taskId: string, patch: Partial) => void; +}) { + if (!task) { + return
    未选择任务
    ; + } + + return ( +
    +
    + 任务在线编制 + {task.id} +
    + +
    + + +
    +
    + + +
    +
    + + +
    +
    + + + +
    +
    + ); +} + +function GraphInlineEditor({ + state, + task, + onClose, + onUpdateTask, +}: { + state: GraphEditState | null; + task: ScheduleTask | null; + onClose: () => void; + onUpdateTask: (taskId: string, patch: Partial) => void; +}) { + if (!state || !task) return null; + + const title = state.mode === 'progress' ? '图上编辑进度' : '图上编辑任务'; + + return ( +
    event.stopPropagation()} + > +
    + {title} + +
    + +
    + + +
    +
    + + +
    +
    + +
    +
    + ); +} + +function DiagramPlanner({ + view, + tasks, + visibleTasks, + selectedTask, + onSelectTask, + onAddTask, + onUpdateTask, + onOpenGraphEditor, + onDeleteTask, +}: { + view: DiagramView; + tasks: ScheduleTask[]; + visibleTasks: VisibleTask[]; + selectedTask: ScheduleTask | null; + onSelectTask: (taskId: string) => void; + onAddTask: (mode?: AddTaskMode) => void; + onUpdateTask: (taskId: string, patch: Partial) => void; + onOpenGraphEditor: (taskId: string, event: ReactMouseEvent, mode: GraphEditMode) => void; + onDeleteTask: () => void; +}) { + const layout = createDiagramLayout(visibleTasks, view); + + return ( +
    + +
    +
    + {viewLabels[view]}在线编制画布 + 节点 {tasks.length} + + +
    +
    + + + + + + + {layout.edges.map((edge) => ( + + ))} + {layout.nodes.map((node) => ( + + + + ))} + +
    +
    +
    + ); +} + +function TimeNetworkNode({ node }: { node: NetworkNode }) { + return ( + <> + + + + {node.task.name} {node.task.progress}% + + ); +} + +function AdmNode({ node, index }: { node: NetworkNode; index: number }) { + return ( + <> + + {index + 1} + {node.task.name} + {node.task.duration}天 + + ); +} + +function PertNode({ node, index }: { node: NetworkNode; index: number }) { + return ( + <> + + {index + 1} + +
    +
    {node.task.earlyStart ?? 0}{node.task.expectedDuration ?? node.task.duration}{node.task.earlyFinish ?? node.task.duration}
    + {node.task.name} +
    {node.task.lateStart ?? 0}{node.task.totalFloat ?? 0}{node.task.lateFinish ?? node.task.duration}
    +
    +
    + + ); +} + +function networkNodeHitbox(node: NetworkNode, view: NetworkView) { + if (view === 'time-network') { + return { + x: node.x, + y: node.y - 18, + width: Math.max(node.width + 230, 260), + height: 36, + }; + } + + if (view === 'pert') { + return { + x: node.x - 22, + y: node.y - 48, + width: 210, + height: 96, + }; + } + + return { + x: node.x - 22, + y: node.y - 28, + width: 220, + height: 56, + }; +} + +function TimelineHeader({ timeline }: { timeline: TimelineUnit[] }) { + return ( +
    +
    + {groupTimelineByMonth(timeline).map((group) => ( + {group.label} + ))} +
    +
    + {timeline.map((unit) => ( + {unit.label}{unit.subLabel} + ))} +
    +
    + ); +} + +function TimelineGrid({ timeline, height }: { timeline: TimelineUnit[]; height: number }) { + return ( + <> + {timeline.map((unit) => ( +
    + ))} + + ); +} + +function LineMarker({ date, timeline, label, className }: { date: Date; timeline: TimelineUnit[]; label: string; className: string }) { + const x = dateToX(date, timeline); + return ( +
    + {label} +
    + ); +} + +interface NetworkNode { + task: ScheduleTask; + x: number; + y: number; + width: number; + color: string; + fill: string; +} + +interface DiagramNode { + task: ScheduleTask; + x: number; + y: number; + width: number; + height: number; +} + +interface DiagramEdge { + id: string; + d: string; + kind: 'tree' | 'dependency'; +} + +function createGanttLayout(visibleTasks: VisibleTask[], timeline: TimelineUnit[]) { + const width = timeline.at(-1) ? timeline.at(-1)!.x + timeline.at(-1)!.width : 1200; + const height = timelineHeaderHeight + visibleTasks.length * taskRowHeight + 40; + const bars = visibleTasks.map((task) => ({ + task, + x: dateToX(parseDate(task.start), timeline), + y: timelineHeaderHeight + task.rowIndex * taskRowHeight + 13, + width: Math.max(24, dateToX(parseDate(task.end), timeline) - dateToX(parseDate(task.start), timeline)), + })); + const barByTaskId = new Map(bars.map((bar) => [bar.task.id, bar])); + const links = bars.flatMap((bar) => bar.task.dependencies.flatMap((dependencyId) => { + const from = barByTaskId.get(dependencyId); + if (!from) return []; + const sx = from.x + from.width; + const sy = from.y + 13; + const tx = bar.x; + const ty = bar.y + 13; + const mid = Math.max(sx + 20, (sx + tx) / 2); + return [{ id: `${dependencyId}-${bar.task.id}`, d: `M ${sx} ${sy} L ${mid} ${sy} L ${mid} ${ty} L ${tx} ${ty}` }]; + })); + return { width, height, bars, links }; +} + +function createNetworkLayout(tasks: ScheduleTask[], timeline: TimelineUnit[], view: NetworkView) { + const width = Math.max(timeline.at(-1) ? timeline.at(-1)!.x + timeline.at(-1)!.width : 1320, 1320); + const rowGap = view === 'time-network' ? 54 : view === 'adm' ? 64 : 82; + const height = Math.max(760, timelineHeaderHeight + tasks.length * rowGap + 180); + const nodes: NetworkNode[] = tasks.map((task, index) => { + const durationWidth = Math.max(44, dateToX(parseDate(task.end), timeline) - dateToX(parseDate(task.start), timeline)); + const x = view === 'time-network' + ? dateToX(parseDate(task.start), timeline) + : 80 + Math.max(0, index % 8) * 180 + Math.floor(index / 8) * 80; + const y = timelineHeaderHeight + 96 + (view === 'time-network' ? index * rowGap : Math.floor(index / 8) * 170 + (index % 2) * 56); + return { + task, + x, + y, + width: durationWidth, + color: taskColor(task.status), + fill: taskFill(task.status), + }; + }); + const nodeById = new Map(nodes.map((node) => [node.task.id, node])); + const edges = nodes.flatMap((node) => node.task.dependencies.flatMap((dependencyId) => { + const source = nodeById.get(dependencyId); + if (!source) return []; + const sx = view === 'time-network' ? source.x + source.width : source.x + 17; + const sy = source.y; + const tx = view === 'time-network' ? node.x : node.x - 17; + const ty = node.y; + const mid = Math.max(sx + 26, (sx + tx) / 2); + return [{ id: `${dependencyId}-${node.task.id}`, d: `M ${sx} ${sy} L ${mid} ${sy} L ${mid} ${ty} L ${tx} ${ty}` }]; + })); + return { width, height, nodes, edges }; +} + +function createDiagramLayout(tasks: VisibleTask[], view: DiagramView) { + const width = view === 'mindmap' ? 1480 : 1280; + const height = view === 'mindmap' ? Math.max(960, tasks.length * 78) : Math.max(760, tasks.length * 52 + 160); + const childrenByParent = new Map(); + for (const task of tasks) { + const children = childrenByParent.get(task.parentId) ?? []; + children.push(task); + childrenByParent.set(task.parentId, children); + } + + const nodes = view === 'mindmap' + ? createMindMapNodes(tasks, childrenByParent) + : createFlowchartNodes(tasks); + const nodeById = new Map(nodes.map((node) => [node.task.id, node])); + const edges: DiagramEdge[] = []; + + for (const node of nodes) { + if (node.task.parentId) { + const parent = nodeById.get(node.task.parentId); + if (parent) { + edges.push({ + id: `${node.task.parentId}-${node.task.id}`, + d: diagramEdgePath(parent, node, view), + kind: 'tree', + }); + } + } + for (const dependency of node.task.dependencies) { + const source = nodeById.get(dependency); + if (source && source.task.id !== node.task.parentId) { + edges.push({ + id: `${dependency}-${node.task.id}-dependency`, + d: diagramEdgePath(source, node, view), + kind: 'dependency', + }); + } + } + } + + return { width, height, nodes, edges }; +} + +function createFlowchartNodes(tasks: VisibleTask[]): DiagramNode[] { + const levelCount = new Map(); + return tasks.map((task) => { + const index = levelCount.get(task.level) ?? 0; + levelCount.set(task.level, index + 1); + return { + task, + x: 54 + (task.level - 1) * 270, + y: 72 + index * 86, + width: task.level === 1 ? 250 : 220, + height: 58, + }; + }); +} + +function createMindMapNodes( + tasks: VisibleTask[], + childrenByParent: Map, +): DiagramNode[] { + const nodes: DiagramNode[] = []; + const nodeById = new Map(); + const root = tasks.find((task) => task.parentId === null) ?? tasks[0]; + if (!root) return nodes; + + const branches = childrenByParent.get(root.id) ?? []; + const branchBlocks = branches.map((branch) => Math.max(140, ((childrenByParent.get(branch.id) ?? []).length || 1) * 82 + 36)); + const rootCenterY = 360; + let cursorY = 100; + + const rootNode: DiagramNode = { task: root, x: 70, y: rootCenterY - 34, width: 250, height: 68 }; + nodes.push(rootNode); + nodeById.set(root.id, rootNode); + + branches.forEach((branch, branchIndex) => { + const blockHeight = branchBlocks[branchIndex] ?? 140; + const branchY = cursorY + blockHeight / 2 - 31; + const branchNode: DiagramNode = { task: branch, x: 390, y: branchY, width: 230, height: 62 }; + nodes.push(branchNode); + nodeById.set(branch.id, branchNode); + placeMindMapChildren(branch, childrenByParent, nodes, nodeById, 660, branchY + 8); + cursorY += blockHeight + 36; + }); + + return nodes; +} + +function placeMindMapChildren( + parent: VisibleTask, + childrenByParent: Map, + nodes: DiagramNode[], + nodeById: Map, + x: number, + centerY: number, +) { + const children = childrenByParent.get(parent.id) ?? []; + children.forEach((child, index) => { + const y = centerY + (index - (children.length - 1) / 2) * 74; + const node: DiagramNode = { task: child, x, y, width: 230, height: 58 }; + nodes.push(node); + nodeById.set(child.id, node); + placeMindMapChildren(child, childrenByParent, nodes, nodeById, x + 270, y); + }); +} + +function diagramEdgePath(source: DiagramNode, target: DiagramNode, view: DiagramView): string { + const sx = source.x + source.width; + const sy = source.y + source.height / 2; + const tx = target.x; + const ty = target.y + target.height / 2; + if (view === 'mindmap') { + const c1 = sx + Math.max(60, (tx - sx) / 2); + const c2 = tx - Math.max(60, (tx - sx) / 2); + return `M ${sx} ${sy} C ${c1} ${sy}, ${c2} ${ty}, ${tx} ${ty}`; + } + const mid = Math.max(sx + 34, (sx + tx) / 2); + return `M ${sx} ${sy} L ${mid} ${sy} L ${mid} ${ty} L ${tx} ${ty}`; +} + +function planningModelToScheduleTasks( + model: ProjectPlanningModel, + networkSchedule: ReturnType, +): ScheduleTask[] { + const analysisByTaskId = new Map(networkSchedule.taskAnalyses.map((analysis) => [analysis.taskId, analysis])); + return model.tasks.map((taskItem) => { + const analysis = analysisByTaskId.get(taskItem.id); + return { + id: taskItem.id, + code: taskItem.code, + parentId: taskItem.parentTaskId ?? null, + name: taskItem.title, + owner: taskItem.owner, + level: taskItem.outlineLevel ?? 2, + start: taskItem.start, + end: taskItem.end, + duration: calculateDuration(taskItem.start, taskItem.end), + progress: taskItem.progress, + dependencies: taskItem.dependencies, + status: mapPlanningStatusToScheduleStatus(taskItem, model.dataDate), + expanded: taskItem.isExpanded ?? true, + earlyStart: analysis?.earlyStartOffset ?? 0, + earlyFinish: analysis?.earlyFinishOffset ?? calculateDuration(taskItem.start, taskItem.end), + lateStart: analysis?.lateStartOffset ?? 0, + lateFinish: analysis?.lateFinishOffset ?? calculateDuration(taskItem.start, taskItem.end), + totalFloat: analysis?.totalFloatDays ?? 0, + freeFloat: analysis?.freeFloatDays ?? 0, + expectedDuration: analysis?.expectedDurationDays ?? calculateDuration(taskItem.start, taskItem.end), + critical: analysis?.isCritical ?? false, + budgetAmount: taskItem.budgetAmount ?? 0, + actualCostAmount: taskItem.actualCostAmount ?? 0, + }; + }); +} + +function mapPlanningStatusToScheduleStatus(taskItem: PlanningTask, dataDate: string): ScheduleStatus { + if (taskItem.status === 'done' || taskItem.progress >= 100) return 'ahead'; + if (taskItem.status === 'blocked') return 'delayed'; + const plannedProgress = deriveTaskPlannedProgress(taskItem, dataDate); + if (plannedProgress === 0 && taskItem.progress === 0) return 'future'; + const delta = plannedProgress - taskItem.progress; + if (delta >= 25) return 'delayed'; + if (delta >= 10) return 'warning'; + return 'normal'; +} + +function mapScheduleStatusToPlanningStatus(status: ScheduleStatus, progress?: number): PlanningTaskStatus { + if (progress !== undefined && progress >= 100) return 'done'; + if (status === 'delayed') return 'blocked'; + if (status === 'future') return 'todo'; + return 'doing'; +} + +function schedulePatchToPlanningPatch(patch: Partial): Partial { + const next: Partial = {}; + if (patch.name !== undefined) next.title = patch.name; + if (patch.owner !== undefined) next.owner = patch.owner; + if (patch.start !== undefined) { + next.start = patch.start; + next.baselineStart = patch.start; + } + if (patch.end !== undefined) { + next.end = patch.end; + next.baselineEnd = patch.end; + } + if (patch.progress !== undefined) next.progress = clampNumber(Math.round(patch.progress), 0, 100); + if (patch.budgetAmount !== undefined) next.budgetAmount = Math.max(0, Math.round(patch.budgetAmount)); + if (patch.actualCostAmount !== undefined) next.actualCostAmount = Math.max(0, Math.round(patch.actualCostAmount)); + if (patch.dependencies !== undefined) { + next.dependencies = patch.dependencies; + next.dependencyRules = patch.dependencies.map((predecessorId) => ({ predecessorId, type: 'FS', lagDays: 0 })); + } + if (patch.parentId !== undefined) next.parentTaskId = patch.parentId; + if (patch.level !== undefined) next.outlineLevel = patch.level; + if (patch.expanded !== undefined) next.isExpanded = patch.expanded; + if (patch.status !== undefined) { + next.status = mapScheduleStatusToPlanningStatus(patch.status, patch.progress); + } else if (patch.progress !== undefined) { + next.status = mapScheduleStatusToPlanningStatus('normal', patch.progress); + } + return next; +} + +function deriveVisibleTasks(tasks: ScheduleTask[]): VisibleTask[] { + const result: VisibleTask[] = []; + const childrenByParent = new Map(); + for (const task of tasks) { + const children = childrenByParent.get(task.parentId) ?? []; + children.push(task); + childrenByParent.set(task.parentId, children); + } + const visit = (task: ScheduleTask) => { + result.push({ ...task, rowIndex: result.length }); + if (task.expanded === false) return; + for (const child of childrenByParent.get(task.id) ?? []) { + visit(child); + } + }; + for (const root of childrenByParent.get(null) ?? []) { + visit(root); + } + return result; +} + +function createTimeline(scale: ScheduleScale, startValue: string, endValue: string): TimelineUnit[] { + const width = scaleColumnWidth[scale]; + const units: TimelineUnit[] = []; + let cursor = parseDate(startValue); + const endDate = parseDate(endValue); + let index = 0; + while (cursor <= endDate) { + const next = scale === 'month' + ? new Date(cursor.getFullYear(), cursor.getMonth() + 1, 1) + : addDays(cursor, scale === 'week' ? 7 : 1); + units.push({ + key: `${scale}-${cursor.toISOString()}`, + label: scale === 'month' ? `${cursor.getFullYear()}年${cursor.getMonth() + 1}月` : String(scale === 'week' ? getWeekNumber(cursor) : cursor.getDate()), + subLabel: scale === 'day' ? dayName(cursor) : '', + start: new Date(cursor), + end: next, + x: index * width, + width, + muted: scale === 'day' ? [0, 6].includes(cursor.getDay()) : index % 2 === 0, + }); + cursor = next; + index += 1; + } + return units; +} + +function groupTimelineByMonth(timeline: TimelineUnit[]) { + const groups: Array<{ label: string; x: number; width: number }> = []; + for (const unit of timeline) { + const label = `${unit.start.getFullYear()}年${unit.start.getMonth() + 1}月`; + const current = groups.at(-1); + if (current?.label === label) { + current.width += unit.width; + } else { + groups.push({ label, x: unit.x, width: unit.width }); + } + } + return groups; +} + +function dateToX(date: Date, timeline: TimelineUnit[]): number { + const first = timeline[0]; + const last = timeline.at(-1); + if (!first || !last) return 0; + if (date < first.start) return 0; + if (date >= last.end) return last.x + last.width; + const unit = timeline.find((item) => date >= item.start && date < item.end) ?? last; + const span = unit.end.getTime() - unit.start.getTime(); + const percent = span > 0 ? (date.getTime() - unit.start.getTime()) / span : 0; + return unit.x + Math.max(0, Math.min(1, percent)) * unit.width; +} + +function parseDate(value: string): Date { + return new Date(`${value}T00:00:00`); +} + +function addDays(date: Date, days: number): Date { + const next = new Date(date); + next.setDate(next.getDate() + days); + return next; +} + +function shiftDate(value: string, days: number): string { + return formatDate(addDays(parseDate(value), days)); +} + +function formatDate(date: Date): string { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; +} + +function formatCompactMoney(value: number): string { + if (!Number.isFinite(value)) return '0'; + if (Math.abs(value) >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`; + if (Math.abs(value) >= 1_000) return `${Math.round(value / 1_000)}k`; + return String(Math.round(value)); +} + +function calculateDuration(start: string, end: string): number { + const diff = parseDate(end).getTime() - parseDate(start).getTime(); + return Math.max(1, Math.round(diff / 86_400_000) + 1); +} + +function clampNumber(value: number, min: number, max: number): number { + if (Number.isNaN(value)) return min; + return Math.max(min, Math.min(max, value)); +} + +function collectDescendantIds(tasks: ScheduleTask[], taskId: string): Set { + const ids = new Set([taskId]); + let changed = true; + while (changed) { + changed = false; + for (const task of tasks) { + if (task.parentId && ids.has(task.parentId) && !ids.has(task.id)) { + ids.add(task.id); + changed = true; + } + } + } + return ids; +} + +function isNetworkView(view: ScheduleView): view is NetworkView { + return view === 'time-network' || view === 'adm' || view === 'pert'; +} + +function getWeekNumber(date: Date): string { + const start = new Date(date.getFullYear(), 0, 1); + const diff = Math.floor((date.getTime() - start.getTime()) / 86_400_000); + return `第${Math.ceil((diff + start.getDay() + 1) / 7)}周`; +} + +function dayName(date: Date): string { + return ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'][date.getDay()] ?? ''; +} + +function taskColor(status: ScheduleStatus): string { + const colors: Record = { + normal: '#2f7df6', + ahead: '#12c86b', + warning: '#ff9f2e', + delayed: '#ef4444', + future: '#9cccf5', + }; + return colors[status]; +} + +function taskFill(status: ScheduleStatus): string { + const colors: Record = { + normal: '#c8e2fb', + ahead: '#bdf6cb', + warning: '#fdecc6', + delayed: '#ffd9d1', + future: '#d6ecff', + }; + return colors[status]; +} diff --git a/03-frontend/components/FileManagerWorkbench.tsx b/03-frontend/components/FileManagerWorkbench.tsx index f8d7d937..658af753 100644 --- a/03-frontend/components/FileManagerWorkbench.tsx +++ b/03-frontend/components/FileManagerWorkbench.tsx @@ -4,19 +4,21 @@ import type { ReactNode } from 'react'; import { ModuleFileExplorer } from '@/components/ModuleFileExplorer'; -import type { ModuleAuditEvent } from '@/lib/module-file-system'; +import type { ModuleAuditEvent, ModuleFileNode } from '@/lib/module-file-system'; import type { ModuleSpec } from '@/lib/module-registry'; export function FileManagerWorkbench({ spec, onAudit, businessHome, + renderFilePreview, }: { spec: ModuleSpec; onAudit?: (event: ModuleAuditEvent) => void; onFeatureSelect?: (featureTitle: string) => void; sidecar?: ReactNode; businessHome?: ReactNode; + renderFilePreview?: (file: ModuleFileNode) => ReactNode | null; }) { function handleAudit(event: ModuleAuditEvent) { onAudit?.(event); @@ -24,7 +26,12 @@ export function FileManagerWorkbench({ return (
    - +
    ); } diff --git a/03-frontend/components/FileOperationDialog.tsx b/03-frontend/components/FileOperationDialog.tsx index 41196030..79c57204 100644 --- a/03-frontend/components/FileOperationDialog.tsx +++ b/03-frontend/components/FileOperationDialog.tsx @@ -103,6 +103,7 @@ export function FileOperationDialog({ placement="center" zIndex={75} modal + defaultViewportRatio={null} bodyClassName="space-y-4 p-5" footer={footer} > diff --git a/03-frontend/components/FilePreviewDrawer.tsx b/03-frontend/components/FilePreviewDrawer.tsx index 368e5750..dd0c82e9 100644 --- a/03-frontend/components/FilePreviewDrawer.tsx +++ b/03-frontend/components/FilePreviewDrawer.tsx @@ -3,6 +3,7 @@ 'use client'; import { FileText } from 'lucide-react'; +import type { ReactNode } from 'react'; import { FloatingWindowFrame } from '@/components/FloatingWindowFrame'; import { UniversalFileViewer } from '@/components/UniversalFileViewer'; import type { ModuleFileNode } from '@/lib/module-file-system'; @@ -11,16 +12,20 @@ export function FilePreviewDrawer({ file, fullView, onClose, + renderFilePreview, }: { file: ModuleFileNode | null; fullView: boolean; onClose: () => void; onFullView: () => void; + renderFilePreview?: (file: ModuleFileNode) => ReactNode | null; }) { if (!file) { return null; } + const customPreview = renderFilePreview?.(file); + return (
    - + {customPreview ?? }
    ); diff --git a/03-frontend/components/InsomeModuleWorkbench.tsx b/03-frontend/components/InsomeModuleWorkbench.tsx index 77ffeaff..40a21201 100644 --- a/03-frontend/components/InsomeModuleWorkbench.tsx +++ b/03-frontend/components/InsomeModuleWorkbench.tsx @@ -368,11 +368,11 @@ export function InsomeModuleWorkbench({ INSOME Home - INSOME Studio + 方案设计
    @@ -540,10 +540,10 @@ export function InsomeModuleWorkbench({

    下一步

    - {nextModuleId ? `进入${MODULE_LABELS[nextModuleId]}模块` : "打开完整 INSOME Studio"} + {nextModuleId ? `进入${MODULE_LABELS[nextModuleId]}模块` : "进入方案设计模块"}
    diff --git a/03-frontend/components/LeadRequirementWorkflowPanel.tsx b/03-frontend/components/LeadRequirementWorkflowPanel.tsx index 37a81714..1e4a0d78 100644 --- a/03-frontend/components/LeadRequirementWorkflowPanel.tsx +++ b/03-frontend/components/LeadRequirementWorkflowPanel.tsx @@ -3,9 +3,13 @@ 'use client'; import { Alert, Button, Cascader, Form, Input, InputNumber, Radio, Select, Space, Tag, Typography } from 'antd'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { ImagePlus, Pencil, Sparkles, Upload } from 'lucide-react'; +import { useCallback, useEffect, useMemo, useState, type ReactNode } from 'react'; +import { LocalFileUploader } from '@/components/LocalFileUploader'; +import { WorksExplorer } from '@/components/shared/works-explorer'; import { createModuleAuditEvent } from '@/lib/module-actions'; import { generationClient, type GenerationJob } from '@/lib/generation-client'; +import { getModuleRootId, type ModuleAuditEvent, type ModuleFileNode } from '@/lib/module-file-system'; import { buildContractDraftDocument, buildDesignConfirmationDocument, @@ -31,7 +35,6 @@ import { type CurrencyCode, } from '@/lib/lead-requirements'; import { moduleFileApiClient } from '@/lib/module-file-api-client'; -import type { ModuleAuditEvent, ModuleFileNode } from '@/lib/module-file-system'; import type { ModuleId } from '@/lib/module-registry'; interface RequirementSummary { @@ -53,6 +56,8 @@ interface LocationOption { children?: LocationOption[]; } +type MarketingWorkflowStep = 'inspiration' | 'intake' | 'reference' | 'design'; + function loc(label: string, children: LocationOption[] = []): LocationOption { return children.length > 0 ? { label, value: label, children } : { label, value: label }; } @@ -223,6 +228,7 @@ function MarketingRequirementCapture({ const [form] = Form.useForm(); const [status, setStatus] = useState('填写客户需求后写入后端 CDE / 数据库,供方案设计模块导入。'); const [submitting, setSubmitting] = useState(false); + const [activeStep, setActiveStep] = useState('inspiration'); const [savedRequirement, setSavedRequirement] = useState<{ record: MarketingRequirementRecord; file: ModuleFileNode; @@ -230,6 +236,16 @@ function MarketingRequirementCapture({ const [confirmedScheme, setConfirmedScheme] = useState(null); const selectedDocumentTemplateId = Form.useWatch('documentTemplateId', form); + function selectStep(step: MarketingWorkflowStep) { + setActiveStep(step); + if (step === 'reference' && !savedRequirement) { + setStatus('请先完成需求录入,再上传参考图作为方案生成输入。'); + } + if (step === 'design' && !savedRequirement) { + setStatus('请先完成需求录入和参考图上传,再开始生成建筑方案。'); + } + } + async function submit(values: MarketingRequirementFormValues) { setSubmitting(true); setStatus('正在写入后端数据库...'); @@ -265,7 +281,8 @@ function MarketingRequirementCapture({ form.resetFields(); setSavedRequirement({ record, file: node }); setConfirmedScheme(null); - setStatus(`已提交设计需求 ${node.name}; 下一步先生成建筑方案供客户比选确认。`); + setActiveStep('reference'); + setStatus(`已提交设计需求 ${node.name}; 下一步上传参考图,再生成建筑方案供客户比选确认。`); } catch (error) { setStatus(`保存失败: ${describeError(error)}`); } finally { @@ -276,102 +293,135 @@ function MarketingRequirementCapture({ return (
    -
    - - REQ-INTAKE -
    - -
    -
    - - - - - - - - - - - - + + + {activeStep === 'inspiration' ? : null} + + {activeStep === 'intake' ? ( +
    +
    + + REQ-INTAKE +
    + + +
    + + + + + + + + + + + + + {selectedDocumentTemplateId === 'custom' ? ( + + + + ) : null} + + + + + + + +
    + + + + + + + ) : null} - - - - - - - -
    - - - - - { if (event.target.files) void uploadFiles(event.target.files); @@ -109,12 +118,13 @@ export function LocalFileUploader({ } disabled:cursor-wait disabled:opacity-60`} > - {uploading ? '正在写入本地运行目录' : '拖拽文件到这里或点击上传'} - 文件会进入模块文件系统、生命周期、审批与审计 + {uploading ? '正在写入本地运行目录' : idleLabel} + {helperText} { if (event.target.files) void uploadFiles(event.target.files); diff --git a/03-frontend/components/ModuleDetailWorkbench.tsx b/03-frontend/components/ModuleDetailWorkbench.tsx index 3a24980c..09dc7f95 100644 --- a/03-frontend/components/ModuleDetailWorkbench.tsx +++ b/03-frontend/components/ModuleDetailWorkbench.tsx @@ -1,15 +1,22 @@ // components/ModuleDetailWorkbench.tsx - Operational module detail surface // License: Apache-2.0 -'use client'; +"use client"; -import { AICenterWorkbench } from '@/components/AICenterWorkbench'; -import { DigitalTwinOperationsPanel } from '@/components/DigitalTwinOperationsPanel'; -import { FileManagerWorkbench } from '@/components/FileManagerWorkbench'; -import { LeadRequirementWorkflowPanel } from '@/components/LeadRequirementWorkflowPanel'; -import { ProjectPlanningStudio } from '@/components/ProjectPlanningStudio'; -import type { ModuleActionResult } from '@/lib/module-actions'; -import type { ModuleAuditEvent } from '@/lib/module-file-system'; -import type { ModuleSpec } from '@/lib/module-registry'; +import { AICenterWorkbench } from "@/components/AICenterWorkbench"; +import { ConceptDesignStudioWorkbench } from "@/components/ConceptDesignStudioWorkbench"; +import { DetailedDesignPlanFinderWorkbench } from "@/components/DetailedDesignPlanFinderWorkbench"; +import { DigitalTwinOperationsPanel } from "@/components/DigitalTwinOperationsPanel"; +import { FeichuanPlanningWorkbench } from "@/components/FeichuanPlanningWorkbench"; +import { FileManagerWorkbench } from "@/components/FileManagerWorkbench"; +import { LeadRequirementWorkflowPanel } from "@/components/LeadRequirementWorkflowPanel"; +import { PaperclipProductionWorkbench } from "@/components/PaperclipProductionWorkbench"; +import { StandardLibrarySemanticDictionaryPanel } from "@/components/StandardLibrarySemanticDictionaryPanel"; +import type { ModuleActionResult } from "@/lib/module-actions"; +import { + isStandardLibrarySemanticDictionaryNode, + type ModuleAuditEvent, +} from "@/lib/module-file-system"; +import type { ModuleSpec } from "@/lib/module-registry"; export function ModuleDetailWorkbench({ spec, @@ -17,14 +24,14 @@ export function ModuleDetailWorkbench({ onFeatureSelect, }: { spec: ModuleSpec; - onAudit?: (event: ModuleActionResult['auditEvent']) => void; + onAudit?: (event: ModuleActionResult["auditEvent"]) => void; onFeatureSelect?: (featureTitle: string) => void; }) { function handleAudit(event: ModuleAuditEvent) { onAudit?.(event); } - if (spec.id === 'ai_center') { + if (spec.id === "ai_center") { return ( } + businessHome={ + + } {...(onFeatureSelect ? { onFeatureSelect } : {})} /> ); } - if (spec.id === 'planning_management') { + if (spec.id === "planning_management") { return ( } + businessHome={} {...(onFeatureSelect ? { onFeatureSelect } : {})} /> ); } - if (spec.id === 'marketing_service' || spec.id === 'concept_design') { + if (spec.id === "marketing_service") { return ( } + businessHome={ + + } + {...(onFeatureSelect ? { onFeatureSelect } : {})} + /> + ); + } + + if (spec.id === "concept_design") { + return ( + } + {...(onFeatureSelect ? { onFeatureSelect } : {})} + /> + ); + } + + if (spec.id === "detailed_design") { + return ( + + } + {...(onFeatureSelect ? { onFeatureSelect } : {})} + /> + ); + } + + if (spec.id === "production_manufacturing") { + return ; + } + + if (spec.id === "standard_library") { + return ( + + isStandardLibrarySemanticDictionaryNode(file) ? ( + + ) : null + } {...(onFeatureSelect ? { onFeatureSelect } : {})} /> ); diff --git a/03-frontend/components/ModuleFileExplorer.tsx b/03-frontend/components/ModuleFileExplorer.tsx index 96b20c4a..d6087620 100644 --- a/03-frontend/components/ModuleFileExplorer.tsx +++ b/03-frontend/components/ModuleFileExplorer.tsx @@ -6,6 +6,7 @@ import { ArrowLeft, Box, ChevronRight, + Database, Download, FileText, Folder, @@ -81,10 +82,12 @@ export function ModuleFileExplorer({ spec, onAudit, businessHome, + renderFilePreview, }: { spec: ModuleSpec; onAudit?: (event: ModuleAuditEvent) => void; businessHome?: ReactNode; + renderFilePreview?: (file: ModuleFileNode) => ReactNode | null; }) { const rootId = getModuleRootId(spec.id); const [snapshot, setSnapshot] = useState(() => moduleBackendAdapter.snapshot(spec.id)); @@ -97,7 +100,9 @@ export function ModuleFileExplorer({ const [dialogTarget, setDialogTarget] = useState(null); const [lastShareLink, setLastShareLink] = useState(null); const [search, setSearch] = useState(''); - const [viewMode, setViewMode] = useState('list'); + const [viewMode, setViewMode] = useState(() => + spec.id === 'standard_library' ? 'grid' : 'list', + ); const [actionMessage, setActionMessage] = useState('文件、事务、审批和审计已接入运行适配器。'); const [directoryPickerOpen, setDirectoryPickerOpen] = useState(false); @@ -895,6 +900,7 @@ export function ModuleFileExplorer({ { setPreviewNode(null); setFullView(false); @@ -1219,7 +1225,7 @@ function FileList({ {node.name} - {node.owner} · {node.updatedAt} · {node.mimeType} + {node.owner} · {node.updatedAt} · {fileKindLabel(node)} {formatModuleFileSize(node.size)} @@ -1304,8 +1310,8 @@ function FileGrid({

    {node.name}

    -

    {node.mimeType}

    -

    {formatModuleFileSize(node.size)} · {node.version}

    +

    {fileKindLabel(node)}

    +

    {fileMetricLabel(node)}

    ))}
    @@ -1365,6 +1371,9 @@ function statusClass(status: ModuleFileNode['status']) { } function fileIcon(node: ModuleFileNode) { + if (node.tags.includes('semantic-dictionary')) { + return ; + } if (node.viewerKind === 'engineering' || node.mimeType.startsWith('model') || node.name.endsWith('.ifc') || node.name.endsWith('.glb')) { return ; } @@ -1374,6 +1383,20 @@ function fileIcon(node: ModuleFileNode) { return ; } +function fileKindLabel(node: ModuleFileNode): string { + if (node.tags.includes('semantic-dictionary')) { + return 'SJG 157 语义字典 / PostgreSQL'; + } + return node.mimeType; +} + +function fileMetricLabel(node: ModuleFileNode): string { + if (node.tags.includes('semantic-dictionary')) { + return '5679 条 · SJG 157-2024'; + } + return `${formatModuleFileSize(node.size)} · ${node.version}`; +} + function buildBreadcrumbs(files: ModuleFileNode[], folderId: string): ModuleFileNode[] { const result: ModuleFileNode[] = []; let cursor = files.find((file) => file.id === folderId) ?? null; diff --git a/03-frontend/components/ModuleOperationalPanel.tsx b/03-frontend/components/ModuleOperationalPanel.tsx index 1f839050..af64c984 100644 --- a/03-frontend/components/ModuleOperationalPanel.tsx +++ b/03-frontend/components/ModuleOperationalPanel.tsx @@ -397,13 +397,15 @@ function ProductionControl({ onAudit }: { onAudit: (summary: string) => void }) const [cncState, setCncState] = useState('未生成'); const [qcState, setQcState] = useState('待检'); const [shipmentState, setShipmentState] = useState('待包装'); + const [paperclipState, setPaperclipState] = useState('v2026.517.0 已接入'); return ( -
    +
    } title="工单状态" value={workOrderState} onClick={() => { setWorkOrderState('已下发 MES'); onAudit('生产制造: 工单状态切换为已下发 MES'); }} /> } title="CNC 文件" value={cncState} onClick={() => { setCncState('NC/DXF 已生成'); onAudit('生产制造: 已生成 CNC/数控文件'); }} /> } title="质检状态" value={qcState} onClick={() => { setQcState('焊接/涂装复检通过'); onAudit('生产制造: 质检状态更新为通过'); }} /> } title="发运批次" value={shipmentState} onClick={() => { setShipmentState('PKG-RF-07 已发运'); onAudit('生产制造: 发运批次已安排'); }} /> + } title="Paperclip编排" value={paperclipState} onClick={() => { setPaperclipState('已同步 heartbeat / budget / issue'); onAudit('生产制造: Paperclip v2026.517.0 编排状态已同步到审计链'); }} />
    ); } diff --git a/03-frontend/components/ModuleWorkbenchShell.tsx b/03-frontend/components/ModuleWorkbenchShell.tsx index 49254229..73fc15ad 100644 --- a/03-frontend/components/ModuleWorkbenchShell.tsx +++ b/03-frontend/components/ModuleWorkbenchShell.tsx @@ -3,7 +3,6 @@ 'use client'; import Link from 'next/link'; -import { useRouter } from 'next/navigation'; import { useState, type CSSProperties, @@ -17,45 +16,27 @@ import { BrainCircuit, Calculator, CalendarDays, - CheckCircle2, ChevronLeft, Command, CreditCard, Factory, - FolderTree, - GitBranch, HardHat, Headphones, Library, Lightbulb, Menu, - Network, PencilRuler, Ruler, Search, - Send, Settings, ShieldCheck, - Sparkles, Truck, Workflow, } from 'lucide-react'; import { FloatingWindowFrame } from '@/components/FloatingWindowFrame'; import { ModuleDetailWorkbench } from '@/components/ModuleDetailWorkbench'; import { ThemeSwitcher } from '@/components/ThemeSwitcher'; -import { - architokenAssistantProfile, - moduleAssistantSuggestions, -} from '@/lib/ai-assistant-profile'; -import { createModuleAuditEvent } from '@/lib/module-actions'; import type { ModuleActionResult } from '@/lib/module-actions'; -import { moduleBackendAdapter } from '@/lib/module-backend-adapter'; -import { - architokenOpenFileEventName, - architokenPendingOpenFileKey, - type ArchitokenOpenFileRequest, -} from '@/lib/module-dialog-events'; -import type { ModuleFileNode } from '@/lib/module-file-system'; import { getModuleSpec, moduleSpecs, @@ -82,7 +63,6 @@ export function ModuleWorkbenchShell({ initialRailExpanded?: boolean; }) { const fallbackModuleId = initialModuleId ?? 'construction_management'; - const router = useRouter(); const [query, setQuery] = useState(''); const [railExpanded, setRailExpanded] = useState(initialRailExpanded); const [railWidth, setRailWidth] = useState(248); @@ -179,8 +159,8 @@ export function ModuleWorkbenchShell({ type="button" onClick={() => setAssistantOpen(true)} className="arch-huly-icon-button" - aria-label="打开 ArchIToken AI" - title="ArchIToken AI" + aria-label="打开 OpenClaw" + title="OpenClaw" > @@ -293,11 +273,8 @@ export function ModuleWorkbenchShell({ router.push(href)} /> ); @@ -439,171 +416,18 @@ function clampNumber(value: number, min: number, max: number) { return Math.min(Math.max(value, min), max); } -function normalizeCommand(value: string) { - return value - .toLowerCase() - .replace(/[“”"',,。::]+/g, '') - .replace(/[\s._/-]+/g, ''); -} - -function resolveModuleFromCommand(input: string) { - const normalized = normalizeCommand(input); - if (!normalized) return null; - - return ( - moduleSpecs.find((spec) => - [spec.zhName, spec.enName, spec.id, spec.track] - .filter(Boolean) - .some((candidate) => normalized.includes(normalizeCommand(candidate))), - ) ?? null - ); -} - -function resolveFileFromCommand(input: string, currentModuleId: ModuleId): ModuleFileNode | null { - const query = extractFileOpenQuery(input); - if (!query) return null; - - const normalizedInput = normalizeCommand(input); - const normalizedQuery = normalizeCommand(query); - if (normalizedQuery.length < 2) return null; - - return moduleBackendAdapter - .snapshot() - .files.filter((file) => file.parentId !== null) - .map((file) => ({ - file, - score: scoreFileCandidate(file, normalizedInput, normalizedQuery, currentModuleId), - })) - .filter((candidate) => candidate.score > 0) - .sort((left, right) => right.score - left.score)[0]?.file ?? null; -} - -function extractFileOpenQuery(input: string): string | null { - const trimmed = input.trim(); - if (!trimmed) return null; - - const hasOpenIntent = /(打开|查看|预览|定位|进入|open|view|preview)/i.test(trimmed); - const hasFileExtension = /\.[a-z0-9]{2,8}\b/i.test(trimmed); - if (!hasOpenIntent && !hasFileExtension) return null; - - return trimmed - .replace(/^(请|帮我|麻烦|请帮我)?\s*(打开|查看|预览|定位|进入|open|view|preview)\s*/i, '') - .replace(/^(这个|当前|一下)\s*/i, '') - .trim(); -} - -function scoreFileCandidate( - file: ModuleFileNode, - normalizedInput: string, - normalizedQuery: string, - currentModuleId: ModuleId, -) { - const normalizedName = normalizeCommand(file.name); - const normalizedBaseName = normalizeCommand(file.name.replace(/\.[^.]+$/, '')); - const normalizedTags = file.tags.map(normalizeCommand); - let score = 0; - - if (normalizedName === normalizedQuery || normalizedBaseName === normalizedQuery) score = 120; - else if (normalizedInput.includes(normalizedName)) score = 110; - else if (normalizedName.includes(normalizedQuery)) score = 92; - else if (normalizedBaseName.includes(normalizedQuery)) score = 88; - else if (normalizedQuery.includes(normalizedName) && normalizedName.length >= 2) score = 82; - else if (normalizedTags.some((tag) => tag && normalizedQuery.includes(tag))) score = 58; - - if (score === 0) return 0; - if (file.moduleId === currentModuleId) score += 25; - if (file.type === 'file') score += 5; - return score; -} - -function dispatchOpenFileRequest(request: ArchitokenOpenFileRequest) { - window.dispatchEvent( - new CustomEvent(architokenOpenFileEventName, { - detail: request, - }), - ); -} - function WorkbenchIntelligenceDialog({ selectedSpec, selectedFeatureTitle, - auditEvents, open, onOpenChange, - onAudit, - onNavigate, }: { selectedSpec: ReturnType; selectedFeatureTitle: string; - auditEvents: ModuleActionResult['auditEvent'][]; open: boolean; onOpenChange: (open: boolean) => void; - onAudit: (event: ModuleActionResult['auditEvent']) => void; - onNavigate: (href: string) => void; }) { - const profile = architokenAssistantProfile; - const suggestions = moduleAssistantSuggestions[selectedSpec.id] ?? [ - '生成当前模块交付物草案并等待人工审批。', - '检查 openBIM / CDE / Speckle / IFCDB-Agent 路由缺口。', - '把当前文件、对象、审批和知识图谱写入可追踪证据链。', - ]; - const selectedFeatureMessage = selectedFeatureTitle - ? `已锁定业务对象: ${selectedFeatureTitle}` - : `${selectedSpec.zhName} 模块上下文已载入`; - const [input, setInput] = useState(''); - const [messages, setMessages] = useState([ - `${profile.name}: ${selectedFeatureMessage}。`, - ]); - - function pushMessage(summary: string) { - const message = `${profile.name}: ${summary}`; - setMessages((current) => [message, ...current].slice(0, 6)); - onAudit(createModuleAuditEvent(`assistant-${selectedSpec.id}`, profile.name, summary)); - } - - function submitMessage() { - const normalizedInput = input.trim(); - if (!normalizedInput) return; - const targetFile = resolveFileFromCommand(normalizedInput, selectedSpec.id); - - if (targetFile) { - const request: ArchitokenOpenFileRequest = { - fileId: targetFile.id, - moduleId: targetFile.moduleId, - query: normalizedInput, - requestedAt: new Date().toISOString(), - }; - const targetModule = getModuleSpec(targetFile.moduleId); - if (targetFile.moduleId === selectedSpec.id) { - dispatchOpenFileRequest(request); - } else { - window.sessionStorage.setItem( - architokenPendingOpenFileKey, - JSON.stringify(request), - ); - onNavigate(targetModule.routeHref); - } - pushMessage(`正在打开 ${targetModule.zhName} / ${targetFile.name}。`); - setInput(''); - return; - } - - const targetModule = resolveModuleFromCommand(normalizedInput); - - if (targetModule) { - pushMessage(`正在打开 ${targetModule.zhName}。全局指令已映射到 ${targetModule.routeHref}。`); - onNavigate(targetModule.routeHref); - setInput(''); - return; - } - - pushMessage(`已记录请求“${normalizedInput}”,当前未匹配到可直接打开的模块或文件。`); - setInput(''); - } - - function runGlobalAction(action: string) { - pushMessage(`${action}: 已记录为当前模块待办动作。`); - } + const openClawSrc = buildOpenClawControlSrc(selectedSpec, selectedFeatureTitle); if (!open) { return ( @@ -611,185 +435,52 @@ function WorkbenchIntelligenceDialog({ type="button" onClick={() => onOpenChange(true)} className="arch-btn-primary fixed bottom-5 right-5 z-50 flex h-12 w-12 items-center justify-center rounded-md shadow-lg" - aria-label="打开 ArchIToken AI 全局对话" - title="ArchIToken AI" + aria-label="打开 ArchIToken" + title="ArchIToken" > ); } - const inputBar = ( - - ); - return ( } onClose={() => onOpenChange(false)} - defaultSize={{ width: 460, height: 760 }} - minSize={{ width: 360, height: 440 }} - placement="bottom-right" - zIndex={50} - bodyClassName="p-3" - footer={inputBar} + defaultSize={{ width: 1120, height: 760 }} + minSize={{ width: 680, height: 520 }} + placement="center" + zIndex={70} + bodyClassName="p-0 overflow-hidden" + defaultViewportRatio={0.75} > -
    -
    -
    -

    - 当前上下文 -

    -

    - {selectedFeatureTitle || selectedSpec.zhName} -

    -
    - -
    -
    - - - - -
    -
    - {profile.capabilityTags.slice(0, 5).map((tag) => ( - - {tag} - - ))} -
    -
    - -
    -
    - -

    知识图谱

    -
    -
    - } label="当前模块" value={selectedSpec.zhName} /> - } label="上下游" value={`${selectedSpec.inputs.length} 输入 / ${selectedSpec.outputs.length} 输出`} /> - } label="标准" value={selectedSpec.standards.slice(0, 3).join(' · ')} /> - } label="运行时" value={selectedSpec.fileTypes.slice(0, 5).join(' · ')} /> -
    -
    - -
    -
    - -

    任务队列

    -
    -
    - {['生成', '校核', '派生', '归档', '诊断', '审批'].map((action) => ( - - ))} -
    -
    - {suggestions.slice(0, 3).map((suggestion) => ( - - ))} -
    -
    - -
    -
    - -

    工程对话

    -
    -
    - {messages.map((message) => ( -

    - {message} -

    - ))} -
    -
    - -
    -

    - 最近审计 -

    -
    - {auditEvents.slice(0, 3).map((event) => ( -

    - {event.summary} -

    - ))} - {auditEvents.length === 0 ? ( -

    暂无本页操作审计。

    - ) : null} -
    -
    +