@@ -26,11 +26,23 @@ function formatCountdown(remainingMs: number) {
2626/**
2727 * 倒计时提示:优先使用调用方传入的权威截止时间(GUI 读工具挂起表,WebUI 读
2828 * 网关参数上的 deadline 盖章),两端与桌面计时同源;缺失时(历史/降级数据)
29- * 回退为挂载时刻近似。超时后 tool_result 会把卡片切到只读态。
29+ * 回退为挂载时刻近似。倒计时归零立即禁止交互,随后 tool_result 把卡片
30+ * 切到只读态。
31+ *
32+ * 盖章用的是桌面时钟,而倒计时读本机时钟:远端浏览器时钟偏移足够大时,
33+ * 一个仍在挂起的提问会在挂载瞬间就显示过期(或远超完整窗口)。因此仅当
34+ * 截止时间落在“挂载时刻(不含)~挂载时刻 + 完整应答窗口(含)”内才采信,
35+ * 否则视为时钟不可比、回退挂载近似,避免把可作答的卡片锁死;真正过期的
36+ * 提交仍由桌面挂起表权威拒绝。
3037 */
3138function useAnswerCountdown ( active : boolean , deadlineAt ?: number ) {
32- const [ fallbackDeadline ] = useState ( ( ) => Date . now ( ) + ASK_USER_QUESTION_TIMEOUT_MS ) ;
33- const deadline = deadlineAt ?? fallbackDeadline ;
39+ const [ mountedAt ] = useState ( ( ) => Date . now ( ) ) ;
40+ const deadline =
41+ deadlineAt !== undefined &&
42+ deadlineAt > mountedAt &&
43+ deadlineAt <= mountedAt + ASK_USER_QUESTION_TIMEOUT_MS
44+ ? deadlineAt
45+ : mountedAt + ASK_USER_QUESTION_TIMEOUT_MS ;
3446 const [ remainingMs , setRemainingMs ] = useState ( ( ) => deadline - Date . now ( ) ) ;
3547
3648 useEffect ( ( ) => {
@@ -98,8 +110,10 @@ export function AskUserQuestionCard({
98110
99111 const isSettled = ( answers ?. length ?? 0 ) > 0 ;
100112 const selections = isSettled ? settledSelections : draftSelections ;
101- const canInteract = interactive && ! isSettled && ! cancelled && ! submitting ;
102- const remainingMs = useAnswerCountdown ( interactive && ! isSettled && ! cancelled , deadlineAt ) ;
113+ const countdownActive = interactive && ! isSettled && ! cancelled ;
114+ const remainingMs = useAnswerCountdown ( countdownActive , deadlineAt ) ;
115+ const countdownExpired = countdownActive && remainingMs <= 0 ;
116+ const canInteract = countdownActive && remainingMs > 0 && ! submitting ;
103117
104118 // 该题是否已作答:普通选项已选,或“其他”选中且文本非空。
105119 const isQuestionAnswered = ( questionId : string ) => {
@@ -267,7 +281,9 @@ export function AskUserQuestionCard({
267281 canInteract && ! isSelected
268282 ? "hover:border-border/70 hover:bg-foreground/[0.03] dark:hover:border-white/[0.14]"
269283 : "" ,
270- ! canInteract && ! isSelected && ( isSettled || cancelled ) ? "opacity-55" : "" ,
284+ ! canInteract && ! isSelected && ( isSettled || cancelled || countdownExpired )
285+ ? "opacity-55"
286+ : "" ,
271287 canInteract ? "cursor-pointer" : "cursor-default" ,
272288 ) }
273289 >
@@ -323,7 +339,9 @@ export function AskUserQuestionCard({
323339 canInteract && ! activeCustomSelected
324340 ? "hover:border-border/70 hover:bg-foreground/[0.03] dark:hover:border-white/[0.14]"
325341 : "" ,
326- ! canInteract && ! activeCustomSelected && ( isSettled || cancelled )
342+ ! canInteract &&
343+ ! activeCustomSelected &&
344+ ( isSettled || cancelled || countdownExpired )
327345 ? "opacity-55"
328346 : "" ,
329347 canInteract ? "cursor-pointer" : "cursor-default" ,
@@ -403,7 +421,7 @@ export function AskUserQuestionCard({
403421 </ span >
404422 < button
405423 type = "button"
406- disabled = { ! allAnswered || submitting }
424+ disabled = { ! allAnswered || ! canInteract }
407425 onClick = { ( ) => void submit ( ) }
408426 className = "shrink-0 rounded-lg bg-primary px-3 py-1.5 text-[calc(11px*var(--zone-font-scale,1))] font-medium leading-none text-primary-foreground transition-opacity hover:opacity-90 disabled:pointer-events-none disabled:opacity-40"
409427 >
0 commit comments