fix(rectification): start agent opening on mount (#77)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 = /<!--AYANAM_RECTIFICATION_SAVED:(\d{2}:\d{2})-->/;
|
||||
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<RenderMessage[]>(() => pendingQuestion ? [{
|
||||
role: "assistant",
|
||||
text: `我先陪你把出生时间范围核对清楚,之后再回到你原来的问题:“${pendingQuestion}”`,
|
||||
@@ -40,11 +49,12 @@ export function AgenticRectificationChat(props: AgenticRectificationChatProps) {
|
||||
const composer = useRef<HTMLTextAreaElement>(null);
|
||||
const conversationEnd = useRef<HTMLDivElement>(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) {
|
||||
|
||||
<div className="composer-footer">
|
||||
<ModelSelector
|
||||
models={props.models}
|
||||
selectedModelId={props.selectedModelId}
|
||||
models={models}
|
||||
selectedModelId={selectedModelId}
|
||||
disabled={busy}
|
||||
onSelect={props.onSelectModel}
|
||||
onSelect={onSelectModel}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user