Skip to content

Commit f87bbf4

Browse files
committed
ADR-0013 切片 3 第 13 + 12 项:跑完的任务里,追问可以直接变成一轮修改,终态不再是死路。modeling-workspace-api 新增 postRunRevision(runId, text) 与回执类型 RunRevisionReceipt(run_id/round/approval_id/suggested_stage/note_id),并导出 RUN_REVISION_TEXT_LIMIT=2000 与服务端 RunRevisionInput.text 的 max_length 对齐。openmathmodel-ui:postRunNote 撞 409 RUN_FINISHED 时那条 info 行不再是死路——原文案「运行已到终态……请基于此任务再发起一次新任务」在切片 2 落地后已经是错话,改为如实说明「若这次运行是正常完成的,可以按这条要求发起一轮修改」,并由新函数 offerRevisionCta 在该行下挂一个真按钮「按这条要求继续修改」。四处取舍:一、按钮只调 /revisions,它只做重开运行、落 global 备注、挂审批门三件事,真正的重跑与花费要等用户在审批门里再确认一次,所以这个按钮不会偷偷扣钱——CSS 上也刻意用次级按钮而非主行动色,不让它看起来像「点了就开跑」;二、落盘的轨迹行不带按钮,按钮是活的 DOM、刷新后点不了,历史里只留一句刷新后仍然成立的说明,成功后只改活的 DOM、不回改已推进 traceLog 的记录;三、正文超过 2000 字直接不给按钮并当场说清「这条有 N 字、上限 2000、请精简后重发」,而不是让用户点一下换回一个 422;四、成功文案不重复报服务端建议的起点,起点以审批门里的选项为准(第 15 项刚把那句话写全),两处各说一遍容易打架,审批门本身经 SSE 的 approval.requested 自己会出现,前端不手动刷新。三种 409 分别给话:RUN_NOT_COMPLETED(失败或已取消的运行没有修订入口)、REVISION_LIMIT_REACHED(三轮用尽请另起新任务)、其余错误恢复按钮并弹 toast 让用户重试。workflow-refresh.css 新增 .stream-cta(复用 .stream-detail 卡片底座,按钮走次级样式,含深色主题);en-US 补一条 toast 词条。验证:npm run check(app+node 两套 tsc + eslint)过、node --test 62/62、四个改动文件 lint 干净。如实备案:web 包没有 DOM 测试栈,这条 CTA 的点击路径(按钮→/revisions→三种 409 分支)本批未经真实浏览器验收,目前只有类型与静态核对作门槛;服务端那一侧由 test_run_revisions.py 覆盖
1 parent 00983cf commit f87bbf4

4 files changed

Lines changed: 122 additions & 3 deletions

File tree

apps/web/src/i18n/en-US.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,7 @@ const TASK_RUNNING: Record<string, string> = {
384384
"回复生成中断": "Reply interrupted",
385385
"回复仍在生成中…": "Reply still generating…",
386386
"回复生成中断,请重新发送。": "Reply generation was interrupted; please send it again.",
387+
"发起修改失败,请稍后重试": "Could not start the revision; please try again",
387388
"暂停生成": "Stop generating",
388389
"已暂停生成": "Generation stopped",
389390
"已暂停生成。": "Generation stopped.",

apps/web/src/integration/modeling-workspace-api.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,21 @@ export interface TaskIntakeResult {
8989
source: "heuristic" | "judge" | "fallback";
9090
}
9191

92+
/** POST /task-runs/{id}/revisions 的受理回执(ADR-0013)。
93+
* `suggested_stage` 是服务端建议的重做起点,已作为审批门里唯一的 recommended 项;
94+
* 最终起点由用户在审批门拍板,本回执只负责把人带到那道门前。 */
95+
export interface RunRevisionReceipt {
96+
run_id: string;
97+
round: number;
98+
approval_id: string;
99+
suggested_stage: string;
100+
note_id: string;
101+
}
102+
103+
/** 修改要求正文上限,与服务端 RunRevisionInput.text 的 max_length 对齐;
104+
* 超长在前端就拦下来,别等服务端回 422 才告诉用户白写了。 */
105+
export const RUN_REVISION_TEXT_LIMIT = 2000;
106+
92107
/** GET /task-runs/{id}/stage-outputs:五类页面正文(未产出的阶段为 null)。 */
93108
export interface StageOutputsPayload {
94109
run_id: string;
@@ -253,6 +268,26 @@ export const modelingWorkspaceApi = {
253268
});
254269
},
255270

