From 01776511b66bfd87b4f8ef57d3fefc1d99e1e8f6 Mon Sep 17 00:00:00 2001
From: shuishen <1109946754@qq.com>
Date: Wed, 19 Aug 2026 16:46:52 +0800
Subject: [PATCH] feat:变化检测相关优化调整

---
 apps/workbench-console/src/api/artifacts.ts |   17 ++++++++++++++---
 1 files changed, 14 insertions(+), 3 deletions(-)

diff --git a/apps/workbench-console/src/api/artifacts.ts b/apps/workbench-console/src/api/artifacts.ts
index 16c794a..d8b4f07 100644
--- a/apps/workbench-console/src/api/artifacts.ts
+++ b/apps/workbench-console/src/api/artifacts.ts
@@ -10,8 +10,11 @@
 export interface MeasurementRun { capability: string; classification: string; created_at?: string; geoai_version: string; method: string; model: string; device: string; input_count: number; processed_images: number; elapsed_seconds: number; images: MeasurementImage[]; limitations: string[]; input_dir?: string; raw_input_dir?: string; }
 export interface MeasurementCase { id: string; label: string; note: string; artifactRoot: string; createdAt: string; run: MeasurementRun; }
 export interface ChangeFeature { type: "Feature"; properties: { feature_id?: number; area_pixels?: number; mean_probability?: number; max_probability?: number; bounds_pixel?: number[]; confidence?: number; class?: number }; geometry: { type: string; coordinates: unknown }; }
-export interface ChangeRun { schema_version: number; capability: string; classification: string; geoai_version: string; method: string; model: string; device: string; processing_mode?: "image" | "geotiff"; requested_processing_mode?: "auto" | "image" | "geotiff"; thresholds: Record<string, number>; tile_size: number; overlap: number; max_dimension?: number; effective_max_dimension?: number; input_files: string[]; input_shape: number[]; processed_shape: number[]; registration: { method: string; matches: number; inliers: number; inlier_ratio: number; valid_ratio: number }; valid_pixel_ratio: number; raw_changed_pixels: number; changed_pixels: number; changed_pixel_ratio: number; vector_feature_count: number; georeferenced: boolean; coordinate_basis: string; crs: string | null; elapsed_seconds: number; limitations: string[]; artifacts: { probability_raster: string; raw_mask_raster: string; mask_raster: string; overlay: string; vector: string; features: string }; created_at?: string; }
+export interface ChangeRun { schema_version: number; capability: string; classification: string; geoai_version: string; method: string; model: string; device: string; processing_mode?: "image" | "geotiff"; requested_processing_mode?: "auto" | "image" | "geotiff"; thresholds: Record<string, number>; tile_size: number; overlap: number; max_dimension?: number; effective_max_dimension?: number; input_files: string[]; input_shape: number[]; processed_shape: number[]; registration: { method: string; matches: number; inliers: number; inlier_ratio: number; valid_ratio: number }; valid_pixel_ratio: number; raw_changed_pixels: number; changed_pixels: number; changed_pixel_ratio: number; vector_feature_count: number; rectangle_feature_count?: number; georeferenced: boolean; coordinate_basis: string; crs: string | null; elapsed_seconds: number; limitations: string[]; artifacts: { probability_raster: string; raw_mask_raster: string; mask_raster: string; overlay: string; vector: string; rectangle_vector?: string; rectangle_vector_wgs84?: string | null; features: string }; created_at?: string; }
 export interface ChangeCase { id: string; label: string; note: string; artifactRoot: string; beforeImage: string; afterImage: string; createdAt: string; run: ChangeRun; features: ChangeFeature[]; }
