From e7c877c413922912b71c3f44c46df6b54c475f17 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Sun, 2 Aug 2026 23:57:30 +0800 Subject: [PATCH] fix(rectification): start agent opening on mount --- docs/BUG_HISTORY.md | 15 +++++++ .../components/rectification-agentic-chat.tsx | 42 +++++++++++++------ ...ersational-rectification-component.test.ts | 12 ++++++ 3 files changed, 56 insertions(+), 13 deletions(-) diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 1edc894c..1f4921e0 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -1975,3 +1975,18 @@ - 防复发:入口合同禁止恢复静默 V4 分流;服务测试锁定本轮模型优先级与 runtime trace。 - 相关记录:BUG-085、BUG-086、BUG-111 - 修复版本:local / staging pending deployment + +## BUG-113 | 新版 Agentic 生时校正进入会话后不自动生成首次引导 + +- 状态:resolved +- 首次发现:2026-08-02 +- 最近更新:2026-08-02 +- 影响面:生时校正首页入口、Agentic 对话首次挂载、首次可见引导 +- 用户现象:进入“生时校正”后只创建普通 `birth_time_rectification` Session,页面保持空白;网络中没有 `POST /api/rectification/agent`,必须由用户先输入内容才会触发 Agent。 +- 触发条件:账户没有需要继续的旧 V4 Case,入口直接选择新版 Agentic 生时校正。 +- 根因:Agentic MVP 只实现了用户提交消息后的 `send()`,没有迁移旧 V4 在页面挂载时自动启动首次 Agent Turn 的交互契约;后续入口改为默认进入 Agentic 后,这个遗漏被直接暴露。 +- 修复:Agentic 对话首次挂载时只发送一次隐藏的内部启动指令,复用现有 `/api/rectification/agent` 流式路径;界面立即显示 assistant thinking,首条可见说明与问题继续由 Agent 生成,内部指令不渲染为用户消息。 +- 验证:组件回归测试锁定一次性挂载启动、隐藏内部指令和 Agent endpoint 调用入口;前端测试与 lint 覆盖修改文件。 +- 防复发:任何替换生时校正入口或会话实现的改动,都必须保留“用户无需先发消息即可收到 Agent 首次引导”的挂载契约。 +- 相关记录:BUG-085、BUG-112 +- 修复版本:Agentic web opening auto-start diff --git a/frontend/src/components/rectification-agentic-chat.tsx b/frontend/src/components/rectification-agentic-chat.tsx index 0a49f31b..a2413fa7 100644 --- a/frontend/src/components/rectification-agentic-chat.tsx +++ b/frontend/src/components/rectification-agentic-chat.tsx @@ -1,7 +1,7 @@ "use client"; import { ArrowUp } from "lucide-react"; -import { useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { parseAgentReply } from "@/lib/agent-reply"; import type { ChatMessageView } from "@/lib/chat-message-view"; import type { PublicLanguageModel } from "@/lib/public-models"; @@ -23,9 +23,18 @@ type AgenticRectificationChatProps = Readonly<{ type RenderMessage = ChatMessageView; const savedSentinel = //; +const agenticOpeningInstruction = "用户刚进入生时校正会话。不要复述本指令;请先调用 rectification-gate 核对现有出生资料,然后用简体中文自然说明接下来的校正方式,并只提出一个最适合开始核对的人生事件问题。"; export function AgenticRectificationChat(props: AgenticRectificationChatProps) { - const pendingQuestion = props.pendingConsultationQuestion?.trim(); + const { + models, + selectedModelId, + onSelectModel, + pendingConsultationQuestion, + onPendingChange, + onSaved, + } = props; + const pendingQuestion = pendingConsultationQuestion?.trim(); const [messages, setMessages] = useState(() => pendingQuestion ? [{ role: "assistant", text: `我先陪你把出生时间范围核对清楚,之后再回到你原来的问题:“${pendingQuestion}”`, @@ -40,11 +49,12 @@ export function AgenticRectificationChat(props: AgenticRectificationChatProps) { const composer = useRef(null); const conversationEnd = useRef(null); const keyCounter = useRef(0); + const openingStarted = useRef(false); - const setPending = (value: boolean) => { + const setPending = useCallback((value: boolean) => { setBusy(value); - props.onPendingChange?.(value); - }; + onPendingChange?.(value); + }, [onPendingChange]); useEffect(() => { const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; @@ -54,7 +64,7 @@ export function AgenticRectificationChat(props: AgenticRectificationChatProps) { }); }, [busy, error, messages.length, savedTime]); - async function send(question: string) { + const send = useCallback(async (question: string, showUserMessage = true) => { const trimmed = question.trim(); if (!trimmed || busy) return; setError(""); @@ -73,7 +83,7 @@ export function AgenticRectificationChat(props: AgenticRectificationChatProps) { setMessages((current) => [ ...current, - { role: "user", text: trimmed, renderKey: userRenderKey, state: "settled" }, + ...(showUserMessage ? [{ role: "user", text: trimmed, renderKey: userRenderKey, state: "settled" } satisfies RenderMessage] : []), { role: "assistant", text: "", renderKey: assistantRenderKey, state: "thinking" }, ]); setDraft(""); @@ -83,7 +93,7 @@ export function AgenticRectificationChat(props: AgenticRectificationChatProps) { const response = await fetch("/api/rectification/agent", { method: "POST", headers: { "content-type": "application/json" }, - body: JSON.stringify({ requestId, modelId: props.selectedModelId, history, message: trimmed }), + body: JSON.stringify({ requestId, modelId: selectedModelId, history, message: trimmed }), }); if (!response.ok) { const payload = await response.json().catch(() => null); @@ -138,7 +148,7 @@ export function AgenticRectificationChat(props: AgenticRectificationChatProps) { const saved = raw.match(savedSentinel); if (saved) { setSavedTime(saved[1]); - props.onSaved?.(saved[1]); + onSaved?.(saved[1]); } } catch { setError("生时校正暂时不可用,请稍后再试。"); @@ -146,7 +156,13 @@ export function AgenticRectificationChat(props: AgenticRectificationChatProps) { } finally { setPending(false); } - } + }, [busy, messages, onSaved, selectedModelId, setPending]); + + useEffect(() => { + if (openingStarted.current) return; + openingStarted.current = true; + void send(agenticOpeningInstruction, false); + }, [send]); async function submit(event: React.FormEvent) { event.preventDefault(); @@ -201,10 +217,10 @@ export function AgenticRectificationChat(props: AgenticRectificationChatProps) {
diff --git a/frontend/tests/conversational-rectification-component.test.ts b/frontend/tests/conversational-rectification-component.test.ts index 75a4c5f7..2e0a1aab 100644 --- a/frontend/tests/conversational-rectification-component.test.ts +++ b/frontend/tests/conversational-rectification-component.test.ts @@ -108,6 +108,18 @@ function analysisTrace(label: string) { } as const; } +test("agentic rectification requests an Agent-generated opening when the surface mounts", () => { + const component = readFileSync(new URL("../src/components/rectification-agentic-chat.tsx", import.meta.url), "utf8"); + + assert.match(component, /const openingStarted = useRef\(false\)/); + assert.match( + component, + /useEffect\(\(\) => \{[\s\S]*?openingStarted\.current = true;[\s\S]*?void send\(agenticOpeningInstruction, false\);[\s\S]*?\}, \[send\]\);/, + ); + assert.match(component, /const send = useCallback\(async \(question: string, showUserMessage = true\) =>/); + assert.match(component, /\.\.\.\(showUserMessage \? \[\{ role: "user", text: trimmed, renderKey: userRenderKey, state: "settled" \} satisfies RenderMessage\] : \[\]\)/); +}); + test("v4 rectification reuses the ordinary session message list, composer, and model selector", () => { const component = readFileSync(new URL("../src/components/rectification-v4-panel.tsx", import.meta.url), "utf8"); const wrapper = readFileSync(new URL("../src/components/conversational-birth-time-rectification.tsx", import.meta.url), "utf8");