问数据页的复核修正(前端):使用者不给「继续运行」,两个标签页同时打开停在半路的一轮时后到的那个接着看

- 使用者(user 角色)续不了运行(服务端照 POST /api/runs/{id}/continue 的老规矩回 403):补救区不摆「继续运行」,服务重启打断的
  那一轮说明里也不叫它去点,改说「如需整轮重来,请点击「重新运行本轮」」(lib/terms 的 CHAT_TURN_TEXT);重试本轮、重新运行本轮照常。
  以前这个按钮对使用者也摆着,点了只弹一句 403。
- 两个标签页同时打开停在半路的一轮:都会请服务端接着收尾,服务端现在只起得来一次,后到的那个收到 409 chat_turn_busy 时不再说
  「无法确认本轮的当前状态」,改成问一下这一轮现在怎样——在跑就从这一次尝试的第一条接着播,收了尾就照那一行摆。
- scripts/check-chat.mjs 新加两段:「使用者(user 角色)」(伪造 /api/auth/me 认出使用者:中断的、跑挂的都不给继续运行,一次续跑都
  不发)、「两个标签页同时打开」(服务端只接着收尾一次,两边都收尾、答案一样、都不写库)。去掉修复回跑,这两段都会失败。
This commit is contained in:
ThearchyHelios
2026-10-18 18:07:03 +08:00
parent 85eee89799
commit d6318b639d
4 changed files with 82 additions and 4 deletions
+6
View File
@@ -768,6 +768,12 @@ export function runCostText(usd: number | null | undefined, unpriced: number, ap
return `${(approx || unpriced > 0) && usd > 0 ? '≥ ' : ''}${formatCost(usd)}`
}
/** 问数据的一轮在服务端跑(编排完善第 1 波 W1-a):使用者(user 角色)不能继续运行时,补救区里那句「怎么办」 */
export const CHAT_TURN_TEXT = {
/** 服务重启打断的一轮:使用者没有「继续运行」,只说重新运行 */
suspendedHintForUser: '如需整轮重来,请点击「重新运行本轮」。',
} as const
/**
* 问数据一轮的花费:助手建图(含挑表、接着写、自查修正)+ 运行 + 复核(next-round 细案 01 做法 3)。以前只算运行那份,
* 这一轮的花费是低报的
+7 -3
View File
@@ -18,7 +18,7 @@ import { humanizeError } from '../lib/errors'
import { explainRunError } from '../lib/explain'
import { canLeave, leavePass } from '../lib/leave'
import { formatNumber, formatTime } from '../lib/format'
import { CHAT_BLUEPRINT_TEXT, REJECTED_TEXT, TURN_COST_TEXT, runCostText } from '../lib/terms'
import { CHAT_BLUEPRINT_TEXT, CHAT_TURN_TEXT, REJECTED_TEXT, TURN_COST_TEXT, runCostText } from '../lib/terms'
import { useCatalog, useCatalogListState, useDatasources, useOnReconnect, type CatalogListState } from '../store/catalog'
import { useAtLeast } from '../store/auth'
import { useBlueprint } from '../store/blueprint'
@@ -665,6 +665,9 @@ function Remedies({ conversationId, turnId, last, onRephrase, readOnly }: {
const { retryTurn, continueTurn, ask, recheck, setScope } = useChat.getState()
const { cap, fallback } = useStepCap()
const [resuming, setResuming] = useState(false)
// 继续运行只给搭建者及以上:使用者(user 角色)续不了运行(服务端照 POST /api/runs/{id}/continue 的老规矩回 403),
// 摆着只会点一下报一次错;重试本轮、重新运行本轮另起一次问数据运行,照常给
const builder = useAtLeast('builder')
if (!turn) return null
const kind = remedyOf(turn)
@@ -674,7 +677,7 @@ function Remedies({ conversationId, turnId, last, onRephrase, readOnly }: {
&& (!s.reason || s.reason === 'steps'))
const current = fallback ? agentSteps(turn.graph, fallback) : null
const target = current && cap ? Math.min(cap, Math.max(current * 2, current + 8)) : null
const canContinue = !!turn.run?.id && (kind === 'suspended'
const canContinue = builder && !!turn.run?.id && (kind === 'suspended'
|| (kind === 'failed' && turn.runStatus === 'failed' && turn.failure?.continuable !== false))
const offNote = readOnly ? IN_TRASH : busy ? '本对话仍有一轮在运行,请等待其结束' : undefined
const off = busy || readOnly
@@ -775,7 +778,8 @@ function Remedies({ conversationId, turnId, last, onRephrase, readOnly }: {
}
// 中断、取消、没核对上在流里不出红框(它们不是故障),为什么停、停了意味着什么在这里说
const note = kind === 'suspended' || kind === 'unknown' ? turn.failure
const note = kind === 'suspended' && !builder && turn.failure ? { ...turn.failure, hint: CHAT_TURN_TEXT.suspendedHintForUser }
: kind === 'suspended' || kind === 'unknown' ? turn.failure
: kind === 'cancelled' && turn.rejected ? { title: '', reason: REJECTED_TEXT.chatReason, hint: turn.rejected.sub }
: kind === 'cancelled' ? { title: '', reason: turn.run ? '你已停止本轮,对应的运行也已一并取消。' : '你在搭建工作流时停止了本轮。' }
: null
+17
View File
@@ -1189,6 +1189,23 @@ async function resumeTurn(conversationId: string, turnId: string) {
play(flow, turn.serverId, Math.max(0, (res.turn?.job?.attempt_seq ?? 1) - 1))
} catch (e) {
if (!flow.alive()) return
// 别处(另一个标签页)刚请服务端接着收尾了这一轮:不再起一次,接着看它(服务端在跑就从这一次尝试的第一条播,收了尾就照那一行摆)
if (e instanceof ApiError && e.status === 409 && e.code === 'chat_turn_busy') {
try {
const res = await api.chatTurns.get(turn.serverId)
if (!flow.alive()) return
if (res.job && LIVE_JOB.has(res.job.status)) {
flow.patch(() => ({ hydrated: 'done', job: res.job }))
play(flow, turn.serverId, Math.max(0, res.job.attempt_seq - 1))
return
}
if (res.turn && res.turn.status !== 'running') {
flow.patch((t) => ({ ...endPatch(t, { turn: res.turn }), hydrated: 'done' }))
return
}
} catch { /* 问不到就照下面说没核对上 */ }
if (!flow.alive()) return
}
const said = errorMessage(e)
const raw = humanizeError(e).raw
const failure: Failure = {