fix(wiki): JobStageBar stuck at queued — add job stage transitions

Root cause: processRawMaterial() created a job record at queued stage
but never called jobService.transition() during processing. The job row
stayed at queued forever, so the stage bar never advanced.

Backend (WikiProcessingService):
- Transition job to ROUTING immediately after creation
- Transition to PHASE_A_RUNNING before chunk processing begins
- Transition to COMPLETED/PARTIAL/FAILED at the end based on finalStatus
- Transition to FAILED in the catch block on unhandled exceptions

Backend (WikiProcessingJobService.transition):
- Handle FAILED, PARTIAL terminal stages (set finishedAt + status)
- Handle non-terminal intermediate stages (set status to running)

Frontend (JobStageBar.vue):
- Add stageMapping for backend stages not shown as dots: phase_a_done →
  phase_b_running, failed/partial/cancelled → completed position
- Guard stageIndex() against -1 (unknown stages default to all-pending)
- Terminal failure states show red failed dot instead of pulsing active
This commit is contained in:
matevip 2026-04-19 20:53:35 +08:00
parent af8f712986
commit 0301d5628e
3 changed files with 62 additions and 4 deletions

View File

@ -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;

View File

@ -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()));

View File

@ -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<string, string> = {
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'