mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 03:55:09 +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 CLI local artifacts
|
||||||
.codex/
|
.codex/
|
||||||
|
.superpowers/
|
||||||
|
|
||||||
# Sync tooling local state (generated each run; report is intentionally tracked)
|
# Sync tooling local state (generated each run; report is intentionally tracked)
|
||||||
scripts/.*-sync-state.json
|
scripts/.*-sync-state.json
|
||||||
|
|||||||
@ -166,6 +166,22 @@ public class WikiTransformationController {
|
|||||||
return R.fail("One of rawId / kbId / transformationId is required");
|
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")
|
@RequireWorkspaceRole("member")
|
||||||
@Operation(summary = "Save a completed run's output as a synthesis wiki page",
|
@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.")
|
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()) {
|
if (output == null || output.isBlank()) {
|
||||||
throw new IllegalStateException("LLM returned empty output");
|
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.setOutput(output);
|
||||||
run.setStatus("completed");
|
run.setStatus("completed");
|
||||||
run.setCompletedAt(LocalDateTime.now());
|
run.setCompletedAt(LocalDateTime.now());
|
||||||
@ -175,6 +185,12 @@ public class WikiTransformationExecutor {
|
|||||||
run.getId(), transformation.getName(), rawId, raw.getKbId(),
|
run.getId(), transformation.getName(), rawId, raw.getKbId(),
|
||||||
run.getDurationMs(), run.getOutputPageId());
|
run.getDurationMs(), run.getOutputPageId());
|
||||||
} catch (Exception e) {
|
} 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");
|
run.setStatus("failed");
|
||||||
String msg = e.getMessage();
|
String msg = e.getMessage();
|
||||||
run.setError(msg == null ? e.getClass().getSimpleName() : msg);
|
run.setError(msg == null ? e.getClass().getSimpleName() : msg);
|
||||||
@ -187,6 +203,27 @@ public class WikiTransformationExecutor {
|
|||||||
return run;
|
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) {
|
private String renderTemplate(String template, WikiRawMaterialEntity raw, String inputText) {
|
||||||
if (template == null) return "";
|
if (template == null) return "";
|
||||||
String result = template;
|
String result = template;
|
||||||
|
|||||||
@ -715,6 +715,8 @@ export const wikiApi = {
|
|||||||
http.delete(`/wiki/transformations/runs/${runId}`),
|
http.delete(`/wiki/transformations/runs/${runId}`),
|
||||||
saveTransformationRunAsPage: (runId: number) =>
|
saveTransformationRunAsPage: (runId: number) =>
|
||||||
http.post(`/wiki/transformations/runs/${runId}/save-as-page`),
|
http.post(`/wiki/transformations/runs/${runId}/save-as-page`),
|
||||||
|
cancelTransformationRun: (runId: number) =>
|
||||||
|
http.post(`/wiki/transformations/runs/${runId}/cancel`),
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Workspace (Team) ====================
|
// ==================== Workspace (Team) ====================
|
||||||
|
|||||||
@ -1838,6 +1838,12 @@ export default {
|
|||||||
saveBtn: 'Save',
|
saveBtn: 'Save',
|
||||||
cancelBtn: 'Cancel',
|
cancelBtn: 'Cancel',
|
||||||
runs: 'Run history',
|
runs: 'Run history',
|
||||||
|
rerunBtn: 'Re-run',
|
||||||
|
rerunning: 'Re-running…',
|
||||||
|
cancelRunBtn: 'Cancel',
|
||||||
|
cancelling: 'Cancelling…',
|
||||||
|
cancelDone: 'Cancelled',
|
||||||
|
cancelFailed: 'Cancel failed',
|
||||||
runOn: 'Ran on',
|
runOn: 'Ran on',
|
||||||
runStatus: 'Status',
|
runStatus: 'Status',
|
||||||
runDuration: 'Duration',
|
runDuration: 'Duration',
|
||||||
|
|||||||
@ -1850,6 +1850,12 @@ export default {
|
|||||||
saveBtn: '保存',
|
saveBtn: '保存',
|
||||||
cancelBtn: '取消',
|
cancelBtn: '取消',
|
||||||
runs: '运行历史',
|
runs: '运行历史',
|
||||||
|
rerunBtn: '重新运行',
|
||||||
|
rerunning: '重新运行中…',
|
||||||
|
cancelRunBtn: '取消',
|
||||||
|
cancelling: '取消中…',
|
||||||
|
cancelDone: '已取消',
|
||||||
|
cancelFailed: '取消失败',
|
||||||
runOn: '运行于',
|
runOn: '运行于',
|
||||||
runStatus: '状态',
|
runStatus: '状态',
|
||||||
runDuration: '耗时',
|
runDuration: '耗时',
|
||||||
|
|||||||
@ -109,11 +109,36 @@
|
|||||||
>
|
>
|
||||||
{{ t('wiki.transformations.openPage') }}
|
{{ t('wiki.transformations.openPage') }}
|
||||||
</button>
|
</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>
|
||||||
<div class="run-output">{{ run.output }}</div>
|
<div class="run-output">{{ run.output }}</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-else-if="run.status === 'failed'" class="run-error">{{ run.error }}</div>
|
<template v-else-if="run.status === 'failed' || run.status === 'cancelled'">
|
||||||
<div v-else class="run-output run-output--muted">{{ t('wiki.transformations.running') }}</div>
|
<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>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
@ -242,7 +267,7 @@ interface WikiTransformationRun {
|
|||||||
rawId: number | null
|
rawId: number | null
|
||||||
pageId: number | null
|
pageId: number | null
|
||||||
inputKind: string
|
inputKind: string
|
||||||
status: 'pending' | 'running' | 'completed' | 'failed'
|
status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'
|
||||||
output: string | null
|
output: string | null
|
||||||
error: string | null
|
error: string | null
|
||||||
durationMs: number | null
|
durationMs: number | null
|
||||||
@ -268,6 +293,8 @@ const editorOpen = ref(false)
|
|||||||
const editing = ref<WikiTransformation | null>(null)
|
const editing = ref<WikiTransformation | null>(null)
|
||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
const savingRunId = ref<number | null>(null)
|
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 }
|
interface ModelOption { id: number; name: string; provider: string; modelName: string }
|
||||||
const availableModels = ref<ModelOption[]>([])
|
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) {
|
async function onOpenSavedPage(run: WikiTransformationRun) {
|
||||||
if (!run.outputPageId || !store.currentKB) return
|
if (!run.outputPageId || !store.currentKB) return
|
||||||
try {
|
try {
|
||||||
@ -640,6 +693,7 @@ onMounted(async () => {
|
|||||||
.run-status--completed { background: var(--el-color-success-light-9); color: var(--el-color-success); }
|
.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--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--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-meta { color: var(--mc-text-tertiary); }
|
||||||
.run-output {
|
.run-output {
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user