diff --git a/mateclaw-server/src/main/java/vip/mate/wiki/job/WikiProcessingJobService.java b/mateclaw-server/src/main/java/vip/mate/wiki/job/WikiProcessingJobService.java index 7e5f1be3..05b7a8ea 100644 --- a/mateclaw-server/src/main/java/vip/mate/wiki/job/WikiProcessingJobService.java +++ b/mateclaw-server/src/main/java/vip/mate/wiki/job/WikiProcessingJobService.java @@ -95,6 +95,15 @@ public class WikiProcessingJobService { } else if (newStage == WikiJobStage.COMPLETED) { job.setFinishedAt(LocalDateTime.now()); job.setStatus(WikiJobStatus.COMPLETED.name().toLowerCase()); + } else if (newStage == WikiJobStage.FAILED) { + job.setFinishedAt(LocalDateTime.now()); + job.setStatus(WikiJobStatus.FAILED.name().toLowerCase()); + } else if (newStage == WikiJobStage.PARTIAL) { + job.setFinishedAt(LocalDateTime.now()); + job.setStatus(WikiJobStatus.PARTIAL.name().toLowerCase()); + } else if (!newStage.isTerminal()) { + // Non-terminal intermediate stage: mark as running + job.setStatus(WikiJobStatus.RUNNING.name().toLowerCase()); } jobMapper.updateById(job); return job; diff --git a/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiProcessingService.java b/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiProcessingService.java index bfcec07a..e27f9311 100644 --- a/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiProcessingService.java +++ b/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiProcessingService.java @@ -137,10 +137,13 @@ public class WikiProcessingService { kbService.updateStatus(kb.getId(), "processing"); - // RFC-030 §9.1: create a processing job record before starting + // RFC-030 §9.1: create a processing job record and track its ID for stage transitions + Long jobId = null; if (wikiJobService != null) { try { - wikiJobService.createHeavyIngest(kb.getId(), rawId); + var job = wikiJobService.createHeavyIngest(kb.getId(), rawId); + jobId = job.getId(); + wikiJobService.transition(jobId, vip.mate.wiki.job.WikiJobStage.ROUTING); } catch (Exception e) { log.warn("[Wiki] Failed to create heavy ingest job record for raw={}: {}", rawId, e.getMessage()); } @@ -182,6 +185,11 @@ public class WikiProcessingService { // Phase 3: 构建已有页面索引(一次构建,所有 chunk 共用) String existingPagesIndex = buildExistingPagesIndex(kb.getId()); + // Transition job to phase_a (chunk processing begins) + if (wikiJobService != null && jobId != null) { + try { wikiJobService.transition(jobId, vip.mate.wiki.job.WikiJobStage.PHASE_A_RUNNING); } catch (Exception ignored) {} + } + // Phase 3: LLM 消化 // result[0] = totalPages, result[1] = failedChunks, result[2] = totalChunks int[] result; @@ -256,6 +264,18 @@ public class WikiProcessingService { "kbPageCount", pageCount)); } + // Transition job to terminal stage + if (wikiJobService != null && jobId != null) { + try { + var terminalStage = switch (finalStatus) { + case "failed" -> vip.mate.wiki.job.WikiJobStage.FAILED; + case "partial" -> vip.mate.wiki.job.WikiJobStage.PARTIAL; + default -> vip.mate.wiki.job.WikiJobStage.COMPLETED; + }; + wikiJobService.transition(jobId, terminalStage); + } catch (Exception ignored) {} + } + log.info("[Wiki] Processing completed for raw={}, kbId={}, generatedPages={}, totalPages={}", rawId, kb.getId(), totalPages, pageCount); @@ -282,6 +302,10 @@ public class WikiProcessingService { log.error("[Wiki] Processing failed for raw={}: {}", rawId, e.getMessage(), e); rawService.updateProcessingStatus(rawId, "failed", e.getMessage()); kbService.updateStatus(kb.getId(), "active"); + // Transition job to failed + if (wikiJobService != null && jobId != null) { + try { wikiJobService.transition(jobId, vip.mate.wiki.job.WikiJobStage.FAILED); } catch (Exception ignored) {} + } // RFC-012 M3:广播异常终态 progressBus.broadcast(kb.getId(), WikiProgressBus.EVENT_RAW_FAILED, java.util.Map.of("rawId", rawId, "error", e.getMessage() == null ? "unknown" : e.getMessage())); diff --git a/mateclaw-ui/src/views/Wiki/components/JobStageBar.vue b/mateclaw-ui/src/views/Wiki/components/JobStageBar.vue index 91974896..e1f3fd9e 100644 --- a/mateclaw-ui/src/views/Wiki/components/JobStageBar.vue +++ b/mateclaw-ui/src/views/Wiki/components/JobStageBar.vue @@ -81,7 +81,25 @@ const stages = [ ] const stageOrder = stages.map(s => s.key) -const currentStage = computed(() => props.stage) + +// Map backend stage values (including intermediates/terminals not shown as dots) +// to their logical position in the visible stage list. +const stageMapping: Record = { + phase_a_done: 'phase_b_running', // between phase_a and phase_b → show as phase_b active + failed: 'completed', // terminal → all dots done up to failure point + partial: 'completed', + cancelled: 'completed', +} + +const currentStage = computed(() => { + const raw = props.stage + return stageMapping[raw] ?? raw +}) + +// True when backend status indicates a terminal failure (dots should not pulse) +const isTerminalFailure = computed(() => + props.status === 'failed' || props.status === 'partial' || props.status === 'cancelled' +) function stageIndex(key: string): number { return stageOrder.indexOf(key) @@ -90,13 +108,20 @@ function stageIndex(key: string): number { function isStageComplete(key: string): boolean { const cur = stageIndex(currentStage.value) const target = stageIndex(key) + if (cur < 0) return false return target < cur } function dotClass(key: string) { const cur = stageIndex(currentStage.value) const target = stageIndex(key) - if (props.status === 'failed' && key === currentStage.value) return 'failed' + if (cur < 0) return 'pending' // unknown stage → all pending + if (isTerminalFailure.value) { + // Terminal failure: all dots before the failure point are done, the failure point is failed + if (target < cur) return 'done' + if (target === cur) return 'failed' + return 'pending' + } if (target < cur) return 'done' if (target === cur) return 'active' return 'pending'