+export interface ChangeScanResult { id: string; label: string; threshold: number; minimumAreaPixels: number; cleanedComponents: number; changedPixels: number; changedPixelRatio: number; vectorFeatureCount: number; fullVectorFeatureCount?: number | null; rectangleFeatureCount?: number | null; overlay: string; mask: string; regions: string; vector?: string | null; rectangleVector?: string | null; rectangleVectorWgs84?: string | null; }
+export interface ChangeParameterScan { id: string; label: string; note: string; artifactRoot: string; sourceRun?: string; userSubmitted?: boolean; contactSheet?: string | null; results: ChangeScanResult[]; }
+export interface ChangeScanJob { id: string; status: "queued" | "running" | "completed" | "failed"; phase?: "inference" | "parameter-scan" | "vectorization" | "done" | "error"; scanId?: string; error?: string; thresholds?: number[]; minimumAreas?: number[]; }
 export interface TrajectoryEvent { event_id: string; event_type: string; track_ids: string[]; start_time: string; end_time: string; duration_seconds: number; }
 export interface TrajectoryCaseRun { case_id: string; input_count: number; track_count: number; event_count: number; dropped_duplicate_observations: number; elapsed_seconds: number; device: string; input: string; thresholds: Record<string, number>; created_at?: string; }
 export interface TrajectorySummary { track_id: string; entity_type: string; point_count: string; distance_m: string; average_speed_mps: string; behavior_labels: string; }
@@ -74,10 +77,13 @@
   const { runs } = await getJson<{ runs: ChangeDefinition[] }>("api/change-detection/runs");
   const cases = await Promise.all(runs.map(async (definition) => {
     const run = await getJson<ChangeRun>(`${definition.artifactRoot}/run_metadata.json`);
-    const vector = await getJson<{ features?: ChangeFeature[] }>(`${definition.artifactRoot}/${run.artifacts.vector}`);
+    const vector = await getJson<{ features?: ChangeFeature[] }>(`${definition.artifactRoot}/${run.artifacts.rectangle_vector || run.artifacts.vector}`);
     return { ...definition, run, features: vector.features ?? [] } satisfies ChangeCase;
   }));
   return Object.fromEntries(cases.map((item) => [item.id, item]));
+}
+export async function loadChangeParameterScans(): Promise<ChangeParameterScan[]> {
+  return (await getJson<{ scans: ChangeParameterScan[] }>("api/change-detection/scans")).scans;
 }
 
 async function postRun<T>(path: string, body: object): Promise<T> { return responseJson<T>(await fetch(artifactUrl(path), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) })); }
@@ -89,8 +95,13 @@
 export async function uploadChangeFile(file: File, role: "before" | "after"): Promise<ChangeUploadRef> {
   const uploadId = globalThis.crypto.randomUUID().replaceAll("-", "");
   const query = new URLSearchParams({ role });
-  const response = await fetch(`/api/change-detection/uploads/${uploadId}?${query.toString()}`, { method: "PUT", headers: { "Content-Type": "application/octet-stream", "X-Upload-Name": file.name }, body: file });
+  // HTTP header values are restricted to ISO-8859-1 by browsers. Percent-encode
+  // the original name so Chinese filenames do not fail before the upload starts.
+  const response = await fetch(`/api/change-detection/uploads/${uploadId}?${query.toString()}`, { method: "PUT", headers: { "Content-Type": "application/octet-stream", "X-Upload-Name": encodeURIComponent(file.name) }, body: file });
   return responseJson<ChangeUploadRef>(response);
 }
 export async function createChangeRunFromUploads(uploads: { before: ChangeUploadRef; after: ChangeUploadRef }, threshold = 0.5, maxDimension = 0, processingMode: "auto" | "image" | "geotiff" = "auto") { return postRun<{ run: ChangeDefinition }>("api/change-detection/runs", { uploads, threshold, maxDimension, processingMode }); }
+export async function createChangeScanFromUploads(uploads: { before: ChangeUploadRef; after: ChangeUploadRef }, thresholds: number[], minimumAreas: number[], maxDimension = 0, processingMode: "auto" | "image" | "geotiff" = "auto") { return postRun<{ job: ChangeScanJob }>("api/change-detection/scans", { uploads, thresholds, minimumAreas, maxDimension, processingMode }); }
+export async function getChangeScanJob(jobId: string) { return (await getJson<{ job: ChangeScanJob }>(`api/change-detection/scan-jobs/${jobId}`)).job; }
+export async function promoteChangeScan(scanId: string, resultId: string) { return postRun<{ run: ChangeDefinition }>(`api/change-detection/scans/${encodeURIComponent(scanId)}/promote`, { resultId }); }
 export function readFileAsPayload(file: File): Promise<UploadFilePayload> { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onerror = () => reject(new Error(`无法读取 ${file.name}`)); reader.onload = () => { const value = String(reader.result ?? ""); resolve({ name: file.name, content: value.slice(value.indexOf(",") + 1) }); }; reader.readAsDataURL(file); }); }

--
Gitblit v1.9.3