271+
/** 对已完成的运行提出修改要求(ADR-0013):重开运行并挂审批门等用户确认重做起点。
272+
* 本接口只受理不推进——真正重跑要等用户在审批门里选定起点。
273+
* 409 分三种:RUN_NOT_COMPLETED(运行还没跑完)、REVISION_LIMIT_REACHED(三轮已满)、
274+
* 以及 /notes 那边的 RUN_FINISHED(终态但不是 COMPLETED,如失败/取消),调用方分别给话。 */
275+
postRunRevision(
276+
runId: string,
277+
text: string,
278+
signal?: AbortSignal,
279+
): Promise<RunRevisionReceipt> {
280+
return request<RunRevisionReceipt>(
281+
`/api/v1/task-runs/${encodeURIComponent(runId)}/revisions`,
282+
{
283+
method: "POST",
284+
signal,
285+
headers: { "Content-Type": "application/json" },
286+
body: JSON.stringify({ text }),
287+
},
288+
);
289+
},
290+
256291
act(runId: string, input: TaskRunActionInput, signal?: AbortSignal): Promise<unknown> {
257292
const idempotencyKey = input.client_token ?? crypto.randomUUID().replaceAll("-", "");
258293
return request(`/api/v1/task-runs/${encodeURIComponent(runId)}/actions`, {

apps/web/src/legacy/openmathmodel-ui.ts

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ import {
6969
syncPrivacyGatesOnce,
7070
} from "../preferences/privacy-preferences";
7171
import { mountModelingWorkspace } from "../integration/modeling-workspace-controller";
72-
import { WorkspaceApiError, modelingWorkspaceApi } from "../integration/modeling-workspace-api";
72+
import { RUN_REVISION_TEXT_LIMIT, WorkspaceApiError, modelingWorkspaceApi } from "../integration/modeling-workspace-api";
7373
import { mountSidebarSearch } from "../integration/sidebar-search";
7474
import { hydrateRecentTasks } from "../integration/recent-tasks";
7575
import { hydrateProjectsPage } from "../integration/projects-page";
@@ -3447,6 +3447,58 @@ import { mountTaskAutosave } from "../tasks/task-autosave";
34473447
};
34483448
}
34493449

3450+
/** 已完成的运行撞上 409 时挂一个「按这条要求继续修改」的按钮(ADR-0013 第 12 项)。
3451+
*
3452+
* 点它只是**受理**:服务端重开运行、落一条 global 备注、挂起审批门等用户选定重做
3453+
* 起点——真正的重跑与花费要等审批门里那一下,所以这个按钮本身不会偷偷扣钱。
3454+
* 三种 409 分别给话:非正常完成的运行(失败/取消)没有修订入口,三轮用尽要另起新任务。
3455+
*/
3456+
function offerRevisionCta(handle, runId, text) {
3457+
if (!handle?.element) return;
3458+
const host = document.createElement("div");
3459+
host.className = "stream-detail stream-cta";
3460+
handle.element.append(host);
3461+
3462+
const settleAs = message => {
3463+
host.replaceChildren(Object.assign(document.createElement("span"), { textContent: message }));
3464+
};
3465+
3466+
// 服务端 RunRevisionInput.text 卡 2000 字。超长就不给按钮:与其让用户点一下
3467+
// 换回一个 422,不如当场说清为什么点不了、要怎么办。
3468+
if (text.length > RUN_REVISION_TEXT_LIMIT) {
3469+
settleAs(`这条消息有 ${text.length} 字,超过修改要求的 ${RUN_REVISION_TEXT_LIMIT} 字上限;请精简成一条明确的修改要求后重新发送。`);
3470+
return;
3471+
}
3472+
3473+
const button = document.createElement("button");
3474+
button.type = "button";
3475+
button.textContent = "按这条要求继续修改";
3476+
host.append(button);
3477+
3478+
button.addEventListener("click", async () => {
3479+
if (button.disabled) return;
3480+
button.disabled = true;
3481+
button.textContent = "正在受理…";
3482+
try {
3483+
const receipt = await modelingWorkspaceApi.postRunRevision(runId, text);
3484+
// 重做起点由审批门里的选项说了算,这里不重复报建议值(免得两处措辞打架);
3485+
// 审批门本身会经 SSE 的 approval.requested 自己出现,无需前端手动刷新。
3486+
settleAs(`已受理第 ${receipt.round} 轮修改:请在待确认事项中选定重做起点后生效。`);
3487+
} catch (error) {
3488+
const code = error instanceof WorkspaceApiError ? error.code : "";
3489+
if (code === "RUN_NOT_COMPLETED") {
3490+
settleAs("这次运行不是正常完成的(失败或已取消),没有修改入口;请基于当前结果新建任务。");
3491+
} else if (code === "REVISION_LIMIT_REACHED") {
3492+
settleAs("本次运行的修改轮数已用完(上限 3 轮);如仍需调整,请基于当前结果新建任务。");
3493+
} else {
3494+
button.disabled = false;
3495+
button.textContent = "按这条要求继续修改";
3496+
toast(t("发起修改失败,请稍后重试"));
3497+
}
3498+
}
3499+
});
3500+
}
3501+
34503502
/** 回复右下角操作区:复制原始回复文本(Markdown 源码,便于粘贴到论文与笔记)。 */
34513503
function appendReplyActions(replyBlock, replyText) {
34523504
const actions = document.createElement("div");
@@ -3624,13 +3676,17 @@ import { mountTaskAutosave } from "../tasks/task-autosave";
36243676
await modelingWorkspaceApi.postRunNote(noteScope.runId, text);
36253677
} catch (error) {
36263678
if (error instanceof WorkspaceApiError && error.code === "RUN_FINISHED") {
3679+
// 终态不再是死路(ADR-0013):已完成的运行可以按这条要求真正返工。
3680+
// 落盘的这行只写「本轮发生了什么」——按钮是活的 DOM,重开页面点不了,
3681+
// 所以历史里不留会骗人的按钮,只留一句仍然成立的说明。
36273682
const row = {
36283683
icon: "info",
36293684
title: "任务已结束,本条按问答处理",
3630-
detail: "运行已到终态,消息不再影响执行与成果;如需按新要求修改,请基于此任务再发起一次新任务。",
3685+
detail: "运行已到终态,这条消息不再影响执行与成果;若这次运行是正常完成的,可以按这条要求发起一轮修改——重开运行后需要你选定从哪个阶段重做。",
36313686
};
3632-
appendReplyTraceRow(replyBlock, row);
3687+
const handle = appendReplyTraceRow(replyBlock, row);
36333688
traceLog.push(row);
3689+
offerRevisionCta(handle, noteScope.runId, text);
36343690
}
36353691
// 网络抖动等其它失败不打断对话本身,也不渲染误导性的成功行
36363692
}

apps/web/src/workflow-refresh.css

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3757,8 +3757,35 @@ html[data-reduce-motion="on"] .assistant-block-reveal { animation: none; }
37573757
}
37583758
html[data-theme="dark"] .stream-narration { color: #d0d0ca; }
37593759
html[data-theme="dark"] .stream-row { color: #b9b9b2; }
3760+
/* 终态运行的「按这条要求继续修改」入口(ADR-0013 第 12 项):过程行下挂一个真按钮。
3761+
点它只是受理修订、真正重跑与花费仍要在审批门里再确认一次,所以用低调的次级按钮,
3762+
不给主行动色——避免看起来像「点了就开跑」。 */
3763+
.stream-cta {
3764+
display: flex;
3765+
align-items: center;
3766+
gap: 10px;
3767+
color: #6d6d67;
3768+
font-size: 13px;
3769+
line-height: 1.75;
3770+
}
3771+
.stream-cta button {
3772+
height: 30px;
3773+
padding: 0 14px;
3774+
flex: 0 0 auto;
3775+
border: 1px solid #dcdcd8;
3776+
border-radius: 8px;
3777+
color: #22221f;
3778+
background: #fff;
3779+
font-size: 13px;
3780+
cursor: pointer;
3781+
}
3782+
.stream-cta button:hover:not(:disabled) { border-color: #bcbcb6; background: #f4f4f1; }
3783+
.stream-cta button:disabled { color: #9a9a95; cursor: default; }
37603784
html[data-theme="dark"] .stream-detail { border-color: #3a3a37; background: #262624; }
37613785
html[data-theme="dark"] .stream-detail pre { color: #a8a8a1; }
3786+
html[data-theme="dark"] .stream-cta { color: #a8a8a1; }
3787+
html[data-theme="dark"] .stream-cta button { border-color: #45453f; color: #d8d8d2; background: #2e2e2c; }
3788+
html[data-theme="dark"] .stream-cta button:hover:not(:disabled) { border-color: #56564f; background: #383835; }
37623789
html[data-reduce-motion="on"] .stream-in,
37633790
html[data-reduce-motion="on"] .stream-item.is-waiting .stream-row > i { animation: none; }
37643791
html[data-reduce-motion="on"] .stream-title.title-shine-once {

0 commit comments

Comments
 (0)