mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
feat(wiki): cancel a running transformation and re-run any past run
This commit is contained in:
parent
ef6ad1b91a
commit
f04ea58c8d
1
.gitignore
vendored
1
.gitignore
vendored
@ -98,6 +98,7 @@ CLAUDE.md
|
||||
|
||||
# Codex CLI local artifacts
|
||||
.codex/
|
||||
.superpowers/
|
||||
|
||||
# Sync tooling local state (generated each run; report is intentionally tracked)
|
||||
scripts/.*-sync-state.json
|
||||
|
||||
@ -166,6 +166,22 @@ public class WikiTransformationController {
|
||||
return R.fail("One of rawId / kbId / transformationId is required");
|
||||
}
|
||||
|
||||
@RequireWorkspaceRole("member")
|
||||
@Operation(summary = "Cancel a still-running transformation run",
|
||||
description = "Marks the run as cancelled so the executor drops the eventual LLM output. "
|
||||
+ "The HTTP request to the model continues server-side because most providers "
|
||||
+ "do not support cancellation; this endpoint affects bookkeeping only.")
|
||||
@PostMapping("/runs/{runId}/cancel")
|
||||
public R<Void> cancelRun(@PathVariable Long runId,
|
||||
@RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) {
|
||||
WikiTransformationRunEntity run = transformationService.getRun(runId);
|
||||
if (run == null) return R.fail("Run not found");
|
||||
verifyKBWorkspace(run.getKbId(), workspaceId != null ? workspaceId : 1L);
|
||||
boolean cancelled = executor.cancelRun(runId);
|
||||
if (!cancelled) return R.fail("Run is not running");
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@RequireWorkspaceRole("member")
|
||||
@Operation(summary = "Save a completed run's output as a synthesis wiki page",
|
||||
description = "Idempotent: re-saving an already-saved run updates the same page slug.")
|
||||
|
||||
@ -150,6 +150,16 @@ public class WikiTransformationExecutor {
|
||||
if (output == null || output.isBlank()) {
|
||||
throw new IllegalStateException("LLM returned empty output");
|
||||
}
|
||||
// Honour a mid-flight cancel: the cancel endpoint flipped the run
|
||||
// row to 'cancelled' while the LLM was still working. Drop the
|
||||
// output and stop here rather than overwrite the cancelled state.
|
||||
WikiTransformationRunEntity current = transformationService.getRun(run.getId());
|
||||
if (current != null && "cancelled".equalsIgnoreCase(current.getStatus())) {
|
||||
log.info("[WikiTransformation] run={} was cancelled mid-flight; discarding {} chars of LLM output",
|
||||
run.getId(), output.length());
|
||||
return current;
|
||||
}
|
||||
|
||||
run.setOutput(output);
|
||||
run.setStatus("completed");
|
||||
run.setCompletedAt(LocalDateTime.now());
|
||||
@ -175,6 +185,12 @@ public class WikiTransformationExecutor {
|
||||
run.getId(), transformation.getName(), rawId, raw.getKbId(),
|
||||
run.getDurationMs(), run.getOutputPageId());
|
||||
} catch (Exception e) {
|
||||
WikiTransformationRunEntity current = transformationService.getRun(run.getId());
|
||||
if (current != null && "cancelled".equalsIgnoreCase(current.getStatus())) {
|
||||
log.info("[WikiTransformation] run={} was cancelled before failure could be recorded ({})",
|
||||
run.getId(), e.getMessage());
|
||||
return current;
|
||||
}
|
||||
run.setStatus("failed");
|
||||
String msg = e.getMessage();
|
||||
run.setError(msg == null ? e.getClass().getSimpleName() : msg);
|
||||
@ -187,6 +203,27 @@ public class WikiTransformationExecutor {
|
||||
return run;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a still-active run as cancelled. The blocked LLM call (if any)
|
||||
* continues server-side but its eventual output is dropped by the
|
||||
* post-call check in {@link #runOnRawSync}.
|
||||
*/
|
||||
public boolean cancelRun(Long runId) {
|
||||
if (runId == null) return false;
|
||||
WikiTransformationRunEntity run = transformationService.getRun(runId);
|
||||
if (run == null) return false;
|
||||
String status = run.getStatus();
|
||||
if (!"pending".equalsIgnoreCase(status) && !"running".equalsIgnoreCase(status)) {
|
||||
return false;
|
||||
}
|
||||
run.setStatus("cancelled");
|
||||
run.setCompletedAt(LocalDateTime.now());
|
||||
if (run.getError() == null) run.setError("Cancelled by user");
|
||||
transformationService.updateRun(run);
|
||||
log.info("[WikiTransformation] run={} cancelled by user", runId);
|
||||
return true;
|
||||
}
|
||||
|
||||
private String renderTemplate(String template, WikiRawMaterialEntity raw, String inputText) {
|
||||
if (template == null) return "";
|
||||
String result = template;
|
||||
|
||||
@ -715,6 +715,8 @@ export const wikiApi = {
|
||||
http.delete(`/wiki/transformations/runs/${runId}`),
|
||||
saveTransformationRunAsPage: (runId: number) =>
|
||||
http.post(`/wiki/transformations/runs/${runId}/save-as-page`),
|
||||
cancelTransformationRun: (runId: number) =>
|
||||
http.post(`/wiki/transformations/runs/${runId}/cancel`),
|
||||
}
|
||||
|
||||
// ==================== Workspace (Team) ====================
|
||||
|
||||
@ -1838,6 +1838,12 @@ export default {
|
||||
saveBtn: 'Save',
|
||||
cancelBtn: 'Cancel',
|
||||
runs: 'Run history',
|
||||
rerunBtn: 'Re-run',
|
||||
rerunning: 'Re-running…',
|
||||
cancelRunBtn: 'Cancel',
|
||||
cancelling: 'Cancelling…',
|
||||
cancelDone: 'Cancelled',
|
||||
cancelFailed: 'Cancel failed',
|
||||
runOn: 'Ran on',
|
||||
runStatus: 'Status',
|
||||
runDuration: 'Duration',
|
||||
|
||||
@ -1850,6 +1850,12 @@ export default {
|
||||
saveBtn: '保存',
|
||||
cancelBtn: '取消',
|
||||
runs: '运行历史',
|
||||
rerunBtn: '重新运行',
|
||||
rerunning: '重新运行中…',
|
||||
cancelRunBtn: '取消',
|
||||
cancelling: '取消中…',
|
||||
cancelDone: '已取消',
|
||||
cancelFailed: '取消失败',
|
||||
runOn: '运行于',
|
||||
runStatus: '状态',
|
||||
runDuration: '耗时',
|
||||
|
||||
@ -109,11 +109,36 @@
|
||||
>
|
||||
{{ t('wiki.transformations.openPage') }}
|
||||
</button>
|
||||
<button class="btn-secondary" :disabled="rerunningRunId === run.id" @click="onRerun(tpl, run)">
|
||||
{{ rerunningRunId === run.id
|
||||
? t('wiki.transformations.rerunning')
|
||||
: t('wiki.transformations.rerunBtn') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="run-output">{{ run.output }}</div>
|
||||
</template>
|
||||
<div v-else-if="run.status === 'failed'" class="run-error">{{ run.error }}</div>
|
||||
<div v-else class="run-output run-output--muted">{{ t('wiki.transformations.running') }}</div>
|
||||
<template v-else-if="run.status === 'failed' || run.status === 'cancelled'">
|
||||
<div class="run-actions">
|
||||
<button class="btn-secondary" :disabled="rerunningRunId === run.id" @click="onRerun(tpl, run)">
|
||||
{{ rerunningRunId === run.id
|
||||
? t('wiki.transformations.rerunning')
|
||||
: t('wiki.transformations.rerunBtn') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="run-error">{{ run.error }}</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="run-actions">
|
||||
<button class="btn-secondary btn-danger"
|
||||
:disabled="cancellingRunId === run.id"
|
||||
@click="onCancelRun(tpl, run)">
|
||||
{{ cancellingRunId === run.id
|
||||
? t('wiki.transformations.cancelling')
|
||||
: t('wiki.transformations.cancelRunBtn') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="run-output run-output--muted">{{ t('wiki.transformations.running') }}</div>
|
||||
</template>
|
||||
</details>
|
||||
</div>
|
||||
</article>
|
||||
@ -242,7 +267,7 @@ interface WikiTransformationRun {
|
||||
rawId: number | null
|
||||
pageId: number | null
|
||||
inputKind: string
|
||||
status: 'pending' | 'running' | 'completed' | 'failed'
|
||||
status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'
|
||||
output: string | null
|
||||
error: string | null
|
||||
durationMs: number | null
|
||||
@ -268,6 +293,8 @@ const editorOpen = ref(false)
|
||||
const editing = ref<WikiTransformation | null>(null)
|
||||
const saving = ref(false)
|
||||
const savingRunId = ref<number | null>(null)
|
||||
const cancellingRunId = ref<number | null>(null)
|
||||
const rerunningRunId = ref<number | null>(null)
|
||||
interface ModelOption { id: number; name: string; provider: string; modelName: string }
|
||||
const availableModels = ref<ModelOption[]>([])
|
||||
|
||||
@ -487,6 +514,32 @@ async function onSaveRunAsPage(tpl: WikiTransformation, run: WikiTransformationR
|
||||
}
|
||||
}
|
||||
|
||||
async function onCancelRun(tpl: WikiTransformation, run: WikiTransformationRun) {
|
||||
cancellingRunId.value = run.id
|
||||
try {
|
||||
await wikiApi.cancelTransformationRun(run.id)
|
||||
ElMessage.success(t('wiki.transformations.cancelDone'))
|
||||
await loadRunsFor(tpl.id)
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.message ?? t('wiki.transformations.cancelFailed'))
|
||||
} finally {
|
||||
cancellingRunId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function onRerun(tpl: WikiTransformation, run: WikiTransformationRun) {
|
||||
if (!run.rawId) return
|
||||
rerunningRunId.value = run.id
|
||||
try {
|
||||
await wikiApi.applyTransformation(tpl.id, run.rawId, true)
|
||||
await loadRunsFor(tpl.id)
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.message ?? t('wiki.transformations.runFailed'))
|
||||
} finally {
|
||||
rerunningRunId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function onOpenSavedPage(run: WikiTransformationRun) {
|
||||
if (!run.outputPageId || !store.currentKB) return
|
||||
try {
|
||||
@ -640,6 +693,7 @@ onMounted(async () => {
|
||||
.run-status--completed { background: var(--el-color-success-light-9); color: var(--el-color-success); }
|
||||
.run-status--failed { background: var(--el-color-danger-light-9); color: var(--el-color-danger); }
|
||||
.run-status--running, .run-status--pending { background: var(--mc-bg-muted); color: var(--mc-text-secondary); }
|
||||
.run-status--cancelled { background: var(--mc-bg-muted); color: var(--mc-text-tertiary); }
|
||||
.run-meta { color: var(--mc-text-tertiary); }
|
||||
.run-output {
|
||||
margin-top: 6px;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user