From 0329f48ba796b138008c63be821d85daf0971869 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Wed, 22 Jul 2026 11:49:08 +0800 Subject: [PATCH 1/5] fix: continue questions after range rectification --- docs/BUG_HISTORY.md | 16 ++++++++++++++++ frontend/src/app/api/consult/route.ts | 2 +- .../tests/consultation-birth-time-mode.test.ts | 13 +++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 734c26e3..aaedfb88 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -297,3 +297,19 @@ - 相关记录:BUG-004、BUG-008、ERR-091 - 复发自:无 - 修复版本:待提交(本地可测) + +## BUG-017 | 候选范围终态继续原问题必定返回 409 + +- 状态:resolved +- 首次发现:2026-07-22 +- 最近更新:2026-07-22 +- 影响面:生时校正范围终态、`POST /api/consult`、durable 原问题交接 +- 用户现象:生时校正安全结束并保存候选范围后,点击“继续原问题”仍提示原问题状态变化,接口返回 409。 +- 触发条件:分钟确认发布门禁未通过,校正以 `candidate.status=pending_validation` 完成;页面按未确认分钟选择 `unverified_birth_time` 并携带 durable handoff。 +- 根因:页面正确区分确认分钟与保存范围,但咨询接口把所有 handoff 硬限制为 `verified_chart`;范围终态因此在计费和模型调用前被拒绝。 +- 修复:handoff 允许 `verified_chart` 与 `unverified_birth_time` 两种星盘模式,继续拒绝 `general_no_birth_time`、旧校正入口和不一致 request ID;分钟确认门禁保持不变。 +- 验证:生产合成账号复现 `409 mode_changed`;`frontend/tests/consultation-birth-time-mode.test.ts` 锁定范围终态模式与一般咨询隔离;发布后需复跑 durable claim、正常咨询和聊天删除 smoke。 +- 防复发:范围终态和精确确认必须分别覆盖 continuation 模式;不得把“拥有 handoff”错误等同于“已经确认出生分钟”。 +- 相关记录:BUG-008、BUG-009、BUG-016、ERR-085、ERR-091 +- 复发自:无 +- 修复版本:待提交(生产 smoke 待当前精确 SHA 发布后补齐) diff --git a/frontend/src/app/api/consult/route.ts b/frontend/src/app/api/consult/route.ts index 1918e1ff..9a20d8b3 100644 --- a/frontend/src/app/api/consult/route.ts +++ b/frontend/src/app/api/consult/route.ts @@ -200,7 +200,7 @@ export async function POST(request: Request) { } if (handoff) { - if (parsed.data.consultationMode !== "verified_chart" + if (!["verified_chart", "unverified_birth_time"].includes(parsed.data.consultationMode) || parsed.data.entrypoint !== undefined || requestId !== handoff.requestId) { return NextResponse.json( diff --git a/frontend/tests/consultation-birth-time-mode.test.ts b/frontend/tests/consultation-birth-time-mode.test.ts index cdb12da5..33e0fb23 100644 --- a/frontend/tests/consultation-birth-time-mode.test.ts +++ b/frontend/tests/consultation-birth-time-mode.test.ts @@ -126,3 +126,16 @@ test("homepage sends explicit modes and never routes an unverified minute throug assert.match(route, /旧版生时校正入口已停用/); assert.ok(route.indexOf("旧版生时校正入口已停用") < route.indexOf("reserveConsultationModel(", route.indexOf("chatRequestSchema.safeParse"))); }); + +test("range-terminal rectification handoff accepts unverified chart mode without weakening general-mode isolation", () => { + const route = readFileSync(new URL("../src/app/api/consult/route.ts", import.meta.url), "utf8"); + const handoffGuard = route.slice( + route.indexOf("if (handoff)"), + route.indexOf("try {", route.indexOf("if (handoff)")), + ); + + assert.match(handoffGuard, /\["verified_chart", "unverified_birth_time"\]\.includes\(parsed\.data\.consultationMode\)/); + assert.doesNotMatch(handoffGuard, /consultationMode !== "verified_chart"/); + assert.doesNotMatch(handoffGuard, /general_no_birth_time/); + assert.match(handoffGuard, /requestId !== handoff\.requestId/); +}); From c07eab2941f8e6e3a6f1dcd2e35f6371e9c9a0bd Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Wed, 22 Jul 2026 15:51:54 +0800 Subject: [PATCH 2/5] feat: show agent activity and simplify starter view --- frontend/package-lock.json | 11 ++++++++ frontend/package.json | 1 + frontend/src/app/globals.css | 15 +++-------- frontend/src/app/page.tsx | 12 --------- .../src/components/agent-activity-status.tsx | 25 +++++++++++++++++++ frontend/src/components/chat-message-row.tsx | 8 ++++-- .../src/components/parameter-freeze-panel.tsx | 23 ----------------- frontend/tests/chat-stream-layout.test.ts | 10 ++++++++ frontend/tests/parameter-freeze-panel.test.ts | 17 +++++-------- 9 files changed, 62 insertions(+), 60 deletions(-) create mode 100644 frontend/src/components/agent-activity-status.tsx delete mode 100644 frontend/src/components/parameter-freeze-panel.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 052b5920..ac5cbc6a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -29,6 +29,7 @@ "sonner": "^2.0.7", "tailwind-merge": "^3.6.0", "tailwindcss": "^4.3.2", + "thinking-orbs": "^0.1.1", "tw-animate-css": "^1.4.0", "zod": "^3.25.76" }, @@ -10834,6 +10835,16 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/thinking-orbs": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/thinking-orbs/-/thinking-orbs-0.1.1.tgz", + "integrity": "sha512-nLvLTGJtk74K13MmP7XiRdzFiQx7UsoZAIyBZNgXyl7Q3h2mdz0r3eKn9LCsuzb3DGOVjwDvMhtnAkIqJJRVQA==", + "license": "MIT", + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", diff --git a/frontend/package.json b/frontend/package.json index 090aca40..51eb115a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -37,6 +37,7 @@ "sonner": "^2.0.7", "tailwind-merge": "^3.6.0", "tailwindcss": "^4.3.2", + "thinking-orbs": "^0.1.1", "tw-animate-css": "^1.4.0", "zod": "^3.25.76" }, diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index e665fb55..5cb021e2 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -164,9 +164,9 @@ button:disabled { cursor: default; opacity: .45; } .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; } -.thinking { height: 24px; display: flex; align-items: center; gap: 5px; } -.thinking i:nth-child(2) { animation-delay: 100ms; } -.thinking i:nth-child(3) { animation-delay: 200ms; } +.agent-activity-status { min-height: 24px; display: flex; align-items: center; gap: var(--space-2); color: var(--color-ink-secondary); font-size: var(--type-body-sm); line-height: 1.5; } +.agent-activity-status canvas { flex: 0 0 auto; } +.agent-activity-status + .message-markdown { margin-top: var(--space-2); } .composer-suggestions::-webkit-scrollbar { display: none; } .composer textarea::placeholder { color: var(--color-ink-tertiary); } .composer button svg { width: 19px; height: 19px; } @@ -542,14 +542,6 @@ button:disabled { cursor: default; opacity: .45; } .product-entrypoint-action .starter-arrow { width: 15px; height: 15px; color: currentColor; } .starter-loading { color: var(--color-ink-secondary); margin-left: 0; padding: var(--space-5); border-radius: var(--radius-lg); background: var(--color-canvas-muted); font-size: 14px; } .starter-note { margin: 10px 0 0; color: var(--color-ink-secondary); line-height: 1.5; grid-column: 1 / -1; font-size: 13px; } -.parameter-freeze-panel { display: grid; grid-column: 1 / -1; gap: var(--space-3); padding: var(--space-4); border: 1px solid var(--color-border); border-radius: var(--radius-lg); background: var(--color-canvas); } -.parameter-freeze-panel > div { display: flex; flex-wrap: wrap; justify-content: space-between; gap: var(--space-2); color: var(--color-ink-secondary); font-size: var(--type-caption); } -.parameter-freeze-panel b { color: var(--color-ink); font-size: var(--type-title-sm); } -.parameter-freeze-panel dl { display: grid; grid-template-columns: repeat(auto-fit, minmax(132px, 1fr)); gap: var(--space-2); margin: 0; } -.parameter-freeze-panel dl > div { min-width: 0; padding: var(--space-2); border-radius: var(--radius-md); background: var(--color-canvas-soft); } -.parameter-freeze-panel dt { color: var(--color-ink-tertiary); font-size: 11px; } -.parameter-freeze-panel dd { overflow: hidden; margin: 2px 0 0; color: var(--color-ink); text-overflow: ellipsis; white-space: nowrap; font-size: 12px; } - .message-list { margin: 0 auto; width: min(900px, 100%); padding: var(--space-8) var(--space-8) var(--space-16); } .message { display: flex; animation: message-enter 160ms var(--ease-out) both; padding: var(--space-2) 0; } .agent-avatar { width: 32px; height: 32px; display: block; flex: 0 0 32px; margin-top: var(--space-2); border-radius: 50%; background: var(--color-canvas) url("/jyotish-logo.png") center / contain no-repeat; box-shadow: 0 0 0 1px var(--color-border); } @@ -565,7 +557,6 @@ button:disabled { cursor: default; opacity: .45; } .message-markdown strong { color: var(--color-ink); font-weight: 500; } .message-markdown a { text-underline-offset: 3px; color: var(--color-action); } .message-markdown code { padding: 2px 5px; font-size: .9em; border-radius: var(--radius-xs); background: var(--color-canvas-muted); font-family: var(--font-mono); } -.thinking i { width: 5px; height: 5px; border-radius: 50%; animation: pulse 850ms ease-in-out infinite alternate; background: var(--color-action); } .error-message { margin: 12px 0 0; padding: 12px 14px; border-left: 3px solid var(--color-danger); background: var(--color-danger-muted); color: var(--color-danger); font-size: 13px; line-height: 1.6; border-color: var(--color-danger); border-radius: 0 var(--radius-md) var(--radius-md) 0; } .composer-wrap { position: sticky; bottom: 0; z-index: 2; min-width: 0; border-top: 1px solid color-mix(in srgb, var(--color-border) 70%, transparent); padding: var(--space-3) var(--space-6) var(--space-4); background: var(--color-frosted); backdrop-filter: saturate(130%) blur(20px); } diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 8ddbd0ce..21d5607c 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -15,7 +15,6 @@ import { ConversationalBirthTimeRectification } from "@/components/conversationa import { ChatMessageContent } from "@/components/chat-message-content"; import { AgentAvatar, ChatMessageRow } from "@/components/chat-message-row"; import { ModelSelector } from "@/components/model-selector"; -import { ParameterFreezePanel, type ParameterFreezeRow } from "@/components/parameter-freeze-panel"; import { Button } from "@/components/ui/button"; import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"; import { Textarea } from "@/components/ui/textarea"; @@ -976,16 +975,6 @@ export default function Home() { const profileComplete = isProfileComplete(profile); const birthTimeDisplay = birthTimeDisplayState(profile); - const profileBirthPlace = selectedBirthPlace(profile); - const parameterFreezeRows: ParameterFreezeRow[] = profileComplete && profileBirthPlace ? [ - { label: "出生时间", value: `${profile.date} ${profile.time || "未定"}` }, - { label: "出生地点", value: profileBirthPlace.label }, - { label: "经纬度", value: `${profileBirthPlace.lat.toFixed(4)}, ${profileBirthPlace.lon.toFixed(4)}` }, - { label: "时区", value: `UTC+${profileBirthPlace.tz}` }, - { label: "Ayanamsa", value: "Lahiri / Sidereal" }, - { label: "Node mode", value: "True Node" }, - { label: "出生时间精度", value: birthTimeDisplay?.kind === "candidate" ? "候选时间" : birthTimeDisplay?.kind === "confirmed" ? "已确认" : "待校正" }, - ] : []; const dailyStarlanguage = dailyStarlanguageCard ?? (profileComplete ? buildDailyStarlanguageCard(profile) : null); const onboardingPending = profileComplete && !onboarding && !onboardingError; const currentOnboardingMessage = onboardingJustCompleted @@ -2915,7 +2904,6 @@ export default function Home() {
正在准备三个入门问题…
) : (
- {parameterFreezeRows.length > 0 && }
diff --git a/frontend/src/components/parameter-freeze-panel.tsx b/frontend/src/components/parameter-freeze-panel.tsx deleted file mode 100644 index 7069b0d9..00000000 --- a/frontend/src/components/parameter-freeze-panel.tsx +++ /dev/null @@ -1,23 +0,0 @@ -export type ParameterFreezeRow = { - label: string; - value: string; -}; - -export function ParameterFreezePanel({ rows }: { readonly rows: readonly ParameterFreezeRow[] }) { - return ( -
-
- 当前排盘参数 - 这些参数会随咨询一起送入证据链。 -
-
- {rows.map((row) => ( -
-
{row.label}
-
{row.value}
-
- ))} -
-
- ); -} diff --git a/frontend/tests/chat-stream-layout.test.ts b/frontend/tests/chat-stream-layout.test.ts index 00e8cc80..995c9255 100644 --- a/frontend/tests/chat-stream-layout.test.ts +++ b/frontend/tests/chat-stream-layout.test.ts @@ -6,6 +6,8 @@ import { chatMessageViews } from "../src/lib/chat-message-view.ts"; const pageSource = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8"); const globalStyles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8"); +const messageRowSource = readFileSync(new URL("../src/components/chat-message-row.tsx", import.meta.url), "utf8"); +const activitySource = readFileSync(new URL("../src/components/agent-activity-status.tsx", import.meta.url), "utf8"); const previousMessages = [ { role: "user", text: "问题" }, @@ -42,6 +44,14 @@ test("does not duplicate a completed assistant answer while loading state settle assert.equal(views.at(-1)?.state, "settled"); }); +test("shows honest agent activity states before and during streamed text", () => { + assert.match(messageRowSource, /state="working" label="正在核对星盘信息…"/); + assert.match(messageRowSource, /message\.state === "streaming" &&