From b1c59051b92513a56b18f3f017631485de172da8 Mon Sep 17 00:00:00 2001 From: Harmon Date: Sun, 14 Jun 2026 18:47:00 +0800 Subject: [PATCH] docs: update production workflow design and runtime changes --- .env.example | 62 ++- .gitignore | 1 + backend/app/config.py | 112 ++++-- backend/app/main.py | 2 +- backend/app/routers/dinsar_products.py | 6 +- backend/app/routers/monitor.py | 18 + backend/app/routers/ps_products.py | 6 +- backend/app/routers/sbas_insar_production.py | 3 + backend/app/routers/sbas_insar_products.py | 4 +- backend/app/routers/water.py | 8 + backend/app/scheduler.py | 2 + .../gf3_sarscape_production_service.py | 9 +- backend/app/services/health_service.py | 13 +- backend/app/services/job_handlers.py | 18 + backend/app/services/root_registry_service.py | 50 +++ .../services/sbas_insar_catalog_service.py | 105 +++-- .../services/sbas_insar_production_service.py | 134 ++++++- backend/app/services/wsl_runtime_registry.py | 10 +- ...AP_TILESERVER_PROXY_AND_ACCESS_20260613.md | 152 +++++++ docs/DEPLOYMENT.md | 36 +- ...ASK_POOL_THREE_ENGINE_REFACTOR_20260614.md | 212 ++++++++++ ...EOTIFF_GAMMA_PREPROCESS_DESIGN_20260515.md | 2 +- ..._ALGORITHM_ENGINEERING_HANDOFF_20260602.md | 3 +- ...SCAPE_NATIVE_TO_GEOTIFF_DESIGN_20260530.md | 29 +- docs/INDEX.md | 15 +- ...AR_API服务接入采购需求说明书_20260604.docx | Bin 0 -> 68383 bytes ...ON_RESULTS_MULTI_ENGINE_DESIGN_20260423.md | 2 + docs/SBAS_INSAR_CURRENT_WORKFLOW.md | 8 +- frontend/index.html | 2 +- frontend/src/DataMonitorPanel.jsx | 47 ++- frontend/src/config/appConstants.js | 7 +- frontend/src/config/taskUiPolicies.js | 5 +- frontend/src/hooks/useAppAuthLifecycle.js | 58 ++- nginx/access_denied.html | 94 +++++ nginx/client_allow.conf | 2 + nginx/nginx.conf | 71 +++- scripts/cleanup_runtime.py | 374 ++++++++++++++++++ scripts/migrate_runtime_roots.py | 175 ++++++++ scripts/start_app.ps1 | 208 +++++++++- scripts/stop_app.ps1 | 94 +++++ stop_system.bat | 17 + 41 files changed, 2042 insertions(+), 134 deletions(-) create mode 100644 docs/BASEMAP_TILESERVER_PROXY_AND_ACCESS_20260613.md create mode 100644 docs/DINSAR_TASK_POOL_THREE_ENGINE_REFACTOR_20260614.md create mode 100644 docs/LandSAR_API服务接入采购需求说明书_20260604.docx create mode 100644 nginx/access_denied.html create mode 100644 nginx/client_allow.conf create mode 100644 scripts/cleanup_runtime.py create mode 100644 scripts/migrate_runtime_roots.py create mode 100644 scripts/stop_app.ps1 create mode 100644 stop_system.bat diff --git a/.env.example b/.env.example index 857e2c4..d1bf001 100644 --- a/.env.example +++ b/.env.example @@ -43,6 +43,10 @@ CONDA_ENV_NAME= NGINX_PATH=C:\nginx\nginx.exe NGINX_HEALTH_URL=http://127.0.0.1/ +# Optional LAN client whitelist for the main nginx entry. +# Empty means allow all. Use semicolon/comma/space-separated IPs or CIDRs, for example: +# NGINX_ALLOWED_CLIENT_IPS=192.168.1.10;192.168.1.23;192.168.1.0/24 +NGINX_ALLOWED_CLIENT_IPS= LICENSE_PATH= CORS_ORIGINS=http://127.0.0.1:5173,http://localhost @@ -63,13 +67,15 @@ MONITOR_DINSAR_DIRS=D:\DInSARResult ORBIT_SOURCE_DIRS=D:\LT1_data_lsarorbit;D:\Sentinel1_EOF_Pool MONITOR_ORBIT_DIR=D:\LT1_data_lsarorbit -GF3_ARCHIVE_SOURCE_DIRS=D:\GF3_L1A_Image_Zip +GF3_ARCHIVE_SOURCE_DIRS=D:\production_inputs\gf3\archives GF3_ARCHIVE_EXTS=.zip,.tar,.tar.gz,.tgz GF3_UNPACK_DELETE_ARCHIVE=true -GF3_SOURCE_DIRS=D:\GF3_L1A_Image -GF3_SARSCAPE_NATIVE_DIRS=D:\GF3_L2_ENVI_Binary_Pool -GF3_STORAGE_DIRS=D:\GF3_L2_Image_Pool -GF3_SARSCAPE_WRAPPER_EXE=D:\Code\Insar_management_system_v2\.codex_tmp\GF3_L1A_To_L2_pipeline\dist\windows\gf3wrapper.exe +GF3_LEGACY_GDAL_ENABLED=false +GF3_SOURCE_DIRS= +GF3_SARSCAPE_NATIVE_DIRS=D:\production_results\gf3\sarscape_native +GF3_STORAGE_DIRS=D:\production_results\gf3\standard_l2 +GF3_SARSCAPE_RUNTIME_DIR=D:\production_runtime\gf3\sarscape_runtime +GF3_SARSCAPE_WRAPPER_EXE=D:\Code\Insar_management_system_v2\third_party\GF3_L1A_To_L2_pipeline\dist\windows\gf3wrapper.exe GF3_SARSCAPE_IDLRT_PATH=C:\Program Files\Harris\ENVI56\IDL88\bin\bin.x86_64\idlrt.exe GF3_SARSCAPE_DEM_PATH=D:\DEM\COPDEM_GLO30_China_4326_DEM GF3_SARSCAPE_POLARIZATIONS=HH,HV @@ -114,8 +120,7 @@ SRTM_DEM_DIR=D:\SRTM30m GF3_GEO_DEM_PATH=D:\DEM\gf3_dem.jp2 WATER_RESULTS_DIR=D:\WaterResult SAR_ANALYSIS_READY_ROOT=D:\production_results\sar_analysis_ready -SAR_ANALYSIS_WORK_ROOT=D:\production_results\sar_analysis_work -SAR_ANALYSIS_PREVIEW_ROOT=D:\production_results\sar_analysis_preview +SAR_ANALYSIS_WORK_ROOT=D:\production_runtime\sar_analysis_work SAR_ANALYSIS_NODATA_VALUE=-9999 SAR_ANALYSIS_OUTPUT_COG=true @@ -125,7 +130,7 @@ SAR_ANALYSIS_OUTPUT_COG=true # ----------------------------------------------------------------------------- IDL_EXECUTABLE=C:\Program Files\Harris\ENVI56\IDL88\bin\bin.x86_64\idl.exe IDL_WORKBENCH_PATH=C:\Program Files\Harris\ENVI56\IDL88\bin\bin.x86_64\idlde.exe -IDL_WORKER_RUNTIME_DIR=D:\Code\Insar_management_system_v2\backend\runtime\idl_worker +IDL_WORKER_RUNTIME_DIR=D:\production_runtime\idl_worker IDL_WORKER_DEFAULT_TIMEOUT_SECONDS=14400 IDL_WORKER_MAX_TIMEOUT_SECONDS=43200 @@ -153,7 +158,7 @@ ENVI_STABILITY_MAX_WAIT=3600 WSL_DISTRO=Ubuntu-24.04 WSL_SHARED_CONDA_ENV=insar_wsl_v1 WSL_SHARED_PYTHON=/home/administrator/miniconda3/envs/insar_wsl_v1/bin/python -WSL_BROKER_JOB_ROOT=D:\Code\Insar_management_system_v2\backend\runtime\wsl_jobs +WSL_BROKER_JOB_ROOT=D:\production_runtime\wsl_jobs ISCE2_RUNTIME_ID=isce2_runtime_v1 PYINT_RUNTIME_ID=gamma_pyint_runtime_v1 @@ -170,7 +175,7 @@ ISCE2_STRIPMAP_APP=/home/administrator/miniconda3/envs/insar_wsl_v1/lib/python3. ISCE2_PIPELINE_SCRIPT= # ISCE2 should point to the prepared WGS84 DEM after the one-time conversion. ISCE2_DEM_PATH=D:\SRTM30m\SRTMDEM_RSP_SARscape.wgs84 -ISCE2_WORK_ROOT=D:\Code\Insar_management_system_v2\backend\runtime\isce2_work +ISCE2_WORK_ROOT=D:\production_runtime\isce2_work ISCE2_OUTPUT_ROOT=D:\production_results\dinsar ISCE2_PER_TASK_TIMEOUT_SECONDS=43200 ISCE2_SMOKE_TEST_ENABLED=false @@ -218,10 +223,10 @@ PYINT_WSL_DISTRO=Ubuntu-24.04 PYINT_WSL_PYTHON=/home/administrator/miniconda3/envs/insar_wsl_v1/bin/python PYINT_HOME=D:\Code\Insar_management_system_v2\third_party\PyINT PYINT_APP_SCRIPT=D:\Code\Insar_management_system_v2\third_party\PyINT\pyint\pyintApp.py -PYINT_TEMPLATE_ROOT=D:\Code\Insar_management_system_v2\backend\runtime\pyint_templates -PYINT_WORK_ROOT=D:\Code\Insar_management_system_v2\backend\runtime\pyint_work +PYINT_TEMPLATE_ROOT=D:\production_runtime\pyint_templates +PYINT_WORK_ROOT=D:\production_runtime\pyint_work PYINT_OUTPUT_ROOT=D:\production_results\dinsar -PYINT_DEM_ROOT=D:\Code\Insar_management_system_v2\backend\runtime\pyint_dem +PYINT_DEM_ROOT=D:\production_runtime\pyint_dem PYINT_DEM_MODE=local_fabdem PYINT_FABDEM_ROOT= # When PYINT_DEM_MODE=prepared_file, point this to the same prepared WGS84 DEM. @@ -267,8 +272,9 @@ GAMMA_SBAS_RUNTIME_ID=gamma_sbas_runtime_v1 GAMMA_SBAS_WSL_DISTRO=Ubuntu-24.04 GAMMA_SBAS_PYTHON=/home/administrator/miniconda3/envs/insar_wsl_v1/bin/python GAMMA_SBAS_ENV_SCRIPT=D:\Code\Insar_management_system_v2\deploy\wsl\profiles\gamma_env.sh -GAMMA_SBAS_WORK_ROOT=D:\Code\Insar_management_system_v2\backend\runtime\sbas_insar_production +GAMMA_SBAS_WORK_ROOT=D:\production_runtime\sbas_insar_work GAMMA_SBAS_PRODUCT_ROOT=D:\production_results\timeseries\sbas +GAMMA_SBAS_TRIAL_ROOT=D:\production_runtime\gamma_ipta_trials GAMMA_SBAS_SCRIPT_TEMPLATE_ROOT=D:\Code\Insar_management_system_v2\backend\templates\gamma_sbas GAMMA_SBAS_SOURCE_ROOTS=D:\LuTan1_Image_Pool GAMMA_SBAS_ORBIT_ROOTS=D:\orbit_pools\envi @@ -290,7 +296,7 @@ TIMESERIES_ENABLED=false TIMESERIES_WSL_DISTRO=Ubuntu-24.04 TIMESERIES_ENV_NAME=insar_wsl_v1 TIMESERIES_PYTHON=/home/administrator/miniconda3/envs/insar_wsl_v1/bin/python -TIMESERIES_WORK_ROOT=D:\Code\Insar_management_system_v2\backend\runtime\timeseries_work +TIMESERIES_WORK_ROOT=D:\production_runtime\timeseries_work TIMESERIES_DEM_PATH=D:\SRTM30m\SRTMDEM_RSP_SARscape.wgs84 TIMESERIES_ORBIT_POOL_ISCE2=D:\orbit_pools\isce2 TIMESERIES_EXPERIMENT_ROOT= @@ -320,6 +326,24 @@ JOB_WORKER_STALE_RUNNING_SECONDS=7200 JOB_WORKER_HEARTBEAT_INTERVAL=5 +# ----------------------------------------------------------------------------- +# Runtime cleanup policy +# ----------------------------------------------------------------------------- +# scripts/cleanup_runtime.py is dry-run by default. Pass --delete --yes to remove +# unprotected generated artifacts. Paths referenced by product/catalog tables are +# protected automatically. +RUNTIME_CLEANUP_PYINT_WORK_RETENTION_DAYS=14 +RUNTIME_CLEANUP_PYINT_DEM_RETENTION_DAYS=60 +RUNTIME_CLEANUP_SBAS_RUN_RETENTION_DAYS=30 +RUNTIME_CLEANUP_TRIAL_RETENTION_DAYS=7 +RUNTIME_CLEANUP_IDL_LOG_RETENTION_DAYS=30 +RUNTIME_CLEANUP_WSL_JOB_RETENTION_DAYS=14 +RUNTIME_CLEANUP_TIMESERIES_WORK_RETENTION_DAYS=30 +RUNTIME_CLEANUP_IMAGE_CACHE_RETENTION_DAYS=60 +RUNTIME_CLEANUP_LOG_RETENTION_DAYS=30 +RUNTIME_CLEANUP_FRONTEND_DIST_RETENTION_DAYS=0 + + # ----------------------------------------------------------------------------- # 解包 # ----------------------------------------------------------------------------- @@ -358,5 +382,11 @@ DEFAULT_VLM_MODEL=qwen3-vl:30b # ----------------------------------------------------------------------------- # 前端 tile server # ----------------------------------------------------------------------------- -VITE_TILE_SERVER_URL=http://127.0.0.1:8910 +# Empty means same-origin /tiles, routed by the main nginx proxy. +VITE_TILE_SERVER_URL= VITE_TILE_SERVER_TOKEN=change_me +TILE_SERVER_AUTO_START=true +TILE_SERVER_AUTO_STOP=true +TILE_SERVER_ROOT=D:\Code\tile-server +TILE_SERVER_START_SCRIPT=start-all.bat +TILE_SERVER_STOP_SCRIPT=stop-all.bat diff --git a/.gitignore b/.gitignore index a0e2157..e2b4170 100644 --- a/.gitignore +++ b/.gitignore @@ -65,6 +65,7 @@ tmp_dir_coreg_*/ # Local third-party runtime bundles third_party/LandSAR/ +third_party/GF3_L1A_To_L2_pipeline/ # Large local datasets / installers Data/ diff --git a/backend/app/config.py b/backend/app/config.py index cd13710..958e3d6 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -42,7 +42,7 @@ def _infer_conda_env_name_from_python(path: str | None) -> str: def _default_idl_runtime_dir() -> str: - return os.path.join(_BACKEND_DIR, "runtime", "idl_worker") + return os.path.join(_default_runtime_root(_PROJECT_ROOT), "idl_worker") def _resolve_idl_runtime_dir(value: str | None) -> str: @@ -77,6 +77,26 @@ def _default_result_publish_root(project_root: str) -> str: return os.path.join(normalized_root, "production_results") +def _default_input_root(project_root: str) -> str: + normalized_root = os.path.normpath(project_root) + drive, _tail = os.path.splitdrive(normalized_root) + if drive: + return os.path.join(drive + os.sep, "production_inputs") + return os.path.join(normalized_root, "production_inputs") + + +def _default_runtime_root(project_root: str) -> str: + normalized_root = os.path.normpath(project_root) + drive, _tail = os.path.splitdrive(normalized_root) + if drive: + return os.path.join(drive + os.sep, "production_runtime") + return os.path.join(normalized_root, "runtime") + + +def _default_runtime_dir(project_root: str, *parts: str) -> str: + return os.path.join(_default_runtime_root(project_root), *parts) + + def _read_env_pairs(env_path: str) -> dict[str, str]: env_map: dict[str, str] = {} if not os.path.isfile(env_path): @@ -197,7 +217,6 @@ class Settings(BaseSettings): WATER_RESULTS_DIR: str = "" SAR_ANALYSIS_READY_ROOT: str = "" SAR_ANALYSIS_WORK_ROOT: str = "" - SAR_ANALYSIS_PREVIEW_ROOT: str = "" SAR_ANALYSIS_NODATA_VALUE: float = -9999.0 SAR_ANALYSIS_OUTPUT_COG: bool = True @@ -206,9 +225,11 @@ class Settings(BaseSettings): GF3_ARCHIVE_SOURCE_DIRS: str = "" GF3_ARCHIVE_EXTS: str = ".zip,.tar,.tar.gz,.tgz" GF3_UNPACK_DELETE_ARCHIVE: bool = True + GF3_LEGACY_GDAL_ENABLED: bool = False GF3_SOURCE_DIRS: str = "" GF3_SARSCAPE_NATIVE_DIRS: str = "" GF3_STORAGE_DIRS: str = "" + GF3_SARSCAPE_RUNTIME_DIR: str = "" GF3_SARSCAPE_WRAPPER_EXE: str = "" GF3_SARSCAPE_IDLRT_PATH: str = r"C:\Program Files\Harris\ENVI56\IDL88\bin\bin.x86_64\idlrt.exe" GF3_SARSCAPE_DEM_PATH: str = "" @@ -340,6 +361,7 @@ class Settings(BaseSettings): GAMMA_SBAS_ENV_SCRIPT: str = "" GAMMA_SBAS_WORK_ROOT: str = "" GAMMA_SBAS_PRODUCT_ROOT: str = "" + GAMMA_SBAS_TRIAL_ROOT: str = "" GAMMA_SBAS_SCRIPT_TEMPLATE_ROOT: str = "" GAMMA_SBAS_SOURCE_ROOTS: str = "" GAMMA_SBAS_ORBIT_ROOTS: str = "" @@ -420,19 +442,13 @@ class Settings(BaseSettings): object.__setattr__( self, "SAR_ANALYSIS_READY_ROOT", - os.path.join(backend_dir, "runtime", "sar_analysis_ready"), + os.path.join(_default_result_publish_root(project_root), "sar_analysis_ready"), ) if not self.SAR_ANALYSIS_WORK_ROOT: object.__setattr__( self, "SAR_ANALYSIS_WORK_ROOT", - os.path.join(backend_dir, "runtime", "sar_analysis_work"), - ) - if not self.SAR_ANALYSIS_PREVIEW_ROOT: - object.__setattr__( - self, - "SAR_ANALYSIS_PREVIEW_ROOT", - os.path.join(backend_dir, "runtime", "sar_analysis_preview"), + _default_runtime_dir(project_root, "sar_analysis_work"), ) object.__setattr__( self, @@ -441,11 +457,29 @@ class Settings(BaseSettings): ) if not self.SRTM_DEM_DIR: object.__setattr__(self, "SRTM_DEM_DIR", os.path.join(backend_dir, "dem_data")) + if not self.GF3_ARCHIVE_SOURCE_DIRS: + object.__setattr__( + self, + "GF3_ARCHIVE_SOURCE_DIRS", + os.path.join(_default_input_root(project_root), "gf3", "archives"), + ) + if not self.GF3_SARSCAPE_NATIVE_DIRS: + object.__setattr__( + self, + "GF3_SARSCAPE_NATIVE_DIRS", + os.path.join(_default_result_publish_root(project_root), "gf3", "sarscape_native"), + ) if not self.GF3_STORAGE_DIRS: object.__setattr__( self, "GF3_STORAGE_DIRS", - os.path.join(backend_dir, "gf3_results"), + os.path.join(_default_result_publish_root(project_root), "gf3", "standard_l2"), + ) + if not self.GF3_SARSCAPE_RUNTIME_DIR: + object.__setattr__( + self, + "GF3_SARSCAPE_RUNTIME_DIR", + _default_runtime_dir(project_root, "gf3", "sarscape_runtime"), ) if not self.ORBIT_QUARANTINE_DIR and self.MONITOR_ORBIT_DIR: object.__setattr__( @@ -496,7 +530,7 @@ class Settings(BaseSettings): object.__setattr__( self, "ISCE2_WORK_ROOT", - os.path.join(backend_dir, "runtime", "isce2_work"), + _default_runtime_dir(project_root, "isce2_work"), ) if not self.ISCE2_OUTPUT_ROOT: object.__setattr__( @@ -582,7 +616,7 @@ class Settings(BaseSettings): object.__setattr__( self, "WSL_BROKER_JOB_ROOT", - os.path.join(backend_dir, "runtime", "wsl_jobs"), + _default_runtime_dir(project_root, "wsl_jobs"), ) if not self.ISCE2_RUNTIME_ID: object.__setattr__(self, "ISCE2_RUNTIME_ID", "isce2_runtime_v1") @@ -608,13 +642,13 @@ class Settings(BaseSettings): object.__setattr__( self, "PYINT_TEMPLATE_ROOT", - os.path.join(backend_dir, "runtime", "pyint_templates"), + _default_runtime_dir(project_root, "pyint_templates"), ) if not self.PYINT_WORK_ROOT: object.__setattr__( self, "PYINT_WORK_ROOT", - os.path.join(backend_dir, "runtime", "pyint_work"), + _default_runtime_dir(project_root, "pyint_work"), ) if not self.PYINT_OUTPUT_ROOT: object.__setattr__( @@ -626,7 +660,7 @@ class Settings(BaseSettings): object.__setattr__( self, "PYINT_DEM_ROOT", - os.path.join(backend_dir, "runtime", "pyint_dem"), + os.path.join(_default_runtime_root(project_root), "pyint_dem"), ) pyint_dem_mode = str(self.PYINT_DEM_MODE or "local_fabdem").strip().lower() or "local_fabdem" if pyint_dem_mode not in {"local_fabdem", "opentopo", "prepared_file"}: @@ -743,7 +777,7 @@ class Settings(BaseSettings): object.__setattr__( self, "GAMMA_SBAS_WORK_ROOT", - os.path.join(backend_dir, "runtime", "sbas_insar_production"), + os.path.join(_default_runtime_root(project_root), "sbas_insar_work"), ) if not self.GAMMA_SBAS_PRODUCT_ROOT: object.__setattr__( @@ -751,6 +785,12 @@ class Settings(BaseSettings): "GAMMA_SBAS_PRODUCT_ROOT", os.path.join(self.TIMESERIES_PRODUCT_DIR, "sbas"), ) + if not self.GAMMA_SBAS_TRIAL_ROOT: + object.__setattr__( + self, + "GAMMA_SBAS_TRIAL_ROOT", + os.path.join(_default_runtime_root(project_root), "gamma_ipta_trials"), + ) if not self.GAMMA_SBAS_SCRIPT_TEMPLATE_ROOT: object.__setattr__( self, @@ -848,7 +888,7 @@ class Settings(BaseSettings): object.__setattr__( self, "TIMESERIES_WORK_ROOT", - os.path.join(backend_dir, "runtime", "timeseries_work"), + _default_runtime_dir(project_root, "timeseries_work"), ) if not self.TIMESERIES_DEM_PATH: object.__setattr__(self, "TIMESERIES_DEM_PATH", self.ISCE2_DEM_PATH) @@ -947,7 +987,14 @@ class Settings(BaseSettings): os.makedirs(settings.RESULT_QUARANTINE_ROOT, exist_ok=True) os.makedirs(settings.SAR_ANALYSIS_READY_ROOT, exist_ok=True) os.makedirs(settings.SAR_ANALYSIS_WORK_ROOT, exist_ok=True) - os.makedirs(settings.SAR_ANALYSIS_PREVIEW_ROOT, exist_ok=True) + for path in split_env_paths(settings.GF3_ARCHIVE_SOURCE_DIRS): + os.makedirs(path, exist_ok=True) + for path in split_env_paths(settings.GF3_SARSCAPE_NATIVE_DIRS): + os.makedirs(path, exist_ok=True) + for path in split_env_paths(settings.GF3_STORAGE_DIRS): + os.makedirs(path, exist_ok=True) + if settings.GF3_SARSCAPE_RUNTIME_DIR: + os.makedirs(settings.GF3_SARSCAPE_RUNTIME_DIR, exist_ok=True) os.makedirs(settings.WSL_BROKER_JOB_ROOT, exist_ok=True) os.makedirs(settings.PYINT_TEMPLATE_ROOT, exist_ok=True) os.makedirs(settings.PYINT_WORK_ROOT, exist_ok=True) @@ -958,6 +1005,7 @@ class Settings(BaseSettings): if settings.GAMMA_SBAS_ENABLED: os.makedirs(settings.GAMMA_SBAS_WORK_ROOT, exist_ok=True) os.makedirs(settings.GAMMA_SBAS_PRODUCT_ROOT, exist_ok=True) + os.makedirs(settings.GAMMA_SBAS_TRIAL_ROOT, exist_ok=True) os.makedirs(settings.GAMMA_SBAS_SCRIPT_TEMPLATE_ROOT, exist_ok=True) if settings.LANDSAR_SBAS_ENABLED: os.makedirs(settings.LANDSAR_SBAS_WORK_ROOT, exist_ok=True) @@ -1115,7 +1163,6 @@ def validate_runtime_config() -> dict[str, Any]: _check_path(label="WATER_RESULTS_DIR", value=settings.WATER_RESULTS_DIR, errors=errors, warnings=warnings, expect_file=False) _check_path(label="SAR_ANALYSIS_READY_ROOT", value=settings.SAR_ANALYSIS_READY_ROOT, errors=errors, warnings=warnings, expect_file=False) _check_path(label="SAR_ANALYSIS_WORK_ROOT", value=settings.SAR_ANALYSIS_WORK_ROOT, errors=errors, warnings=warnings, expect_file=False) - _check_path(label="SAR_ANALYSIS_PREVIEW_ROOT", value=settings.SAR_ANALYSIS_PREVIEW_ROOT, errors=errors, warnings=warnings, expect_file=False) _check_path(label="MONITOR_ORBIT_DIR", value=settings.MONITOR_ORBIT_DIR, errors=errors, warnings=warnings, expect_file=False) for label, value in ( ("SOURCE_PRODUCT_DIRS", settings.SOURCE_PRODUCT_DIRS), @@ -1143,10 +1190,9 @@ def validate_runtime_config() -> dict[str, Any]: ("INSAR_STORAGE_DIRS", settings.INSAR_STORAGE_DIRS), ("MONITOR_RADAR_DIRS", settings.MONITOR_RADAR_DIRS), ("MONITOR_DINSAR_DIRS", settings.MONITOR_DINSAR_DIRS), - ("GF3_ARCHIVE_SOURCE_DIRS", settings.GF3_ARCHIVE_SOURCE_DIRS), - ("GF3_SOURCE_DIRS", settings.GF3_SOURCE_DIRS), ("GF3_SARSCAPE_NATIVE_DIRS", settings.GF3_SARSCAPE_NATIVE_DIRS), ("GF3_STORAGE_DIRS", settings.GF3_STORAGE_DIRS), + ("GF3_SARSCAPE_RUNTIME_DIR", settings.GF3_SARSCAPE_RUNTIME_DIR), ): values = split_env_paths(raw_value) if not values: @@ -1155,6 +1201,21 @@ def validate_runtime_config() -> dict[str, Any]: for item in values: _check_path(label=label, value=item, errors=errors, warnings=warnings, expect_file=False) + gf3_archive_dirs = split_env_paths(settings.GF3_ARCHIVE_SOURCE_DIRS) + if not gf3_archive_dirs: + warnings.append("GF3_ARCHIVE_SOURCE_DIRS 未配置;无法触发 GF3 SARscape 生产,只能扫描已有原生结果。") + for item in gf3_archive_dirs: + _check_path(label="GF3_ARCHIVE_SOURCE_DIRS", value=item, errors=errors, warnings=warnings, expect_file=False) + + if settings.GF3_LEGACY_GDAL_ENABLED: + legacy_dirs = split_env_paths(settings.GF3_SOURCE_DIRS) + if not legacy_dirs: + errors.append("GF3_LEGACY_GDAL_ENABLED=true but GF3_SOURCE_DIRS is not configured.") + for item in legacy_dirs: + _check_path(label="GF3_SOURCE_DIRS", value=item, errors=errors, warnings=warnings, expect_file=False) + elif split_env_paths(settings.GF3_SOURCE_DIRS): + warnings.append("GF3_SOURCE_DIRS is configured but GF3_LEGACY_GDAL_ENABLED=false; legacy GDAL preprocessing is disabled.") + if settings.ISCE2_ENABLED: if not settings.ISCE2_WSL_DISTRO: errors.append("ISCE2_ENABLED=true 但 ISCE2_WSL_DISTRO 未配置。") @@ -1279,6 +1340,13 @@ def validate_runtime_config() -> dict[str, Any]: warnings=warnings, expect_file=False, ) + _check_path( + label="GAMMA_SBAS_TRIAL_ROOT", + value=settings.GAMMA_SBAS_TRIAL_ROOT, + errors=errors, + warnings=warnings, + expect_file=False, + ) _check_path( label="GAMMA_SBAS_SCRIPT_TEMPLATE_ROOT", value=settings.GAMMA_SBAS_SCRIPT_TEMPLATE_ROOT, diff --git a/backend/app/main.py b/backend/app/main.py index d732f97..1c38e6e 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -88,7 +88,7 @@ async def lifespan(app: FastAPI): sbas_catalog_bootstrap = await sbas_insar_catalog_service.bootstrap_catalog_on_startup_clean() except Exception as exc: sbas_catalog_bootstrap = { - "storage_root": settings.GAMMA_SBAS_WORK_ROOT, + "storage_root": settings.GAMMA_SBAS_PRODUCT_ROOT, "manifest_count": 0, "db_count": 0, "needs_rebuild": False, diff --git a/backend/app/routers/dinsar_products.py b/backend/app/routers/dinsar_products.py index 2e4dd3d..1d0efbc 100644 --- a/backend/app/routers/dinsar_products.py +++ b/backend/app/routers/dinsar_products.py @@ -260,4 +260,8 @@ async def get_dinsar_product_preview( if not preview_path or not os.path.isfile(preview_path): raise HTTPException(status_code=404, detail="Preview not found") media_type = mimetypes.guess_type(preview_path)[0] or "application/octet-stream" - return FileResponse(preview_path, media_type=media_type) + return FileResponse( + preview_path, + media_type=media_type, + headers={"Cache-Control": "public, max-age=31536000, immutable"}, + ) diff --git a/backend/app/routers/monitor.py b/backend/app/routers/monitor.py index a9f5653..21fbb89 100644 --- a/backend/app/routers/monitor.py +++ b/backend/app/routers/monitor.py @@ -43,8 +43,10 @@ class MonitorConfig(BaseModel): dinsar_dirs: List[str] = [] gf3_archive_source_dirs: List[str] = [] gf3_source_dirs: List[str] = [] + gf3_legacy_gdal_enabled: bool = False gf3_sarscape_native_dirs: List[str] = [] gf3_storage_dirs: List[str] = [] + gf3_sarscape_runtime_dir: Optional[str] = None gf3_sarscape_wrapper_exe: Optional[str] = None gf3_sarscape_idlrt_path: Optional[str] = None gf3_sarscape_dem_path: Optional[str] = None @@ -159,6 +161,14 @@ async def run_gf3_batch_process(admin_user: AuthUserORM = Depends(_require_admin """ 批量 GF3 L1A→L2 处理:扫描 GF3_SOURCE_DIRS,过滤已处理的,逐个处理并自动入库。 """ + if not settings.GF3_LEGACY_GDAL_ENABLED: + raise HTTPException( + status_code=409, + detail=( + "Legacy GF3 Python/GDAL preprocessing is disabled. " + "Use GF3 SARscape production or set GF3_LEGACY_GDAL_ENABLED=true explicitly." + ), + ) gf3_source_dirs = MONITOR_CONFIG.get("gf3_source_dirs") or [] if not gf3_source_dirs: raise HTTPException(status_code=400, detail="GF3_SOURCE_DIRS is not configured.") @@ -331,6 +341,14 @@ async def run_gf3_unpack( """ 将 GF3 压缩包池解包到 GF3_SOURCE_DIRS,作为后续 L1A→L2 预处理输入。 """ + if not settings.GF3_LEGACY_GDAL_ENABLED: + raise HTTPException( + status_code=409, + detail=( + "Legacy GF3 archive unpack is disabled. " + "Use GF3 SARscape production or set GF3_LEGACY_GDAL_ENABLED=true explicitly." + ), + ) gf3_archive_source_dirs = MONITOR_CONFIG.get("gf3_archive_source_dirs") or [] gf3_source_dirs = MONITOR_CONFIG.get("gf3_source_dirs") or [] if not gf3_archive_source_dirs: diff --git a/backend/app/routers/ps_products.py b/backend/app/routers/ps_products.py index aa1fe70..2436419 100644 --- a/backend/app/routers/ps_products.py +++ b/backend/app/routers/ps_products.py @@ -142,4 +142,8 @@ async def get_psinsar_product_preview( preview_path = str(detail.get("preview_path") or "").strip() if not preview_path or not os.path.isfile(preview_path): raise HTTPException(status_code=404, detail="Preview not found") - return FileResponse(preview_path, media_type="image/png") + return FileResponse( + preview_path, + media_type="image/png", + headers={"Cache-Control": "public, max-age=31536000, immutable"}, + ) diff --git a/backend/app/routers/sbas_insar_production.py b/backend/app/routers/sbas_insar_production.py index 5dbfde8..dfdc6ba 100644 --- a/backend/app/routers/sbas_insar_production.py +++ b/backend/app/routers/sbas_insar_production.py @@ -479,6 +479,7 @@ async def get_landsar_sbas_run_artifact(run_id: str, relative_path: str): artifact_path, media_type=media_type, filename=artifact_path.name, + headers={"Cache-Control": "public, max-age=31536000, immutable"}, ) @@ -1022,6 +1023,7 @@ async def get_sbas_insar_run_artifact(run_id: str, relative_path: str): artifact_path, media_type=media_type, filename=artifact_path.name, + headers={"Cache-Control": "public, max-age=31536000, immutable"}, ) @@ -1054,4 +1056,5 @@ async def get_sbas_insar_artifact(trial_id: str, relative_path: str): artifact_path, media_type=media_type, filename=artifact_path.name, + headers={"Cache-Control": "public, max-age=31536000, immutable"}, ) diff --git a/backend/app/routers/sbas_insar_products.py b/backend/app/routers/sbas_insar_products.py index d50225f..ff177ae 100644 --- a/backend/app/routers/sbas_insar_products.py +++ b/backend/app/routers/sbas_insar_products.py @@ -21,6 +21,7 @@ from .dependencies import _add_operation_audit_log, _get_current_user, _require_ router = APIRouter() +STATIC_ASSET_CACHE_HEADERS = {"Cache-Control": "public, max-age=31536000, immutable"} class SbasInsarCatalogRebuildRequest(BaseModel): @@ -189,7 +190,7 @@ async def get_sbas_insar_product_preview( preview_path = str(detail.get("preview_path") or "").strip() if not preview_path or not os.path.isfile(preview_path): raise HTTPException(status_code=404, detail="Preview not found") - return FileResponse(preview_path, media_type="image/png") + return FileResponse(preview_path, media_type="image/png", headers=STATIC_ASSET_CACHE_HEADERS) @router.get("/sbas-insar-products/{product_db_id}/assets/{asset_id}") @@ -209,4 +210,5 @@ async def get_sbas_insar_product_asset( asset.absolute_path, media_type=asset.media_type or "application/octet-stream", filename=asset.asset_name or os.path.basename(asset.absolute_path), + headers=STATIC_ASSET_CACHE_HEADERS, ) diff --git a/backend/app/routers/water.py b/backend/app/routers/water.py index 3d17327..69a83f8 100644 --- a/backend/app/routers/water.py +++ b/backend/app/routers/water.py @@ -902,6 +902,14 @@ async def submit_gf3_process( admin_user: AuthUserORM = Depends(_require_admin), ): """提交 GF3 L1A→L2 处理任务(辐射定标 + RPC 几何校正)。""" + if not settings.GF3_LEGACY_GDAL_ENABLED: + raise HTTPException( + status_code=409, + detail=( + "Legacy GF3 Python/GDAL preprocessing is disabled. " + "Use /monitor/gf3-sarscape-produce or /monitor/gf3-sarscape-sync." + ), + ) if not os.path.exists(req.input_dir): raise HTTPException(status_code=400, detail=f"输入路径不存在: {req.input_dir}") diff --git a/backend/app/scheduler.py b/backend/app/scheduler.py index 250898a..e5405e7 100644 --- a/backend/app/scheduler.py +++ b/backend/app/scheduler.py @@ -21,8 +21,10 @@ MONITOR_CONFIG = { # GF3 链路 "gf3_archive_source_dirs": split_env_paths(settings.GF3_ARCHIVE_SOURCE_DIRS), "gf3_source_dirs": split_env_paths(settings.GF3_SOURCE_DIRS), + "gf3_legacy_gdal_enabled": bool(settings.GF3_LEGACY_GDAL_ENABLED), "gf3_sarscape_native_dirs": split_env_paths(settings.GF3_SARSCAPE_NATIVE_DIRS), "gf3_storage_dirs": split_env_paths(settings.GF3_STORAGE_DIRS), + "gf3_sarscape_runtime_dir": settings.GF3_SARSCAPE_RUNTIME_DIR, "gf3_sarscape_wrapper_exe": settings.GF3_SARSCAPE_WRAPPER_EXE, "gf3_sarscape_idlrt_path": settings.GF3_SARSCAPE_IDLRT_PATH, "gf3_sarscape_dem_path": settings.GF3_SARSCAPE_DEM_PATH or settings.GF3_GEO_DEM_PATH, diff --git a/backend/app/services/gf3_sarscape_production_service.py b/backend/app/services/gf3_sarscape_production_service.py index 1641b73..ad1e7f3 100644 --- a/backend/app/services/gf3_sarscape_production_service.py +++ b/backend/app/services/gf3_sarscape_production_service.py @@ -115,7 +115,7 @@ def _resolve_config_path(raw: Any) -> Path | None: def _fallback_wrapper_exe() -> Path | None: candidate = ( Path(settings.PROJECT_ROOT) - / ".codex_tmp" + / "third_party" / "GF3_L1A_To_L2_pipeline" / "dist" / "windows" @@ -443,7 +443,12 @@ def run_gf3_sarscape_production( "results": [], } - runtime_dir = native_root_path / ".gf3_runtime" + configured_runtime_dir = _clean_text(getattr(settings, "GF3_SARSCAPE_RUNTIME_DIR", "")) + runtime_dir = ( + Path(os.path.normpath(configured_runtime_dir)).resolve() + if configured_runtime_dir + else native_root_path / ".gf3_runtime" + ) runtime_dir.mkdir(parents=True, exist_ok=True) config_path = runtime_dir / "gf3wrapper.json" env = os.environ.copy() diff --git a/backend/app/services/health_service.py b/backend/app/services/health_service.py index fe8c09b..5c7753e 100644 --- a/backend/app/services/health_service.py +++ b/backend/app/services/health_service.py @@ -969,7 +969,11 @@ async def _check_source_roots() -> Dict[str, Any]: for path in split_env_paths(settings.GF3_SOURCE_DIRS): status = _probe_directory_status(path) - status["role"] = "gf3_l1a_source" + status["role"] = ( + "gf3_l1a_source" + if settings.GF3_LEGACY_GDAL_ENABLED + else "gf3_legacy_l1a_source_disabled" + ) items.append(status) for path in split_env_paths(settings.GF3_SARSCAPE_NATIVE_DIRS): @@ -982,6 +986,12 @@ async def _check_source_roots() -> Dict[str, Any]: status["role"] = "gf3_l2_storage" items.append(status) + runtime_dir = str(getattr(settings, "GF3_SARSCAPE_RUNTIME_DIR", "") or "").strip() + if runtime_dir: + status = _probe_directory_status(runtime_dir) + status["role"] = "gf3_sarscape_runtime" + items.append(status) + wrapper_exe = str(settings.GF3_SARSCAPE_WRAPPER_EXE or "").strip() if wrapper_exe: status = _probe_file_status(wrapper_exe) @@ -1017,7 +1027,6 @@ async def _check_sar_analysis_ready() -> Dict[str, Any]: roots = { "ready": _probe_directory_status(settings.SAR_ANALYSIS_READY_ROOT), "work": _probe_directory_status(settings.SAR_ANALYSIS_WORK_ROOT), - "preview": _probe_directory_status(settings.SAR_ANALYSIS_PREVIEW_ROOT), } for role, payload in roots.items(): payload["role"] = role diff --git a/backend/app/services/job_handlers.py b/backend/app/services/job_handlers.py index ba44fab..7d7764f 100644 --- a/backend/app/services/job_handlers.py +++ b/backend/app/services/job_handlers.py @@ -3612,6 +3612,12 @@ async def _handle_gf3_process(job: SystemJobORM) -> None: """GF3 L1A→L2 处理 job handler(辐射定标 + RPC 几何校正)。""" from .gf3_service import run_gf3_l1a_to_l2 + if not settings.GF3_LEGACY_GDAL_ENABLED: + raise ValueError( + "Legacy GF3 Python/GDAL preprocessing is disabled. " + "Use GF3 SARscape production or set GF3_LEGACY_GDAL_ENABLED=true explicitly." + ) + payload = job.payload or {} processing_id = payload.get("processing_id") if not processing_id: @@ -3685,6 +3691,12 @@ async def _handle_gf3_unpack(job: SystemJobORM) -> None: """GF3 archive inbox -> persistent L1A source pool.""" from .gf3_unpack_service import run_gf3_archive_unpack + if not settings.GF3_LEGACY_GDAL_ENABLED: + raise ValueError( + "Legacy GF3 archive unpack is disabled. " + "Use GF3 SARscape production or set GF3_LEGACY_GDAL_ENABLED=true explicitly." + ) + if not job.task_id: raise ValueError("GF3_UNPACK requires task_id for progress tracking.") @@ -3746,6 +3758,12 @@ async def _handle_gf3_batch_process(job: SystemJobORM) -> None: from .gf3_service import run_gf3_l1a_to_l2, register_l2_to_radar_data from .sar_analysis_ready_service import standardize_gf3_l2_for_radar + if not settings.GF3_LEGACY_GDAL_ENABLED: + raise ValueError( + "Legacy GF3 Python/GDAL preprocessing is disabled. " + "Use GF3 SARscape production or set GF3_LEGACY_GDAL_ENABLED=true explicitly." + ) + payload = job.payload or {} source_dirs = payload.get("source_dirs") or [] if not source_dirs: diff --git a/backend/app/services/root_registry_service.py b/backend/app/services/root_registry_service.py index 5554c09..29f1bc9 100644 --- a/backend/app/services/root_registry_service.py +++ b/backend/app/services/root_registry_service.py @@ -271,6 +271,16 @@ def _build_root_specs_from_settings() -> List[RootSpec]: scan_mode="scene_directory", ) ) + specs.extend( + _iter_single_root_specs( + env_var="GF3_SARSCAPE_RUNTIME_DIR", + path=settings.GF3_SARSCAPE_RUNTIME_DIR, + root_role="work_root_gf3_sarscape", + display_name="GF3 SARscape Runtime", + scan_mode="workspace", + owner_engine="sarscape", + ) + ) specs.extend( _iter_single_root_specs( env_var="SAR_ANALYSIS_READY_ROOT", @@ -438,6 +448,46 @@ def _build_root_specs_from_settings() -> List[RootSpec]: owner_engine="pyint", ) ) + specs.extend( + _iter_single_root_specs( + env_var="PYINT_DEM_ROOT", + path=settings.PYINT_DEM_ROOT, + root_role="dem_cache_root_pyint", + display_name="PyINT DEM Cache Root", + scan_mode="workspace", + owner_engine="pyint", + ) + ) + specs.extend( + _iter_single_root_specs( + env_var="GAMMA_SBAS_WORK_ROOT", + path=settings.GAMMA_SBAS_WORK_ROOT, + root_role="work_root_gamma_sbas", + display_name="Gamma SBAS Work Root", + scan_mode="workspace", + owner_engine="gamma", + ) + ) + specs.extend( + _iter_single_root_specs( + env_var="GAMMA_SBAS_PRODUCT_ROOT", + path=settings.GAMMA_SBAS_PRODUCT_ROOT, + root_role="publish_root_gamma_sbas", + display_name="Gamma SBAS Publish Root", + scan_mode="manifest_tree", + owner_engine="gamma", + ) + ) + specs.extend( + _iter_single_root_specs( + env_var="GAMMA_SBAS_TRIAL_ROOT", + path=settings.GAMMA_SBAS_TRIAL_ROOT, + root_role="trial_root_gamma_sbas", + display_name="Gamma SBAS Trial Root", + scan_mode="workspace", + owner_engine="gamma", + ) + ) return specs diff --git a/backend/app/services/sbas_insar_catalog_service.py b/backend/app/services/sbas_insar_catalog_service.py index 56ea2df..91585e1 100644 --- a/backend/app/services/sbas_insar_catalog_service.py +++ b/backend/app/services/sbas_insar_catalog_service.py @@ -289,6 +289,50 @@ def _read_gamma_int_param(path: Path, key: str) -> Optional[int]: return _safe_int(_read_gamma_key_values(path).get(key)) +def _expert_gamma_work_run_dir(run_dir: Path) -> Path: + run_id = run_dir.name + candidates = [ + run_dir, + Path(settings.GAMMA_SBAS_WORK_ROOT or "") / "runs" / run_id if settings.GAMMA_SBAS_WORK_ROOT else run_dir, + ] + seen: set[str] = set() + for candidate in candidates: + normalized = _normalize_path(candidate) + if normalized in seen: + continue + seen.add(normalized) + if ( + (candidate / "sbas" / "disp.TS_tab").is_file() + and (candidate / "sbas" / "mli.ave.par").is_file() + and any((candidate / "dem").glob("*.lt_fine")) + ): + return candidate + return run_dir + + +def _expert_gamma_lookup_path(work_run_dir: Path, manifest: dict[str, Any]) -> Path: + lt_path = work_run_dir / "dem" / f"{manifest.get('reference_date') or ''}.lt_fine" + if lt_path.is_file(): + return lt_path + candidates = sorted((work_run_dir / "dem").glob("*.lt_fine")) + return candidates[0] if candidates else lt_path + + +def _resolve_expert_gamma_tab_path(raw_path: str, *, work_run_dir: Path) -> Path: + path = Path(_wsl_path_to_windows(raw_path)) + if path.is_file(): + return path + run_id = work_run_dir.name + parts = list(path.parts) + if run_id in parts: + index = parts.index(run_id) + relative_parts = parts[index + 1 :] + candidate = work_run_dir.joinpath(*relative_parts) + if candidate.is_file(): + return candidate + return path + + def _haversine_m(lon1: float, lat1: float, lon2: float, lat2: float) -> float: radius_m = 6371008.8 phi1 = math.radians(lat1) @@ -762,12 +806,13 @@ def _read_geo_rate_window( def _query_expert_gamma_point_timeseries(run_dir: Path, *, lon: float, lat: float) -> dict[str, Any]: source_path = run_dir / "publish" / "geotiff" / "geo_los_def_rate.tif" coverage_path = run_dir / "publish" / "geotiff" / "geo_los_def_rate_rgb.tif" - lt_path = run_dir / "dem" / f"{_safe_read_json(run_dir / 'run_manifest.json').get('reference_date') or ''}.lt_fine" - if not lt_path.is_file(): - candidates = sorted((run_dir / "dem").glob("*.lt_fine")) - lt_path = candidates[0] if candidates else lt_path - mli_par = run_dir / "sbas" / "mli.ave.par" - disp_tab = run_dir / "sbas" / "disp.TS_tab" + work_run_dir = _expert_gamma_work_run_dir(run_dir) + work_manifest = _safe_read_json(work_run_dir / "run_manifest.json") + if not work_manifest: + work_manifest = _safe_read_json(run_dir / "run_manifest.json") + lt_path = _expert_gamma_lookup_path(work_run_dir, work_manifest) + mli_par = work_run_dir / "sbas" / "mli.ave.par" + disp_tab = work_run_dir / "sbas" / "disp.TS_tab" if not source_path.is_file(): raise FileNotFoundError("geo_los_def_rate.tif is missing") if not lt_path.is_file(): @@ -852,11 +897,13 @@ def _query_expert_gamma_point_timeseries(run_dir: Path, *, lon: float, lat: floa img_y = min(max(0, img_y), radar_lines - 1) raw_disp_paths = [ - Path(_wsl_path_to_windows(line.strip())) + _resolve_expert_gamma_tab_path(line.strip(), work_run_dir=work_run_dir) for line in disp_tab.read_text(encoding="utf-8", errors="ignore").splitlines() if line.strip() ] - dates = _read_expert_sbas_dates(run_dir) + dates = _read_expert_sbas_dates(work_run_dir) + if not dates: + dates = _read_expert_sbas_dates(run_dir) displacements: list[dict[str, Any]] = [] for index, disp_path in enumerate(raw_disp_paths): value_m = _read_radar_float32(disp_path, width=radar_width, img_x=img_x, img_y=img_y) @@ -872,6 +919,8 @@ def _query_expert_gamma_point_timeseries(run_dir: Path, *, lon: float, lat: floa return { "schema": "insar.gamma-sbas-point-query/v1", "source_tool": "disp.TS_tab_radar_pixel_sample", + "source_run_dir": str(run_dir), + "work_run_dir": str(work_run_dir), "query": {"lon": lon, "lat": lat}, "matched": { **valid_choice, @@ -906,11 +955,11 @@ def _locate_radar_points_in_geocoded_product(run_dir: Path, points: list[dict[st source_path = run_dir / "publish" / "geotiff" / "geo_los_def_rate.tif" coverage_path = run_dir / "publish" / "geotiff" / "geo_los_def_rate_rgb.tif" - manifest = _safe_read_json(run_dir / "run_manifest.json") - lt_path = run_dir / "dem" / f"{manifest.get('reference_date') or ''}.lt_fine" - if not lt_path.is_file(): - candidates = sorted((run_dir / "dem").glob("*.lt_fine")) - lt_path = candidates[0] if candidates else lt_path + work_run_dir = _expert_gamma_work_run_dir(run_dir) + manifest = _safe_read_json(work_run_dir / "run_manifest.json") + if not manifest: + manifest = _safe_read_json(run_dir / "run_manifest.json") + lt_path = _expert_gamma_lookup_path(work_run_dir, manifest) if not source_path.is_file() or not lt_path.is_file(): return {} @@ -1982,6 +2031,12 @@ def _stack_dates_from_manifest(stack_manifest: dict[str, Any], manifest: dict[st class SbasInsarCatalogService: def get_run_root(self) -> str: + root = Path(settings.GAMMA_SBAS_PRODUCT_ROOT or Path(settings.TIMESERIES_PRODUCT_DIR) / "sbas") + run_root = root / "runs" + run_root.mkdir(parents=True, exist_ok=True) + return _normalize_path(run_root) + + def get_work_run_root(self) -> str: root = Path(settings.GAMMA_SBAS_WORK_ROOT or Path(settings.BACKEND_DIR) / "runtime" / "sbas_insar_production") run_root = root / "runs" run_root.mkdir(parents=True, exist_ok=True) @@ -1989,6 +2044,9 @@ class SbasInsarCatalogService: def get_run_roots(self) -> list[str]: roots = [self.get_run_root()] + work_root = self.get_work_run_root() + if work_root not in roots: + roots.append(work_root) try: landsar_root = landsar_sbas_service.configured_run_root() if landsar_root not in roots: @@ -1999,17 +2057,17 @@ class SbasInsarCatalogService: def _iter_run_manifest_paths(self, run_root: Optional[str] = None) -> list[str]: roots = [run_root] if run_root else self.get_run_roots() - manifest_paths: list[str] = [] + manifest_paths_by_run: dict[str, str] = {} for raw_root in roots: root = Path(raw_root) if not root.is_dir(): continue - manifest_paths.extend( - _normalize_path(path) - for path in sorted(root.glob("*/run_manifest.json")) - if self._is_publish_ready(path.parent, _safe_read_json(path)) - ) - return sorted(dict.fromkeys(manifest_paths)) + for path in sorted(root.glob("*/run_manifest.json")): + if not self._is_publish_ready(path.parent, _safe_read_json(path)): + continue + run_id = path.parent.name + manifest_paths_by_run.setdefault(run_id, _normalize_path(path)) + return list(manifest_paths_by_run.values()) @staticmethod def _is_landsar_manifest(manifest: dict[str, Any]) -> bool: @@ -2295,9 +2353,12 @@ class SbasInsarCatalogService: if self._is_landsar_manifest(manifest): return self._build_landsar_product(run_dir, manifest_file, manifest) - detail = sbas_insar_production_service.get_run_detail(run_dir.name) - coverage = detail.get("geographic_coverage") or {} stack_manifest = _safe_read_json(run_dir / "stack_manifest.json") + try: + detail = sbas_insar_production_service.get_run_detail(run_dir.name) + coverage = detail.get("geographic_coverage") or {} + except FileNotFoundError: + coverage = sbas_insar_production_service._build_run_geographic_coverage(run_dir, manifest) monitor_summary = _safe_read_json(run_dir / "monitor_points_summary.json") workflow_summary = _safe_read_json(run_dir / "workflow_summary.json") is_expert_gamma = self._is_expert_gamma_manifest(manifest, run_dir) diff --git a/backend/app/services/sbas_insar_production_service.py b/backend/app/services/sbas_insar_production_service.py index 06fc0ad..8dfdcf5 100644 --- a/backend/app/services/sbas_insar_production_service.py +++ b/backend/app/services/sbas_insar_production_service.py @@ -786,8 +786,20 @@ class SbasInsarProductionService: } def __init__(self) -> None: - self.trial_root = Path(settings.BACKEND_DIR) / "runtime" / "gamma_ipta_trials" - self.production_root = Path(settings.GAMMA_SBAS_WORK_ROOT or (Path(settings.BACKEND_DIR) / "runtime" / "sbas_insar_production")) + project_drive = Path(settings.PROJECT_ROOT).drive + default_runtime_root = Path(f"{project_drive}\\production_runtime") if project_drive else Path(settings.PROJECT_ROOT) / "runtime" + self.trial_root = Path(settings.GAMMA_SBAS_TRIAL_ROOT or default_runtime_root / "gamma_ipta_trials") + self.production_root = Path(settings.GAMMA_SBAS_WORK_ROOT or default_runtime_root / "sbas_insar_work") + self.product_root = Path(settings.GAMMA_SBAS_PRODUCT_ROOT or Path(settings.TIMESERIES_PRODUCT_DIR) / "sbas") + + def product_run_root(self) -> Path: + return self.product_root / "runs" + + def product_run_dir(self, run_id: str) -> Path: + clean_id = str(run_id or "").strip() + if not clean_id or Path(clean_id).name != clean_id: + raise ValueError("invalid run id") + return self.product_run_root() / clean_id def get_capabilities(self) -> dict[str, Any]: return { @@ -797,6 +809,7 @@ class SbasInsarProductionService: "implementation_state": "expert_manifest_script_runner_primary", "trial_root": str(self.trial_root), "production_root": str(self.production_root), + "product_root": str(self.product_root), "min_common_overlap_ratio": self._effective_min_common_overlap_ratio(None), "workflow_runner": { "enabled": bool(settings.GAMMA_SBAS_ENABLED), @@ -3412,6 +3425,8 @@ class SbasInsarProductionService: self._write_json(run_dir / "monitor_points_summary.json", summary) self._write_json(manifest_path, manifest) self._refresh_command_manifest_after_monitor_points(run_dir, manifest) + if manifest["status"] == "MONITOR_POINTS_READY": + self.sync_product_package(run_id) return self.get_run_detail(run_id) def list_trial_runs(self) -> dict[str, Any]: @@ -3828,6 +3843,8 @@ class SbasInsarProductionService: run_manifest["next_stage"] = "fix_workflow" self._write_json(manifest_path, run_manifest) self._write_json(run_dir / "workflow_summary.json", summary) + if run_manifest.get("status") == "WORKFLOW_COMPLETED": + self.sync_product_package(run_id) return self.get_run_detail(run_id) @staticmethod @@ -5204,6 +5221,7 @@ class SbasInsarProductionService: errors.append(f"{label} does not point to an existing Gamma DEM + .par pair: {raw_path}") cache_roots = [ + Path(settings.PYINT_DEM_ROOT), Path(settings.BACKEND_DIR) / "runtime" / "pyint_dem", Path(settings.BACKEND_DIR) / "runtime" / "pyint_dem_cache", ] @@ -7138,6 +7156,118 @@ class SbasInsarProductionService: out_path.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8") return out_path + @staticmethod + def _copy_file_if_newer(source: Path, target: Path) -> bool: + if not source.is_file(): + return False + target.parent.mkdir(parents=True, exist_ok=True) + try: + source_stat = source.stat() + target_stat = target.stat() if target.exists() else None + if ( + target_stat is not None + and target_stat.st_size == source_stat.st_size + and int(target_stat.st_mtime) >= int(source_stat.st_mtime) + ): + return False + shutil.copy2(source, target) + return True + except OSError: + shutil.copy2(source, target) + return True + + @staticmethod + def _copy_tree_files(source_dir: Path, target_dir: Path) -> tuple[int, int]: + if not source_dir.is_dir(): + return 0, 0 + copied = 0 + skipped = 0 + for source in source_dir.rglob("*"): + if not source.is_file(): + continue + target = target_dir / source.relative_to(source_dir) + if SbasInsarProductionService._copy_file_if_newer(source, target): + copied += 1 + else: + skipped += 1 + return copied, skipped + + def sync_product_package(self, run_id: str) -> dict[str, Any]: + run_dir = self._resolve_run_dir(run_id) + product_dir = self.product_run_dir(run_dir.name) + product_dir.mkdir(parents=True, exist_ok=True) + copied = 0 + skipped = 0 + + for relative in ( + "run_manifest.json", + "stack_manifest.json", + "pair_network.json", + "workflow_summary.json", + "monitor_points_summary.json", + "product_summary.json", + "quality_summary.json", + "gamma_command_manifest.json", + "expert_command_audit.json", + ): + if self._copy_file_if_newer(run_dir / relative, product_dir / relative): + copied += 1 + else: + skipped += 1 + + for dirname in ("publish",): + tree_copied, tree_skipped = self._copy_tree_files(run_dir / dirname, product_dir / dirname) + copied += tree_copied + skipped += tree_skipped + + for relative in ( + "diff_dir/bprep_file.png", + "diff_dir/mean.cc_mask.bmp", + "sbas/final_unw_tab", + ): + if self._copy_file_if_newer(run_dir / relative, product_dir / relative): + copied += 1 + else: + skipped += 1 + + final_tab = run_dir / "sbas" / "final_unw_tab" + diff_dir = run_dir / "diff_dir" + pair_ids: list[str] = [] + if final_tab.is_file(): + for line in final_tab.read_text(encoding="utf-8", errors="ignore").splitlines(): + raw_path = line.strip().split()[0] if line.strip() else "" + if not raw_path: + continue + name = Path(self._path_to_windows(raw_path) or raw_path).name + pair_id = name.replace(".unw.atmsub_1", "").replace(".unw", "") + if pair_id and pair_id not in pair_ids: + pair_ids.append(pair_id) + qcs = [diff_dir / f"{pair_id}.adf.unw.bmp" for pair_id in pair_ids if (diff_dir / f"{pair_id}.adf.unw.bmp").is_file()] + if not qcs and diff_dir.is_dir(): + qcs = sorted(diff_dir.glob("*.adf.unw.bmp")) + if len(qcs) > 3: + last_index = len(qcs) - 1 + indexes = sorted({round(index * last_index / 2) for index in range(3)}) + qcs = [qcs[index] for index in indexes] + for source in qcs: + target = product_dir / source.relative_to(run_dir) + if self._copy_file_if_newer(source, target): + copied += 1 + else: + skipped += 1 + + marker = { + "schema": "insar.gamma-sbas-product-package/v1", + "run_id": run_dir.name, + "source_run_dir": str(run_dir), + "product_run_dir": str(product_dir), + "copied_files": copied, + "skipped_files": skipped, + "synced_at": datetime.utcnow().isoformat(timespec="seconds") + "Z", + } + self._write_json(product_dir / "product_package_manifest.json", marker) + return marker + @staticmethod def _write_json(path: Path, payload: dict[str, Any]) -> Path: path.parent.mkdir(parents=True, exist_ok=True) diff --git a/backend/app/services/wsl_runtime_registry.py b/backend/app/services/wsl_runtime_registry.py index 4fbca40..4be0574 100644 --- a/backend/app/services/wsl_runtime_registry.py +++ b/backend/app/services/wsl_runtime_registry.py @@ -24,6 +24,14 @@ def _project_file_windows(*relative_parts: str) -> str: return os.path.normpath(str(Path(settings.PROJECT_ROOT, *relative_parts))) +def _default_runtime_root_windows() -> str: + root = os.path.normpath(str(settings.PROJECT_ROOT)) + drive, _tail = os.path.splitdrive(root) + if drive: + return os.path.join(drive + os.sep, "production_runtime") + return os.path.join(root, "runtime") + + @dataclass(frozen=True) class WslRuntimeDefinition: runtime_id: str @@ -106,7 +114,7 @@ def build_wsl_runtime_registry() -> WslRuntimeRegistry: ).strip() broker_job_root_windows = os.path.normpath( settings.WSL_BROKER_JOB_ROOT - or os.path.join(settings.BACKEND_DIR, "runtime", "wsl_jobs") + or os.path.join(_default_runtime_root_windows(), "wsl_jobs") ) broker_job_root_wsl = _windows_path_to_wsl_mount(broker_job_root_windows) diff --git a/docs/BASEMAP_TILESERVER_PROXY_AND_ACCESS_20260613.md b/docs/BASEMAP_TILESERVER_PROXY_AND_ACCESS_20260613.md new file mode 100644 index 0000000..18379c3 --- /dev/null +++ b/docs/BASEMAP_TILESERVER_PROXY_AND_ACCESS_20260613.md @@ -0,0 +1,152 @@ +# Basemap Tile Server Proxy and LAN Access + +Date: 2026-06-13 + +This document records the local tile-server integration used by the InSAR Management System v2 Windows launcher. + +## Goals + +- Start and stop `D:\Code\tile-server` from the main `start_system.bat` / `stop_system.bat` workflow. +- Serve basemap tiles through the main Nginx entry instead of exposing tile-server directly to browsers. +- Keep tile access token configuration in local `.env`; do not commit real tokens. +- Allow optional LAN client IP whitelisting. +- Show a clear restricted-access page for non-whitelisted clients. + +## Runtime Topology + +```text +LAN browser + -> http:/// + -> main nginx :80 + -> frontend dist + -> /api/ -> FastAPI backend on 127.0.0.1: + -> /tiles/ -> tile-server on 127.0.0.1:8910 + -> /geojson/ -> tile-server on 127.0.0.1:8910 +``` + +The tile-server can stay bound to `127.0.0.1:8910`. LAN clients should not call `8910` directly. + +## `.env` Settings + +Tile-server frontend URL should normally be empty, which means same-origin routing through Nginx: + +```env +VITE_TILE_SERVER_URL= +VITE_TILE_SERVER_TOKEN=change_me +TILE_SERVER_AUTO_START=true +TILE_SERVER_AUTO_STOP=true +TILE_SERVER_ROOT=D:\Code\tile-server +TILE_SERVER_START_SCRIPT=start-all.bat +TILE_SERVER_STOP_SCRIPT=stop-all.bat +``` + +The real `VITE_TILE_SERVER_TOKEN` belongs only in local `.env`. + +## LAN IP Whitelist + +Use `NGINX_ALLOWED_CLIENT_IPS` to restrict clients: + +```env +NGINX_ALLOWED_CLIENT_IPS=192.168.1.10;192.168.1.23 +``` + +Accepted separators are semicolon, comma, or whitespace. CIDR values are also accepted: + +```env +NGINX_ALLOWED_CLIENT_IPS=192.168.1.0/28 +``` + +Empty value means no client IP restriction: + +```env +NGINX_ALLOWED_CLIENT_IPS= +``` + +When the whitelist is enabled, the launcher generates `nginx/client_allow.conf` with `allow` rules plus `deny all`. + +## Downstream Router Cases + +When client devices are behind another router, write the IP that the InSAR server actually sees. + +### NAT router mode + +If a downstream router has an upstream LAN address such as `192.168.1.63`, and its clients are hidden behind NAT, Nginx will see every downstream client as: + +```text +192.168.1.63 +``` + +Whitelist the router IP: + +```env +NGINX_ALLOWED_CLIENT_IPS=192.168.1.63 +``` + +This allows all clients behind that NAT router because the server cannot distinguish them by their private downstream IPs. + +### AP or bridge mode + +If the downstream device works as an AP/bridge, clients usually receive addresses from the main LAN, for example: + +```text +192.168.1.80 +192.168.1.81 +``` + +Whitelist the actual device IPs: + +```env +NGINX_ALLOWED_CLIENT_IPS=192.168.1.80;192.168.1.81 +``` + +### Routed subnet without NAT + +If the downstream router forwards another subnet without NAT, the server may see the real downstream subnet addresses, for example: + +```text +192.168.63.10 +192.168.63.11 +``` + +Whitelist exact client IPs: + +```env +NGINX_ALLOWED_CLIENT_IPS=192.168.63.10;192.168.63.11 +``` + +Or whitelist the whole downstream subnet when that is acceptable: + +```env +NGINX_ALLOWED_CLIENT_IPS=192.168.63.0/24 +``` + +### How to decide + +Open the system from the downstream device before whitelisting it. The restricted-access page displays the detected client IP. Put that displayed IP, or its trusted CIDR range, into `NGINX_ALLOWED_CLIENT_IPS`. + +## Restricted Client Page + +Nginx maps `403` responses to `nginx/access_denied.html`. The page displays: + +- access restricted message +- detected client IP +- requested host +- operator hint to update `NGINX_ALLOWED_CLIENT_IPS` + +## Applying Changes + +After changing `.env`, run `start_system.bat` or reload Nginx: + +```powershell +C:\nginx-1.29.6\nginx.exe -t -c D:\Code\Insar_management_system_v2\nginx\nginx.conf +C:\nginx-1.29.6\nginx.exe -s reload -c D:\Code\Insar_management_system_v2\nginx\nginx.conf +``` + +From an allowed client: + +```text +http:/// +http:///tiles/gaode_image/7/107/42.webp?token= +``` + +From a denied client, Nginx should return the restricted-access page with HTTP 403. diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index d86a9e5..2d1c0b9 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -111,11 +111,37 @@ ORBIT_POOL_LANDSAR= IDL_EXECUTABLE=C:\Program Files\Harris\ENVI56\IDL88\bin\bin.x86_64\idl.exe IDL_WORKBENCH_PATH=C:\Program Files\Harris\ENVI56\IDL88\bin\bin.x86_64\idlde.exe IDL_DINSAR_DEM_BASE_FILE=D:\SRTM30m\SRTMDEM_RSP_SARscape -IDL_WORKER_RUNTIME_DIR=D:\Code\Insar_management_system_v2\backend\runtime\idl_worker +IDL_WORKER_RUNTIME_DIR=D:\production_runtime\idl_worker ENVI_TASK_TIMEOUT_SECONDS=21600 ``` -### 3.5 WSL 共享运行时 +### 3.5 GF3 SARscape 生产 + +GF3 当前主线是“ENVI/SARscape 生产原生 `_geo` 证据层,平台标准化成 GeoTIFF 后入库和供洪涝/水体算法消费”。旧 Python/GDAL L1A 预处理链路默认关闭。 + +```env +GF3_ARCHIVE_SOURCE_DIRS=D:\production_inputs\gf3\archives +GF3_LEGACY_GDAL_ENABLED=false +GF3_SOURCE_DIRS= +GF3_SARSCAPE_NATIVE_DIRS=D:\production_results\gf3\sarscape_native +GF3_STORAGE_DIRS=D:\production_results\gf3\standard_l2 +GF3_SARSCAPE_RUNTIME_DIR=D:\production_runtime\gf3\sarscape_runtime +GF3_SARSCAPE_WRAPPER_EXE=D:\Code\Insar_management_system_v2\third_party\GF3_L1A_To_L2_pipeline\dist\windows\gf3wrapper.exe +GF3_SARSCAPE_IDLRT_PATH=C:\Program Files\Harris\ENVI56\IDL88\bin\bin.x86_64\idlrt.exe +GF3_SARSCAPE_DEM_PATH=D:\DEM\COPDEM_GLO30_China_4326_DEM +GF3_SARSCAPE_POLARIZATIONS=HH,HV +GF3_SARSCAPE_AUTO_STANDARDIZE=true +GF3_SARSCAPE_CLEAN_AFTER_SUCCESS=true +``` + +说明: + +- `GF3_SARSCAPE_NATIVE_DIRS` 长期保留 `_geo`、`.hdr`、`.sml`、快视、KML、日志和 manifest。 +- `GF3_STORAGE_DIRS` 是标准 L2 GeoTIFF 池,后续入库、预览、洪涝/水体分析优先消费这里和 `SAR_ANALYSIS_READY_ROOT`。 +- `GF3_SARSCAPE_RUNTIME_DIR` 只放 wrapper 配置和运行时临时文件,不应混入原生结果池。 +- 如确需恢复旧 L1A 解包/预处理,需要同时设置 `GF3_LEGACY_GDAL_ENABLED=true` 和 `GF3_SOURCE_DIRS`。 + +### 3.6 WSL 共享运行时 当前推荐采用“一套共享 distro + 一套共享 conda 环境”模式。 @@ -123,7 +149,7 @@ ENVI_TASK_TIMEOUT_SECONDS=21600 WSL_DISTRO=Ubuntu-24.04 WSL_SHARED_CONDA_ENV=insar_wsl_v1 WSL_SHARED_PYTHON=/home/administrator/miniconda3/envs/insar_wsl_v1/bin/python -WSL_BROKER_JOB_ROOT=D:\Code\Insar_management_system_v2\backend\runtime\wsl_jobs +WSL_BROKER_JOB_ROOT=D:\production_runtime\wsl_jobs ISCE2_RUNTIME_ID=isce2_runtime_v1 PYINT_RUNTIME_ID=gamma_pyint_runtime_v1 @@ -138,7 +164,7 @@ PYINT_RUNTIME_ID=gamma_pyint_runtime_v1 使用共享 python,runner 为 `deploy/wsl/runners/gamma_pyint_runner.py` Gamma 固定环境脚本为 `deploy/wsl/profiles/gamma_env.sh` -### 3.6 ISCE2 D-InSAR 与旧时序兼容 +### 3.7 ISCE2 D-InSAR 与旧时序兼容 ```env ISCE2_ENABLED=false @@ -156,7 +182,7 @@ TIMESERIES_PYTHON=/home/administrator/miniconda3/envs/insar_wsl_v1/bin/python - 旧 ISCE2/MintPy 时序生产链默认关闭,不再作为 SBAS 生产入口。 - 如果必须做历史链路对比,需要显式开启 `TIMESERIES_ENABLED=true` 并提供完整旧脚本路径。 -### 3.7 Gamma / PyINT / SBAS +### 3.8 Gamma / PyINT / SBAS ```env PYINT_ENABLED=true diff --git a/docs/DINSAR_TASK_POOL_THREE_ENGINE_REFACTOR_20260614.md b/docs/DINSAR_TASK_POOL_THREE_ENGINE_REFACTOR_20260614.md new file mode 100644 index 0000000..c352fb8 --- /dev/null +++ b/docs/DINSAR_TASK_POOL_THREE_ENGINE_REFACTOR_20260614.md @@ -0,0 +1,212 @@ +# D-InSAR 三引擎 Task_Pool 生产链路重构设计 + +更新日期:2026-06-14 + +## 1. 结论 + +D-InSAR 后续生产只保留三类核心引擎: + +- `sarscape`:ENVI + SARscape。 +- `landsar`:LandSAR D-InSAR。 +- `pyint`:Gamma / PyINT。 + +`isce2` 从 D-InSAR 正式生产链路退出。后端运行入口、引擎注册、任务类型、结果发布特殊分支、前端引擎选择和结果筛选都需要清理。已有 ISCE2 历史结果不在本次设计中直接删除,默认进入 legacy 只读语义:不参与新任务分发、不在默认结果管理视图中展示,只在显式历史查询或迁移脚本中处理。 + +三条保留链路统一走 Task_Pool 任务格式。LandSAR 和 ENVI + SARscape 只处理陆探 LT-1 数据;Gamma / PyINT 同时支持 LT-1 和 Sentinel-1。 + +## 2. 当前代码事实 + +当前系统已经具备多引擎基础,但还没有完成 Task_Pool 一致化: + +- `backend/app/routers/dinsar_production.py` 暴露 D-InSAR 多引擎生产接口,当前仍接受 `sarscape / isce2 / pyint / landsar`。 +- `backend/app/dinsar_engines/registry.py` 仍注册 `Isce2Engine`。 +- `backend/app/services/dinsar_production_service.py` 已经按 engine/profile 生成运行项、当前指针和未完成判断,但 `_task_type_for_engine`、`_workflow_name_for_engine`、`_workflow_step_name_for_engine` 仍包含 ISCE2。 +- `backend/app/services/job_handlers.py` 同时存在 SARscape、ISCE2、PyINT、LandSAR 的 job handler。 +- `backend/app/dinsar_engines/landsar_engine.py` 已经围绕 LT-1 `Task_*/Input_Data` 和 `master/slave` 原始目录做输入检查。 +- `backend/app/dinsar_engines/pyint_engine.py` 支持 `lt1_gamma_dinsar` 和 `s1_gamma_dinsar` profile,但未完成判断目前偏向 LT-1 profile,需要按 profile 收敛。 +- `backend/app/services/result_catalog_service.py` 的结果目录、manifest、current 指针和 `result_products` 表已经能承载多引擎多次运行,但展示层仍是“一个引擎结果一条记录”。 +- `backend/app/routers/task_batches.py` 的 D-InSAR batch/item 表保存了配对身份和任务状态,但没有保存或返回各引擎结果矩阵,也没有显式 Task_Pool 物理发布状态。 +- `frontend/src/components/DinsarCatalogPanel.jsx` 和 `frontend/src/utils/dinsarEngines.js` 仍把 ISCE2 当作当前引擎之一,结果管理也是平铺展示。 + +## 3. 目标 Task_Pool 契约 + +D-InSAR 正式生产入口统一接受 Task_Pool 任务目录,不再把任意平铺目录作为正式生产输入。兼容导入可以保留在迁移工具或单独入口,不能作为标准链路。 + +推荐根路径: + +```text +D:\Task_Pool\DInSAR\ + Task__[_suffix]\ + task_manifest.json + .dinsar_pair.json + master\ + ... + slave\ + ... + Input_Data\ + ... +``` + +目录语义: + +- `task_manifest.json`:Task_Pool 任务清单,记录任务来源、配对身份、数据类型、允许引擎、各引擎结果快照和分发状态。新实现应写入;旧任务可由 `.dinsar_pair.json` 和目录结构推断。 +- `.dinsar_pair.json`:现有配对侧车文件,继续作为 `pair_key`、`pair_uid`、`network_run_id`、`network_edge_id` 等身份信息的本地来源。 +- `master/`、`slave/`:标准主从影像资产目录。LT-1 放陆探原始包或解包后的 xml/tif;Sentinel-1 放 SAFE/ZIP 及可解析的轨道资产引用。 +- `Input_Data/`:LandSAR 运行所需的 LT-1 输入目录。可以由 `master/slave` 导入生成,但生成关系必须写入 manifest,避免后续误判源数据。 + +任务身份以 `pair_key` 为稳定主键,`network_edge_id` 为配对网络中的边身份,`run_key` 为一次具体生产实例。已有代码中的 `.dinsar_run.json` 继续用于单次运行元数据。 + +## 4. 引擎能力矩阵 + +| 引擎 | engine_code | 数据类型 | 输入目录 | 结果 profile | +| --- | --- | --- | --- | --- | +| ENVI + SARscape | `sarscape` | LT-1 | `Task_*/master` + `Task_*/slave` | `custom6`、`metatask` | +| LandSAR | `landsar` | LT-1 | `Task_*/Input_Data`,或从 `master/slave` 导入生成 | `lt1_dinsar` | +| Gamma / PyINT | `pyint` | LT-1 | `Task_*/master` + `Task_*/slave` | `lt1_gamma_dinsar` | +| Gamma / PyINT | `pyint` | Sentinel-1 | `Task_*/master` + `Task_*/slave`,配套轨道资产 | `s1_gamma_dinsar` | + +生产入口需要按数据类型和 profile 做硬约束: + +- LT-1 任务允许 `sarscape`、`landsar`、`pyint`。 +- Sentinel-1 任务只允许 `pyint`。 +- `sarscape` 和 `landsar` 对 Sentinel-1 返回明确不可分发状态,而不是进入运行后失败。 +- `pyint` 的未完成判断必须按 profile 区分,不能只检查 `lt1_gamma_dinsar`。 + +## 5. 配对发布与完成状态 + +当前 `/task-batches/dinsar` 只保存配对条目,不知道每个 engine 是否已经产出结果。后续需要在“发布到 Task_Pool”和“查看任务池条目”两个位置补齐结果矩阵。 + +建议在 D-InSAR task item 响应中增加 `engine_results`: + +```json +{ + "pair_key": "lt1_20250101_20250201_xxxxx", + "task_alias": "Task_20250101_20250201", + "task_pool_dir": "D:\\Task_Pool\\DInSAR\\Task_20250101_20250201", + "engine_results": { + "sarscape": { + "allowed": true, + "status": "missing", + "profile_code": "custom6", + "latest_product_id": null, + "can_dispatch": true, + "skip_reason": null + }, + "landsar": { + "allowed": true, + "status": "ready", + "profile_code": "lt1_dinsar", + "latest_product_id": 123, + "can_dispatch": false, + "skip_reason": "result_exists" + }, + "pyint": { + "allowed": true, + "status": "failed", + "profile_code": "lt1_gamma_dinsar", + "latest_product_id": 124, + "can_dispatch": true, + "skip_reason": "retry_allowed" + } + } +} +``` + +状态建议: + +- `missing`:没有对应引擎/profile 的有效产品。 +- `running`:已有未完成 job 或运行锁。 +- `ready`:已有有效产品,默认不重复分发。 +- `failed`:最近一次产品或 job 失败,可按策略允许重跑。 +- `blocked`:数据类型、轨道、DEM、许可证或输入结构不满足该引擎。 +- `legacy`:历史结果存在,但不参与当前三引擎分发规则。 + +结果矩阵优先从 `result_products` 和运行状态表实时聚合,不建议一开始复制到 batch item 表中。`task_manifest.json` 可以保存发布时快照,但界面展示应以数据库/catalog 聚合结果为准。 + +## 6. 结果管理重构 + +当前结果管理按 `result_products` 平铺展示,导致同一任务的三个引擎结果分成三条记录。目标视图应按任务/配对聚合: + +```text +Task_20250101_20250201 + pair_key: lt1_20250101_20250201_xxxxx + sarscape: ready / latest run / preview / assets + landsar: missing + pyint: ready / latest run / preview / assets +``` + +后端建议保留 `result_products` 作为原子产品表,新增聚合查询: + +- `GET /dinsar-products/pairs`:按 `pair_key`、`pair_uid`、`network_edge_id` 聚合结果。 +- `GET /dinsar-products/pairs/{pair_key}`:返回同一任务下三个引擎的运行历史、资产、预览和 current 指针。 +- `GET /dinsar-products/{product_db_id}`:继续保留单产品详情,作为资产钻取入口。 + +前端结果管理调整为“一行一个任务/配对”,内部显示三个引擎状态 chip。引擎筛选不再把结果拆成多行,而是控制哪些引擎列/状态参与显示。ISCE2 默认不显示,只能通过 legacy 显式开关查看历史记录。 + +## 7. 中间文件删除模块 + +删除能力必须建立在“结果已注册成资产”之后,不能直接按目录名粗暴删除。建议新增 D-InSAR intermediate cleanup 服务,职责如下: + +- 扫描某个 `pair_key`、`run_key` 或 `product_db_id` 下可清理路径。 +- 校验 `manifest.json`、`execution_manifest.json`、`result_assets` 或 manifest assets 中的必要资产都存在。 +- 只删除或隔离中间目录,例如 `native/`、engine work dir、临时转换目录、LandSAR 导入过程缓存、PyINT/Gamma 临时处理目录。 +- 永远保留标准发布资产:`assets/`、`preview/`、`manifest.json`、`execution_manifest.json`、`.dinsar_run.json`、current 指针、Task_Pool 任务 manifest 和 `.dinsar_pair.json`。 +- 支持 `dry_run`,返回将删除的路径、大小和风险原因。 +- 第一阶段建议移动到 quarantine/trash,再由运维策略做永久删除。 +- 删除动作写审计日志,并把清理状态写回产品摘要或独立 cleanup 表。 + +建议接口: + +- `POST /dinsar-products/{product_db_id}/cleanup-intermediates?dry_run=true` +- `POST /dinsar-products/pairs/{pair_key}/cleanup-intermediates?dry_run=true` +- `GET /dinsar-products/{product_db_id}/cleanup-intermediates/plan` + +## 8. 后端改造清单 + +第一阶段只做读模型和状态聚合,不改变生产执行: + +- 新增三引擎常量和能力矩阵,作为后续后端和前端共同事实来源。 +- 新增 D-InSAR task item engine result 聚合函数。 +- 新增 result catalog pair-grouped 查询。 +- 修正 PyINT 按 profile 判断完成状态。 + +第二阶段收紧 Task_Pool 输入: + +- D-InSAR 运行入口只接受 Task_Pool 任务目录或任务目录集合。 +- Pairing 发布 batch 时生成或补齐 `task_manifest.json` 与 `.dinsar_pair.json`。 +- 对 LT-1 / Sentinel-1 做引擎可分发校验。 +- LandSAR 的 `Input_Data` 生成关系写入 manifest。 + +第三阶段移除 ISCE2 正式链路: + +- 删除 `Isce2Engine` 注册。 +- 移除 D-InSAR submit API 中的 `isce2` 分支。 +- 移除 ISCE2 job type 到新任务分发的映射。 +- 移除 result catalog 新发布中的 ISCE2 特殊处理。 +- 前端移除 ISCE2 引擎选项、筛选项和当前说明文案。 +- 历史 ISCE2 产品标记为 legacy,只在显式历史入口展示。 + +第四阶段上线中间文件清理: + +- 先提供 dry-run 和 quarantine。 +- 加入资产完整性校验。 +- 加入审计记录和恢复说明。 +- 稳定后再允许永久删除。 + +## 9. 前端改造清单 + +- D-InSAR 生产面板只展示 `sarscape`、`landsar`、`pyint`。 +- 数据类型为 Sentinel-1 时,只允许选择 Gamma / PyINT;LT-1 时允许三引擎。 +- Batch/Task_Pool 列表展示每个任务的三引擎结果状态。 +- 已有结果的任务默认显示“已有结果”,并允许按策略重跑或跳过。 +- 结果管理从产品平铺改成任务聚合视图。 +- 单产品详情保留,用于下载、预览和资产级操作。 +- 中间文件清理入口放在产品详情或任务聚合详情内,默认先显示 dry-run 计划。 + +## 10. 风险与待定项 + +- 现有 ISCE2 历史结果是否需要迁移到 legacy catalog,还是只隐藏默认入口后保留原记录,需在实际清理前确认。 +- ENVI + SARscape 的 `metatask` 是否继续作为可选 profile,还是统一收敛到 `custom6`,需要结合现有项目参数模板决定。 +- Task_Pool 根路径需要明确配置项,建议新增 `DINSAR_TASK_POOL_ROOT`,默认 `D:\Task_Pool\DInSAR`。 +- 中间文件清理需要先确认每个引擎的“可删目录”和“必须保留资产”清单,不能用一套规则覆盖全部。 +- 前端 grouped 结果视图需要兼容旧 flat API 一段时间,避免已有页面一次性断裂。 diff --git a/docs/FLOOD_GEOTIFF_GAMMA_PREPROCESS_DESIGN_20260515.md b/docs/FLOOD_GEOTIFF_GAMMA_PREPROCESS_DESIGN_20260515.md index 815ad76..1a4e320 100644 --- a/docs/FLOOD_GEOTIFF_GAMMA_PREPROCESS_DESIGN_20260515.md +++ b/docs/FLOOD_GEOTIFF_GAMMA_PREPROCESS_DESIGN_20260515.md @@ -503,7 +503,7 @@ Git 可以提供平台代码、Python 算法、任务编排、数据库迁移和 The first code pass now implements LT1 and GF3 as analysis-ready scene preprocessors: -- New config roots: `SAR_ANALYSIS_READY_ROOT`, `SAR_ANALYSIS_WORK_ROOT`, `SAR_ANALYSIS_PREVIEW_ROOT`. +- Current config roots: `SAR_ANALYSIS_READY_ROOT` and `SAR_ANALYSIS_WORK_ROOT`. `SAR_ANALYSIS_PREVIEW_ROOT` was removed; previews live beside each analysis-ready scene. - `sar_scene_geo` now stores `analysis_tif_path`, `analysis_dir`, preview path, engine/profile, backscatter unit, nodata, metadata and quality JSON. - `/flood/preprocess` routes GF3 scenes to `gf3_gdal` standardization and LT1 scenes to `lt_gamma` single-scene preprocessing. - GF3 uses the existing GDAL/RPC L1A->L2 result, then registers the selected L2 GeoTIFF under `SAR_ANALYSIS_READY_ROOT`. diff --git a/docs/FLOOD_WATER_ALGORITHM_ENGINEERING_HANDOFF_20260602.md b/docs/FLOOD_WATER_ALGORITHM_ENGINEERING_HANDOFF_20260602.md index f377939..bc5cbcc 100644 --- a/docs/FLOOD_WATER_ALGORITHM_ENGINEERING_HANDOFF_20260602.md +++ b/docs/FLOOD_WATER_ALGORITHM_ENGINEERING_HANDOFF_20260602.md @@ -54,7 +54,7 @@ | `pixel_size_m` | 近似像元大小 | | `status` | `PENDING/RUNNING/DONE/FAILED` | -当前 GF3 SARscape 链路会把原生 `_geo` ENVI 二进制转换为 `D:\GF3_L2_Image_Pool` 下的 GeoTIFF,并注册到这里。 +当前 GF3 SARscape 链路会先产出原生 `_geo` ENVI 二进制,再由平台转换为 `D:\production_results\gf3\standard_l2` 下的 GeoTIFF,并注册到这里。 ### 3.2 WaterExtractionORM @@ -508,4 +508,3 @@ options: dict | None = None - 支持模型版本; - 输出概率图和二值图; - 与 RF/baseline 可切换。 - diff --git a/docs/GF3_SARSCAPE_NATIVE_TO_GEOTIFF_DESIGN_20260530.md b/docs/GF3_SARSCAPE_NATIVE_TO_GEOTIFF_DESIGN_20260530.md index 720912d..d582159 100644 --- a/docs/GF3_SARSCAPE_NATIVE_TO_GEOTIFF_DESIGN_20260530.md +++ b/docs/GF3_SARSCAPE_NATIVE_TO_GEOTIFF_DESIGN_20260530.md @@ -19,12 +19,15 @@ GF3 原始压缩包池 ## 2. 目录约定 -推荐继续沿用现场已有目录语义,并新增一个 ENVI/SARscape 原生池。 +推荐把输入、生产结果和运行时目录分开,避免把系统工作文件混入业务结果池。 ```env -GF3_ARCHIVE_SOURCE_DIRS=D:\GF3_Image_Pool_Zip -GF3_SARSCAPE_NATIVE_DIRS=D:\GF3_L2_ENVI_Binary_Pool -GF3_STORAGE_DIRS=D:\GF3_L2_Image_Pool +GF3_ARCHIVE_SOURCE_DIRS=D:\production_inputs\gf3\archives +GF3_LEGACY_GDAL_ENABLED=false +GF3_SOURCE_DIRS= +GF3_SARSCAPE_NATIVE_DIRS=D:\production_results\gf3\sarscape_native +GF3_STORAGE_DIRS=D:\production_results\gf3\standard_l2 +GF3_SARSCAPE_RUNTIME_DIR=D:\production_runtime\gf3\sarscape_runtime SAR_ANALYSIS_READY_ROOT=D:\production_results\sar_analysis_ready ``` @@ -33,8 +36,10 @@ SAR_ANALYSIS_READY_ROOT=D:\production_results\sar_analysis_ready | 目录 | 职责 | 系统是否直接分析 | | --- | --- | --- | | `GF3_ARCHIVE_SOURCE_DIRS` | 原始 GF3 L1A `.tar.gz` 池 | 否 | +| `GF3_SOURCE_DIRS` | legacy Python/GDAL L1A 解包输入,默认关闭 | 否 | | `GF3_SARSCAPE_NATIVE_DIRS` | SARscape `_geo` 原生结果池 | 否 | | `GF3_STORAGE_DIRS` | GF3 标准 GeoTIFF 池 | 是 | +| `GF3_SARSCAPE_RUNTIME_DIR` | wrapper 配置、IDL 运行时临时文件 | 否 | | `SAR_ANALYSIS_READY_ROOT` | 洪涝/水体分析级统一输入 | 是 | 生产服务器可以不部署完整管理系统。只要把完成后的 `_geo` 原生结果组放入 `GF3_SARSCAPE_NATIVE_DIRS`,管理系统就可以扫描、转换和入库。 @@ -44,7 +49,7 @@ SAR_ANALYSIS_READY_ROOT=D:\production_results\sar_analysis_ready 原生池以批次日期或人工批次号分组。单景目录名尽量保持 GF3 原始产品名。 ```text -D:\GF3_L2_ENVI_Binary_Pool +D:\production_results\gf3\sarscape_native 20260514 GF3_MH1_FSII_051377_E132.3_N48.2_20260514_L1A_HHHV_L10007356478 GF3_MH1_FSII_..._hh_geo @@ -110,7 +115,7 @@ SLC 中间产物 系统从原生池转换后写入 `GF3_STORAGE_DIRS`。 ```text -D:\GF3_L2_Image_Pool +D:\production_results\gf3\standard_l2 20260514 GF3_MH1_FSII_051377_E132.3_N48.2_20260514_L1A_HHHV_L10007356478 HH_L2.tif @@ -269,8 +274,8 @@ status=DONE { "schema": "gf3_sarscape_native.v1", "scene_name": "GF3_MH1_FSII_...", - "native_dir": "D:\\GF3_L2_ENVI_Binary_Pool\\20260514\\GF3_MH1_FSII_...", - "source_archive": "D:\\GF3_Image_Pool_Zip\\20260514\\GF3_MH1_FSII_....tar.gz", + "native_dir": "D:\\production_results\\gf3\\sarscape_native\\20260514\\GF3_MH1_FSII_...", + "source_archive": "D:\\production_inputs\\gf3\\archives\\20260514\\GF3_MH1_FSII_....tar.gz", "polarizations": ["HH", "HV"], "status": "NATIVE_READY", "assets": [ @@ -295,8 +300,8 @@ status=DONE { "schema": "gf3_standard_geotiff.v1", "scene_name": "GF3_MH1_FSII_...", - "native_manifest": "D:\\GF3_L2_ENVI_Binary_Pool\\...\\gf3_native_manifest.json", - "standard_dir": "D:\\GF3_L2_Image_Pool\\20260514\\GF3_MH1_FSII_...", + "native_manifest": "D:\\production_results\\gf3\\sarscape_native\\...\\gf3_native_manifest.json", + "standard_dir": "D:\\production_results\\gf3\\standard_l2\\20260514\\GF3_MH1_FSII_...", "status": "DONE", "converter": { "name": "gf3_sarscape_geo_to_tif", @@ -446,7 +451,7 @@ GF3_ARCHIVE_SOURCE_DIRS 新增配置: ```env -GF3_SARSCAPE_WRAPPER_EXE=D:\Code\Insar_management_system_v2\.codex_tmp\GF3_L1A_To_L2_pipeline\dist\windows\gf3wrapper.exe +GF3_SARSCAPE_WRAPPER_EXE=D:\Code\Insar_management_system_v2\third_party\GF3_L1A_To_L2_pipeline\dist\windows\gf3wrapper.exe GF3_SARSCAPE_IDLRT_PATH=C:\Program Files\Harris\ENVI56\IDL88\bin\bin.x86_64\idlrt.exe GF3_SARSCAPE_DEM_PATH=D:\DEM\GMTED2010.jp2 GF3_SARSCAPE_POLARIZATIONS=HH,HV @@ -473,4 +478,4 @@ GF3_SARSCAPE_PRODUCE_TIMEOUT_SECONDS=0 - 每景写 `gf3_cleanup_manifest.json`,记录删除条目和释放字节数。 - 不删除 `GF3_ARCHIVE_SOURCE_DIRS` 中的原始压缩包,也不删除 `GF3_STORAGE_DIRS` 中的标准 GeoTIFF。 -这样 `D:\GF3_L2_ENVI_Binary_Pool` 只长期保存可追溯的最终 `_geo` 原生结果组,中间过程文件在标准化完成后自动释放空间。 +这样 `D:\production_results\gf3\sarscape_native` 只长期保存可追溯的最终 `_geo` 原生结果组,中间过程文件在标准化完成后自动释放空间;wrapper 配置和临时运行文件放在 `D:\production_runtime\gf3\sarscape_runtime`。 diff --git a/docs/INDEX.md b/docs/INDEX.md index 624b9c8..4e13a26 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -1,6 +1,6 @@ # 文档索引 -最后更新:2026-05-30 +最后更新:2026-06-14 本页是当前有效文档入口。没有列在本页的历史设计、实验记录和过程文档不再作为当前系统事实依据。 @@ -12,6 +12,8 @@ - [DEPLOYMENT.md](DEPLOYMENT.md) Windows + PostgreSQL + WSL2 + Gamma/ISCE2/ENVI 的部署与运行说明。 +- [BASEMAP_TILESERVER_PROXY_AND_ACCESS_20260613.md](BASEMAP_TILESERVER_PROXY_AND_ACCESS_20260613.md) + Tile-server proxy, LAN access, token configuration, and Nginx IP whitelist. - [DOCUMENTATION_GOVERNANCE.md](DOCUMENTATION_GOVERNANCE.md) 文档治理规则、事实来源优先级和清理约定。 @@ -24,10 +26,13 @@ ## 生产与结果 - [PRODUCTION_RESULTS_MULTI_ENGINE_DESIGN_20260423.md](PRODUCTION_RESULTS_MULTI_ENGINE_DESIGN_20260423.md) - 统一结果目录、标准产品包、catalog 与多引擎结果共存约定。 + 统一结果目录、标准产品包、catalog 与多引擎结果共存约定。D-InSAR 当前引擎集合以 2026-06-14 三引擎 Task_Pool 重构设计为准。 + +- [DINSAR_TASK_POOL_THREE_ENGINE_REFACTOR_20260614.md](DINSAR_TASK_POOL_THREE_ENGINE_REFACTOR_20260614.md) + D-InSAR 保留 ENVI/SARscape、LandSAR、Gamma/PyINT 三引擎,退出 ISCE2,统一 Task_Pool、结果聚合和中间文件清理的当前设计。 - [DINSAR_PRODUCTION_CORES_OVERVIEW.md](DINSAR_PRODUCTION_CORES_OVERVIEW.md) - ENVI/SARscape、ISCE2、Gamma/PyINT 三条 D-InSAR 生产核心说明。 + 旧版 ENVI/SARscape、ISCE2、Gamma/PyINT D-InSAR 生产核心说明。ISCE2 相关内容仅作历史背景。 - [SBAS_INSAR_CURRENT_WORKFLOW.md](SBAS_INSAR_CURRENT_WORKFLOW.md) 当前 Gamma SBAS-InSAR 生产、AOI 选栈、结果 catalog、产物和 LOS 符号约定。 @@ -41,10 +46,10 @@ PROJ / GDAL 配置说明。 - [ISCE2_MANAGED_DINSAR_IMPLEMENTATION_20260424.md](ISCE2_MANAGED_DINSAR_IMPLEMENTATION_20260424.md) - ISCE2 托管 D-InSAR 落地说明。 + ISCE2 托管 D-InSAR 历史落地说明。新 D-InSAR 生产不再采用。 - [ISCE2_PRODUCTION_RELIABILITY_HARDENING_DESIGN_20260424.md](ISCE2_PRODUCTION_RELIABILITY_HARDENING_DESIGN_20260424.md) - ISCE2 生产链路稳定性约束。 + ISCE2 生产链路历史稳定性约束。新 D-InSAR 生产不再采用。 ## 数据与业务模块 diff --git a/docs/LandSAR_API服务接入采购需求说明书_20260604.docx b/docs/LandSAR_API服务接入采购需求说明书_20260604.docx new file mode 100644 index 0000000000000000000000000000000000000000..91a58379ee0e17dcfa9fddfa284aa51195f88d01 GIT binary patch literal 68383 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,xEuSJd(jIU=#r%OYhUl3*D{;tprpk zf}%~#!rGix@sv(SBlOvmo@w$mdm@8=WDx0FRv_d|Rpi%GkD_(bJpp!wc#u5LP*Z7J zgUYTp*rHv0!Wms4p*+3du7Kf*k-m#9?2f2VAsYMuY;HD~3Y@@m{8AZivh#1=^=<;q zv3YY)NyNT2Wv4o`z4!Rq2o5w@0(Hk=&$se4YdY^+hgr;LQY7s&55X%s6!^dD8iPsm ztU+BNI~-!3bpw)wJ$y?($^JZ>P?O*;M->(r+7Xz`!injnED9wA-!Uh|jc^#pxpHJg(XmkxOg%=NG;tFXz3NPDMU^i% zKyo#K737s5C@(-vn-?H@gp2B1~1eGLvf50)u)lue|8g3k}y9U9x8$?O3_Jh~gkCupfCMpfo5A%Bu#S zH+==WU&+Zvg`%p3d-pv{JZL+_o>%++JQ3)KK{sB~SQ$A1rd#-FNQK?I2Pmde*Vv#c zF{#=?Rx+Hpf=@!|us~lx_|B$>QS!jFRWj+iJEv#vlkZxNfC(IQ`CidcRWszJJk{h` z4v{og1yR>v;lhK{uW{RIQ4>rGeR-eX#T_Z1hICXlO)%Yn6A(;2ugZdj;`BRH$kCpT zJJ=aC*G+O=uu{Eom5 z-IN%m4!f7xBZ8PqQhf69mwmnrfwk^!VvOGxeaFe*K0XD5P|+_Y$kGE#Nbk_X^U8~C zdXPH|`?~?`?Km7GlQ4^7dsP!Pty+36af*fv)@y=k0KO49KK;Z-vdL1OQfiIk{kT$@ zS2C@SPb^*cf#@y&3HV#{xUJyzq)g7fg7mGH6Bu474g+WN>TLFTn$Hkrt#mxktUnDl zVe^uF^VR_Wdcf(_3-K$BVE=T0!Jg)sCcEe#IhKpzmvF!;aEq)`%X+>)*v93Z>X3KL zDQFyuY>H&$Zu0^AeRnNjZ5vUjep!;X>xAeG|Ro&QupEzDu28 zmEQA^g!ZH0f#MH%<;+HT2k@()JtxH0oDlC=$lpX(48T*7xkQx_Rb6MF^%2I@^x3^( z?AU&PUzD++{Z#cQ7Hr>$O2T(R8$VGBSpj>f0#v|#o1az%e)!WC8s&*deX=_K{<~L? zwp^n*wjMs;b95_-Q;vakzj^op0tOcJRJh+d35^ub8IhlWZ zA(l+Wn)C0QA{PVSqGf7BB>rCc;eAJF?8R6NrCR*Ams*qR?LL|=boHoS+7IaUg;>M2 z_tmS0Sbb}AB$|A``ls*ww;z`Mu&nG4%F6!fg6PUQTau4({2Fc|9<#i{tScZ?;q|M|9|jFT|Cm12vyR*y9X|~|N1BY_WgIt n= 2026-06-14 状态说明:本文保留结果包、catalog、manifest、current 指针等通用目录约定;D-InSAR 当前引擎集合已调整为 ENVI/SARscape、LandSAR、Gamma/PyINT 三类,ISCE2 退出正式生产链路。D-InSAR 引擎、Task_Pool、结果聚合和中间文件清理以 `DINSAR_TASK_POOL_THREE_ENGINE_REFACTOR_20260614.md` 为准。 + ## 1. 目标 本设计解决三个长期问题: diff --git a/docs/SBAS_INSAR_CURRENT_WORKFLOW.md b/docs/SBAS_INSAR_CURRENT_WORKFLOW.md index 4c96ea7..6608cc3 100644 --- a/docs/SBAS_INSAR_CURRENT_WORKFLOW.md +++ b/docs/SBAS_INSAR_CURRENT_WORKFLOW.md @@ -66,12 +66,13 @@ GAMMA_SBAS_DEFAULT_MB_MODE GAMMA_SBAS_DEFAULT_REFERENCE_WINDOW GAMMA_SBAS_STEP_TIMEOUT_SECONDS GAMMA_SBAS_WORKFLOW_TIMEOUT_SECONDS +GAMMA_SBAS_TRIAL_ROOT ``` 默认工作根: ```text -backend/runtime/sbas_insar_production +D:\production_runtime\sbas_insar_work ``` 默认产品根: @@ -80,6 +81,11 @@ backend/runtime/sbas_insar_production D:\production_results\timeseries\sbas ``` +`GAMMA_SBAS_WORK_ROOT` stores heavy, restartable Gamma intermediate data. +`GAMMA_SBAS_PRODUCT_ROOT` stores lightweight catalog/download packages under +`runs/`. The result catalog indexes the product root first and keeps the +work root only as a compatibility fallback. + ## 4. 生产流程 用户侧推荐顺序: diff --git a/frontend/index.html b/frontend/index.html index 6053408..c0843f2 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,7 +2,7 @@ - + frontend diff --git a/frontend/src/DataMonitorPanel.jsx b/frontend/src/DataMonitorPanel.jsx index 3f29e3d..279daf2 100644 --- a/frontend/src/DataMonitorPanel.jsx +++ b/frontend/src/DataMonitorPanel.jsx @@ -9,8 +9,10 @@ const DEFAULT_MONITOR_CONFIG = { dinsar_dirs: [], gf3_archive_source_dirs: [], gf3_source_dirs: [], + gf3_legacy_gdal_enabled: false, gf3_sarscape_native_dirs: [], gf3_storage_dirs: [], + gf3_sarscape_runtime_dir: '', gf3_sarscape_wrapper_exe: '', gf3_sarscape_idlrt_path: '', gf3_sarscape_dem_path: '', @@ -302,6 +304,7 @@ const DataMonitorPanel = ({ apiEndpoint, onTaskStart, readOnly = false, enabled const hasGf3SourceDirs = config.gf3_source_dirs.length > 0; const hasGf3SarscapeNativeDirs = config.gf3_sarscape_native_dirs.length > 0; const hasGf3StorageDirs = config.gf3_storage_dirs.length > 0; + const gf3LegacyGdalEnabled = Boolean(config.gf3_legacy_gdal_enabled); const hasGf3SarscapeWrapper = typeof config.gf3_sarscape_wrapper_exe === 'string' && config.gf3_sarscape_wrapper_exe.trim() !== ''; const hasGf3SarscapeDem = typeof config.gf3_sarscape_dem_path === 'string' && config.gf3_sarscape_dem_path.trim() !== ''; @@ -311,8 +314,8 @@ const DataMonitorPanel = ({ apiEndpoint, onTaskStart, readOnly = false, enabled const canRunS1Scan = !readOnly && configLoaded && hasS1SourceDirs; const canRunS1OrbitScan = !readOnly && configLoaded && hasS1OrbitDirs; const canRunGf3Scan = !readOnly && configLoaded && hasGf3StorageDirs; - const canRunGf3Unpack = !readOnly && configLoaded && hasGf3ArchiveSourceDirs && hasGf3SourceDirs; - const canRunGf3Process = !readOnly && configLoaded && hasGf3SourceDirs; + const canRunGf3Unpack = !readOnly && configLoaded && gf3LegacyGdalEnabled && hasGf3ArchiveSourceDirs && hasGf3SourceDirs; + const canRunGf3Process = !readOnly && configLoaded && gf3LegacyGdalEnabled && hasGf3SourceDirs; const canRunGf3SarscapeProduce = !readOnly && configLoaded && hasGf3ArchiveSourceDirs && hasGf3SarscapeNativeDirs && hasGf3StorageDirs && hasGf3SarscapeWrapper && hasGf3SarscapeDem; const canRunGf3SarscapeSync = !readOnly && configLoaded && hasGf3SarscapeNativeDirs && hasGf3StorageDirs; const canRunGf3SarscapeClean = !readOnly && configLoaded && hasGf3SarscapeNativeDirs && hasGf3StorageDirs; @@ -797,9 +800,10 @@ const DataMonitorPanel = ({ apiEndpoint, onTaskStart, readOnly = false, enabled
S1 存储{formatList(config.s1_storage_dirs)}
S1 精轨{formatList(config.s1_orbit_dirs)}
GF3 压缩包{formatList(config.gf3_archive_source_dirs)}
-
GF3 来源{formatList(config.gf3_source_dirs)}
+
GF3 legacy L1A{formatList(config.gf3_source_dirs)}
GF3 原生{formatList(config.gf3_sarscape_native_dirs)}
GF3 存储{formatList(config.gf3_storage_dirs)}
+
GF3 runtime{config.gf3_sarscape_runtime_dir || '未配置'}
D-InSAR 结果{formatList(config.dinsar_dirs)}
@@ -888,31 +892,36 @@ const DataMonitorPanel = ({ apiEndpoint, onTaskStart, readOnly = false, enabled
-
GF3 归档预处理
+
GF3 SARscape 生产
压缩包来源{formatList(config.gf3_archive_source_dirs)}
-
L1A 来源{formatList(config.gf3_source_dirs)}
SARscape 原生{formatList(config.gf3_sarscape_native_dirs)}
L2 存储{formatList(config.gf3_storage_dirs)}
+
Runtime{config.gf3_sarscape_runtime_dir || '未配置'}
Wrapper{config.gf3_sarscape_wrapper_exe || '未配置'}
SARscape DEM{config.gf3_sarscape_dem_path || '未配置'}
极化{config.gf3_sarscape_polarizations || 'HH,HV'}
+
Legacy GDAL{gf3LegacyGdalEnabled ? '启用' : '关闭'}
- - + {gf3LegacyGdalEnabled && ( + <> + + + + )}