diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index dd25cb72..6e3a8818 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -4713,7 +4713,7 @@ - 防复发:`selection_allowed` 不得单独出示采用卡。卡片必须等本轮 `rectification-offer-candidates`。有 `next_followup` 时不得 offer。 - 相关记录:BUG-120、BUG-304、BUG-312 - 复发自:BUG-120(过早出示采用卡);BUG-312(用 `selectionAllowed` 当出示门) -- 修复版本:待提交 +- 修复版本:`a732ff4b` ## BUG-314 | 生时纠正宫位表比 Agent 正文更宽、更靠左 @@ -4729,4 +4729,20 @@ - 防复发:宫位表左缘和宽度必须跟 Agent 正文列一致;inset 只加在 snapshot 容器上。 - 相关记录:BUG-310、BUG-311 - 复发自:BUG-311(去掉消息缩进后未收回正文列) +- 修复版本:`a732ff4b` + +## BUG-315 | Docker `npm run build` 因候选卡消息可能为 undefined 类型检查失败 + +- 状态:resolved +- 首次发现:2026-08-20 +- 最近更新:2026-08-20 +- 影响面:`deploy/railway-web.Dockerfile` 的 `RUN npm run build`、生时纠正时间选择卡 +- 用户现象:staging publish 在 web 镜像构建失败。BuildKit 日志末尾是 `skill-package-registry.ts` 的 Import traces,真正失败是 `Failed to type check`:`latestOfferMessageKey` 可能为 `undefined`。 +- 触发条件:向 `staging` 推送后走 publish 镜像构建。push 上的 validate 跳过 `npm run build`,因此质量门绿、镜像红。 +- 根因:`showSelectionCards` 用 `Boolean(...)` 包了一层,TypeScript 不会因此收窄 `find()` 的 `T | undefined`。随后 `showSelectionCards ? latestOfferMessageKey.renderKey` 在 true 分支仍可能是 `undefined`,`next build` 类型检查退出 1。 +- 修复:变量改名为实际类型 `latestOfferMessage`;取 `renderKey` 时用 `showSelectionCards && latestOfferMessage` 收窄。 +- 验证:`./node_modules/.bin/tsc --noEmit` 无错误。`tsx --test tests/rectification-agentic-entry.test.ts` 锁定收窄写法。 +- 防复发:对 `find()` 结果取属性不得只靠外层 `Boolean(...)`;staging push 的 validate 不跑 `next build`,类型回归只在 publish Docker 暴露。 +- 相关记录:BUG-309、BUG-312、BUG-313 +- 复发自:BUG-309(validate 跳过 `next build`;日志末尾 Import traces 掩盖类型检查失败) - 修复版本:待提交 diff --git a/frontend/src/components/rectification-agentic-chat.tsx b/frontend/src/components/rectification-agentic-chat.tsx index 42665fba..a2c46c83 100644 --- a/frontend/src/components/rectification-agentic-chat.tsx +++ b/frontend/src/components/rectification-agentic-chat.tsx @@ -549,7 +549,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) { && Boolean(message.turnId) && Boolean(message.text) ))?.renderKey; - const latestOfferMessageKey = [...messages] + const latestOfferMessage = [...messages] .reverse() .find((message) => ( message.role === "assistant" @@ -559,12 +559,14 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) { )); const showSelectionCards = Boolean( candidateResult?.selectionAllowed - && latestOfferMessageKey - && turnOfferedSelection(latestOfferMessageKey) + && latestOfferMessage + && turnOfferedSelection(latestOfferMessage) && !busy && regeneratingMessageKey === null, ); - const selectionCardMessageKey = showSelectionCards ? latestOfferMessageKey.renderKey : undefined; + const selectionCardMessageKey = showSelectionCards && latestOfferMessage + ? latestOfferMessage.renderKey + : undefined; const canSend = !busy && !readonly && !regeneratingMessageKey; return ( diff --git a/frontend/tests/rectification-agentic-entry.test.ts b/frontend/tests/rectification-agentic-entry.test.ts index 7ac1d5c5..b0b2173d 100644 --- a/frontend/tests/rectification-agentic-entry.test.ts +++ b/frontend/tests/rectification-agentic-entry.test.ts @@ -367,7 +367,11 @@ test("time-selection cards appear under the latest settled agent bubble only aft assert.match(chat, /candidateResult\?\.selectionAllowed/); assert.match(chat, /turnOfferedSelection/); assert.match(chat, /rectification-offer-candidates/); - assert.match(chat, /showSelectionCards = Boolean\(\s*candidateResult\?\.selectionAllowed[\s\S]*turnOfferedSelection\(latestOfferMessageKey\)[\s\S]*!busy/); + assert.match(chat, /showSelectionCards = Boolean\(\s*candidateResult\?\.selectionAllowed[\s\S]*turnOfferedSelection\(latestOfferMessage\)[\s\S]*!busy/); + assert.match( + chat, + /selectionCardMessageKey = showSelectionCards && latestOfferMessage\s*\? latestOfferMessage\.renderKey\s*: undefined/, + ); assert.match(messageLoop, /showSelectionCards && message\.renderKey === selectionCardMessageKey/); assert.doesNotMatch(messageLoop, /className="rectification-snapshot"/); });