From f5c57efeba9968892cee251b1f1143ab1d53a893 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Wed, 22 Jul 2026 10:50:23 +0800 Subject: [PATCH] fix: restore birth-time rectification entry --- docs/BUG_HISTORY.md | 22 ++++- .../app/api/birth-time-conversation/route.ts | 87 ++++++++++++++----- frontend/src/app/globals.css | 1 - frontend/src/components/chat-message-row.tsx | 3 +- .../technical-packet.ts | 7 +- .../tests/consultation-report-export.test.ts | 11 ++- ...conversational-rectification-route.test.ts | 58 +++++++++++++ .../conversational-technical-packet.test.ts | 13 ++- 8 files changed, 164 insertions(+), 38 deletions(-) diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 8d7c88b0..734c26e3 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -208,11 +208,11 @@ - 首次发现:2026-07-22 - 最近更新:2026-07-22 - 影响面:普通问答消息、Agent 回答顶部区域 -- 用户现象:回答正文上方显示“证据状态:not-applicable”和“证据链摘要 · not-applicable”等工程审计信息。 +- 用户现象:回答正文上方显示“证据状态:not-applicable”和“证据链摘要 · not-applicable”等工程审计信息,正文下方还显示单条回答的报告下载控件。 - 触发条件:任意已完成的 Agent 回答,尤其是后端返回 `not-applicable` 时。 - 根因:消息行对每条非思考态 Agent 消息无条件渲染 claim boundary 与 Technique Audit Table;未识别状态又直接回退显示原始状态值。 -- 修复:从普通聊天消息行移除内部证据徽章与审计面板;证据状态和 workflow receipt 仍随消息保存并供内部约束及报告生成使用。 -- 验证:`frontend/tests/claim-boundary-badge.test.ts`、`frontend/tests/evidence-audit-panel.test.ts` 锁定聊天消息不再挂载两个内部组件。 +- 修复:从普通聊天消息行移除内部证据徽章、审计面板和单条回答报告下载控件;证据状态和 workflow receipt 仍随消息保存并供内部约束使用。 +- 验证:`frontend/tests/claim-boundary-badge.test.ts`、`frontend/tests/evidence-audit-panel.test.ts`、`frontend/tests/consultation-report-export.test.ts` 锁定聊天消息不再挂载这些内部组件。 - 防复发:聊天消息渲染契约禁止直接展示 `techniqueTruth` 和 `workflowReceipt`;需要运营或调试时使用独立的受控界面。 - 相关记录:BUG-009 - 复发自:无 @@ -281,3 +281,19 @@ - 相关记录:BUG-009、BUG-014、ERR-045、ERR-053、ERR-086 - 复发自:无 - 修复版本:d7d9703 + +## BUG-016 | 生时校正首轮被扫描稀疏度或候选分区超时打成 503 + +- 状态:resolved +- 首次发现:2026-07-22 +- 最近更新:2026-07-22 +- 影响面:首页“先完成生时校正”、`POST /api/birth-time-conversation` 首轮技术包 +- 用户现象:点击生时校正后仍停留在普通咨询页,并显示“生时校正暂时无法继续”;接口返回 503。 +- 触发条件:候选扫描包含真实但不连续的时点差异,或动态候选分区依赖在 45 秒内没有返回。 +- 根因:技术包只承认相邻分钟切换,错误拒绝了稀疏扫描中的真实范围差异;同时首轮把用于改善问题排序的候选分区依赖当成不可降级的硬依赖。生产版本 `b981c4e` 的日志分别记录了 `RectificationTechnicalPacketRangeError` 和 `candidate_differences TimeoutError`。 +- 修复:稀疏扫描差异改用明确的范围级文案,禁止伪称相邻分钟切换;超过六小时的宽范围首轮跳过分钟级候选分区,较窄范围在候选分区发生已知超时、取消、配置或服务故障时保留服务端扫描证据并继续收集,不再返回 503;未知编程或契约错误仍失败关闭。 +- 验证:`frontend/tests/conversational-technical-packet.test.ts`、`frontend/tests/conversational-rectification-route.test.ts`、真实 Chromium 390px 组件回归和 `npm run build` 通过;完整前端套件 844 项中 837 项通过,余下 7 项均为沙箱 Chromium / PostgreSQL 端口环境失败,针对性 Chromium 在沙箱外 8/8 通过。 +- 防复发:首轮必须区分核心扫描失败与可降级的候选排序依赖;稀疏差异文案必须显式否认相邻分钟切换;生产验收必须核对接口状态与实际进入校正会话。 +- 相关记录:BUG-004、BUG-008、ERR-091 +- 复发自:无 +- 修复版本:待提交(本地可测) diff --git a/frontend/src/app/api/birth-time-conversation/route.ts b/frontend/src/app/api/birth-time-conversation/route.ts index 53c66cfa..30d4299e 100644 --- a/frontend/src/app/api/birth-time-conversation/route.ts +++ b/frontend/src/app/api/birth-time-conversation/route.ts @@ -23,6 +23,7 @@ import { import type { RectificationNarrativeGenerator } from "../../../lib/conversational-rectification/narrative-agent.ts"; import type { BirthTimeJourneyEngine, RectificationQuestionnaire } from "../../../lib/birth-time-journey-service.ts"; import type { CandidateResult, LifeEvent } from "../../../lib/birth-time-evidence.ts"; +import type { CandidateDifferenceBuild } from "../../../lib/birth-time-dynamic-choice-internal.ts"; import { conversationalRectificationCreationPolicyFromEnvironment } from "../../../lib/conversational-rectification/creation-policy.ts"; import { conversationalRectificationLatencyBucket, @@ -449,6 +450,43 @@ function layerMetadata(scan: RectificationQuestionnaire, calculationVersion: str }; } +function unavailableCandidateDifferences( + caseId: string, + range: { readonly startTime: string; readonly endTime: string }, +): CandidateDifferenceBuild { + return { + packet: { + caseId, + scoringVersion: "birth-time-choice-scoring-v2", + currentRange: range, + opportunities: [], + askedQuestionFingerprints: [], + candidatePartitionFingerprints: [], + recentRangeHistory: [], + }, + candidateModel: { version: "birth-time-choice-scoring-v2" }, + scoringPartitions: {}, + }; +} + +function candidateDifferencesAreUnavailable(error: unknown): boolean { + return error instanceof Error && [ + "AbortError", + "TimeoutError", + "BirthTimeJourneyEngineError", + "BirthTimeJourneyEngineConfigurationError", + ].includes(error.name); +} + +function candidateDifferenceRangeIsTooWide( + range: { readonly startTime: string; readonly endTime: string }, +): boolean { + const start = minute(range.startTime); + let end = minute(range.endTime); + if (end < start) end += 1_440; + return end - start > 360; +} + function boundaryDistance(range: { readonly startTime: string; readonly endTime: string }, representative: string) { const start = minute(range.startTime); let end = minute(range.endTime); @@ -529,26 +567,35 @@ export async function buildProductionConversationalRectificationPacket( "merge_scans", () => mergeQuestionnaireScans(questionnaires, range), ); - const candidateDifferences = await rectificationPacketStage( - "candidate_differences", - () => engine.buildDifferencePacket({ - caseId: input.caseId, - asOfDate: input.asOfDate, - birthDate: input.declaredBirthInput.birthDate, - startTime: range.startTime, - endTime: range.endTime, - lat: latitude, - lon: longitude, - tz: place.timezoneOffset, - evidence: [], - events, - dismissedOpportunityIds: [], - questionFingerprints: [], - partitionFingerprints: [], - recentRanges: [], - candidateModel: null, - }), - ); + let candidateDifferences = unavailableCandidateDifferences(input.caseId, range); + if (!candidateDifferenceRangeIsTooWide(range)) { + try { + candidateDifferences = await rectificationPacketStage( + "candidate_differences", + () => engine.buildDifferencePacket({ + caseId: input.caseId, + asOfDate: input.asOfDate, + birthDate: input.declaredBirthInput.birthDate, + startTime: range.startTime, + endTime: range.endTime, + lat: latitude, + lon: longitude, + tz: place.timezoneOffset, + evidence: [], + events, + dismissedOpportunityIds: [], + questionFingerprints: [], + partitionFingerprints: [], + recentRanges: [], + candidateModel: null, + }), + ); + } catch (error) { + if (!candidateDifferencesAreUnavailable(error)) throw error; + // Candidate partitions improve question ranking, but evidence collection can still + // proceed from the server scan. Do not turn an optional slow dependency into a 503. + } + } const version = calculationVersion ? `${candidateDifferences.packet.scoringVersion}+${calculationVersion}` : candidateDifferences.packet.scoringVersion; diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 905d22de..e665fb55 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -161,7 +161,6 @@ button:disabled { cursor: default; opacity: .45; } .raw-evidence-receipt { margin: 0 var(--space-3) var(--space-3); border-top: 1px solid color-mix(in srgb, var(--color-border) 64%, transparent); padding-top: var(--space-2); } .raw-evidence-receipt summary { padding: var(--space-1) 0; color: var(--color-ink-tertiary); font-size: 12px; } .raw-evidence-receipt pre { max-height: 180px; overflow: auto; margin: var(--space-2) 0 0; padding: var(--space-3); border-radius: var(--radius-md); background: var(--color-canvas); color: var(--color-ink-secondary); font-size: 11px; line-height: 1.5; } -.report-download-button { margin-top: var(--space-3); border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-canvas-soft); color: var(--color-ink-secondary); cursor: pointer; padding: 7px 10px; font-size: 12px; } .message-markdown p { margin: 0 0 14px; } .message-markdown ul, .message-markdown ol { margin: 10px 0 16px; padding-left: 24px; } .message-markdown li { margin: 6px 0; } diff --git a/frontend/src/components/chat-message-row.tsx b/frontend/src/components/chat-message-row.tsx index 87a37719..5abd93c3 100644 --- a/frontend/src/components/chat-message-row.tsx +++ b/frontend/src/components/chat-message-row.tsx @@ -1,6 +1,5 @@ import { ChatMessageContent } from "@/components/chat-message-content"; import type { ChatMessageView } from "@/lib/chat-message-view"; -import { consultationReportMarkdown, downloadMarkdownReport } from "@/lib/consultation-report-export"; export function AgentAvatar() { return