fix rectification session persistence
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { parseAgentReply } from "@/lib/agent-reply";
|
||||
import type { ChatMessage } from "@/lib/chat-message-view";
|
||||
import { getAgenticRectificationAgent } from "@/mastra/agentic-rectification";
|
||||
import { defaultLanguageModel, resolveLanguageModel } from "@/mastra/model";
|
||||
import { blocksPromptExtraction } from "@/lib/consult-safety";
|
||||
@@ -17,6 +19,7 @@ export const maxDuration = 120;
|
||||
|
||||
const agenticRectificationRequestFields = {
|
||||
requestId: z.string().uuid(),
|
||||
sessionId: z.string().uuid(),
|
||||
modelId: z.string().trim().min(1).max(64).optional(),
|
||||
name: z.string().trim().max(80).optional().default(""),
|
||||
history: z
|
||||
@@ -45,6 +48,22 @@ const agenticRectificationRequestSchema = z.discriminatedUnion("action", [
|
||||
const openingContext = "The user opened birth-time rectification. Begin the session now: run the required gate, briefly explain the evidence-based process in Simplified Chinese, and ask exactly one natural question about the most useful dated life event. Do not mention this server event.";
|
||||
const agenticRectificationMaxSteps = 8;
|
||||
|
||||
function readPersistedMessages(value: unknown): ChatMessage[] {
|
||||
if (!Array.isArray(value)) return [];
|
||||
return value.flatMap((item): ChatMessage[] => {
|
||||
if (!item || typeof item !== "object") return [];
|
||||
const message = item as Partial<ChatMessage>;
|
||||
if ((message.role !== "user" && message.role !== "assistant") || typeof message.text !== "string") return [];
|
||||
return [{
|
||||
role: message.role,
|
||||
text: message.text.slice(0, 100_000),
|
||||
...(Array.isArray(message.suggestions)
|
||||
? { suggestions: message.suggestions.filter((suggestion): suggestion is string => typeof suggestion === "string").slice(0, 3) }
|
||||
: {}),
|
||||
}];
|
||||
});
|
||||
}
|
||||
|
||||
function currentTimeContext(now = new Date()) {
|
||||
const chinaTime = new Date(now.getTime() + 8 * 60 * 60 * 1000)
|
||||
.toISOString()
|
||||
@@ -127,6 +146,32 @@ export async function POST(request: Request) {
|
||||
const requestId = parsed.data.requestId;
|
||||
const requestTime = new Date();
|
||||
|
||||
const { data: chatSession, error: chatSessionError } = await supabase
|
||||
.from("chat_sessions")
|
||||
.select("id,messages,session_type")
|
||||
.eq("id", parsed.data.sessionId)
|
||||
.eq("user_id", userId)
|
||||
.maybeSingle();
|
||||
if (chatSessionError) {
|
||||
return NextResponse.json(
|
||||
{ error: "暂时无法读取生时校正会话", message: "请稍后重试。" },
|
||||
{ status: 503 },
|
||||
);
|
||||
}
|
||||
if (!chatSession || chatSession.session_type !== "birth_time_rectification") {
|
||||
return NextResponse.json(
|
||||
{ error: "生时校正会话不存在", message: "请重新进入生时校正。" },
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
const persistedMessages = readPersistedMessages(chatSession.messages);
|
||||
if (parsed.data.action === "opening" && persistedMessages.length > 0) {
|
||||
return NextResponse.json(
|
||||
{ code: "opening_already_started", error: "生时校正已开始", message: "已有校正记录,无需重复生成首次引导。" },
|
||||
{ status: 409 },
|
||||
);
|
||||
}
|
||||
|
||||
let profile;
|
||||
try {
|
||||
profile = await loadAgenticRectificationProfile(accounting, userId);
|
||||
@@ -196,6 +241,7 @@ export async function POST(request: Request) {
|
||||
const body = new ReadableStream<Uint8Array>({
|
||||
async start(controller) {
|
||||
let emitted = false;
|
||||
let raw = "";
|
||||
let settled = false;
|
||||
const settle = async (complete: boolean) => {
|
||||
if (settled) return;
|
||||
@@ -233,6 +279,7 @@ export async function POST(request: Request) {
|
||||
);
|
||||
for await (const chunk of result.textStream) {
|
||||
if (/\S/.test(chunk)) emitted = true;
|
||||
raw += chunk;
|
||||
send({ type: "delta", text: chunk });
|
||||
}
|
||||
void recordModelUsage(
|
||||
@@ -242,13 +289,37 @@ export async function POST(request: Request) {
|
||||
selectedModel.id,
|
||||
result.totalUsage,
|
||||
);
|
||||
if (!emitted) {
|
||||
const reply = parseAgentReply(raw, "general");
|
||||
if (!emitted || !reply.text) {
|
||||
console.warn(`[agentic-rectification] empty response request=${requestId}`);
|
||||
send({ type: "error", message: "生时校正没有生成有效回复,本次不会扣除点数,请重新发送。" });
|
||||
await settle(false);
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
const requestHistory = parsed.data.history.map((message) => ({
|
||||
role: message.role,
|
||||
text: message.text,
|
||||
} satisfies ChatMessage));
|
||||
const baseMessages = requestHistory.length > persistedMessages.length
|
||||
? requestHistory
|
||||
: persistedMessages;
|
||||
const nextMessages: ChatMessage[] = [
|
||||
...baseMessages,
|
||||
...(parsed.data.action === "message"
|
||||
? [{ role: "user" as const, text: parsed.data.message }]
|
||||
: []),
|
||||
{ role: "assistant" as const, text: reply.text, suggestions: reply.suggestions },
|
||||
].slice(-500);
|
||||
const { data: savedSession, error: saveError } = await supabase
|
||||
.from("chat_sessions")
|
||||
.update({ messages: nextMessages, updated_at: new Date().toISOString() })
|
||||
.eq("id", parsed.data.sessionId)
|
||||
.eq("user_id", userId)
|
||||
.eq("session_type", "birth_time_rectification")
|
||||
.select("id")
|
||||
.maybeSingle();
|
||||
if (saveError || !savedSession) throw new Error("RectificationSessionPersistenceError");
|
||||
send({ type: "done", emitted: true });
|
||||
await settle(true);
|
||||
controller.close();
|
||||
|
||||
@@ -2048,24 +2048,24 @@ export default function Home() {
|
||||
rectificationOpenInFlight.current = true;
|
||||
setRectificationLoading(true);
|
||||
setRectificationError("");
|
||||
setRectificationPendingQuestion(requestedQuestion);
|
||||
setDraft("");
|
||||
setDraftTheme(null);
|
||||
setDraftEntrypoint(null);
|
||||
setRectificationSessionId(rectificationSession.id);
|
||||
activeSessionIdRef.current = rectificationSession.id;
|
||||
setActiveSessionId(rectificationSession.id);
|
||||
|
||||
try {
|
||||
if (!existing) {
|
||||
setSessions((current) => [rectificationSession, ...current.filter((session) => session.id !== rectificationSession.id)]);
|
||||
await rectificationPersistence.current.enqueue(
|
||||
rectificationSession.id,
|
||||
() => persistSession(rectificationSession, "create"),
|
||||
);
|
||||
setSessions((current) => [rectificationSession, ...current.filter((session) => session.id !== rectificationSession.id)]);
|
||||
}
|
||||
setRectificationPendingQuestion(requestedQuestion);
|
||||
setDraft("");
|
||||
setDraftTheme(null);
|
||||
setDraftEntrypoint(null);
|
||||
setRectificationSessionId(rectificationSession.id);
|
||||
activeSessionIdRef.current = rectificationSession.id;
|
||||
setActiveSessionId(rectificationSession.id);
|
||||
} catch {
|
||||
setComposerNotice("生时校正已打开,但会话列表暂时未同步到云端。");
|
||||
setComposerNotice("生时校正会话暂时无法创建,请稍后重试。");
|
||||
} finally {
|
||||
rectificationOpenInFlight.current = false;
|
||||
setRectificationLoading(false);
|
||||
@@ -2091,6 +2091,15 @@ export default function Home() {
|
||||
void refreshAccount();
|
||||
}
|
||||
|
||||
function handleRectificationMessagesChange(messages: Message[]) {
|
||||
if (!rectificationSessionId) return;
|
||||
updateSession(rectificationSessionId, (session) => ({
|
||||
...session,
|
||||
messages,
|
||||
updatedAt: timestamp(),
|
||||
}));
|
||||
}
|
||||
|
||||
async function draftSynastryQuestionFromChart(record: ChartLibraryRecord, relationshipType: SynastryRelationshipType) {
|
||||
if (record.role !== "other") return;
|
||||
if (synastryPendingId) return;
|
||||
@@ -2959,9 +2968,14 @@ export default function Home() {
|
||||
|
||||
{rectificationSurfaceOpen && (
|
||||
<ConversationalBirthTimeRectification
|
||||
key={rectificationSessionId}
|
||||
sessionId={rectificationSessionId}
|
||||
initialMessages={activeSession?.messages ?? []}
|
||||
models={modelCatalog?.models ?? []}
|
||||
selectedModelId={activeSession?.modelId ?? ""}
|
||||
onSelectModel={(modelId) => void selectSessionModel(modelId)}
|
||||
onMessagesChange={handleRectificationMessagesChange}
|
||||
onCompleted={() => void refreshAccount()}
|
||||
pendingConsultationQuestion={rectificationPendingQuestion}
|
||||
onPendingChange={setRectificationMutationPending}
|
||||
onProfileIncomplete={handleRectificationProfileIncomplete}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import type { PublicLanguageModel } from "../lib/public-models.ts";
|
||||
import type { ChatMessage } from "../lib/chat-message-view.ts";
|
||||
import { AgenticRectificationChat } from "./rectification-agentic-chat.tsx";
|
||||
|
||||
export type ConversationalBirthTimeRectificationProps = Readonly<{
|
||||
sessionId: string;
|
||||
initialMessages: readonly ChatMessage[];
|
||||
models: readonly PublicLanguageModel[];
|
||||
selectedModelId: string;
|
||||
onSelectModel: (modelId: string) => void;
|
||||
onMessagesChange?: (messages: ChatMessage[]) => void;
|
||||
onCompleted?: () => void;
|
||||
pendingConsultationQuestion?: string | null;
|
||||
onPendingChange?: (pending: boolean) => void;
|
||||
onProfileIncomplete?: () => void;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { ArrowUp } from "lucide-react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { parseAgentReply } from "@/lib/agent-reply";
|
||||
import type { ChatMessageView } from "@/lib/chat-message-view";
|
||||
import type { ChatMessage, ChatMessageView } from "@/lib/chat-message-view";
|
||||
import type { PublicLanguageModel } from "@/lib/public-models";
|
||||
import { ChatMessageRow } from "./chat-message-row";
|
||||
import { ModelSelector } from "./model-selector";
|
||||
@@ -11,9 +11,13 @@ import { Button } from "./ui/button";
|
||||
import { Textarea } from "./ui/textarea";
|
||||
|
||||
type AgenticRectificationChatProps = Readonly<{
|
||||
sessionId: string;
|
||||
initialMessages: readonly ChatMessage[];
|
||||
models: readonly PublicLanguageModel[];
|
||||
selectedModelId: string;
|
||||
onSelectModel: (modelId: string) => void;
|
||||
onMessagesChange?: (messages: ChatMessage[]) => void;
|
||||
onCompleted?: () => void;
|
||||
pendingConsultationQuestion?: string | null;
|
||||
onPendingChange?: (pending: boolean) => void;
|
||||
onProfileIncomplete?: () => void;
|
||||
@@ -31,21 +35,32 @@ type AgenticRectificationRequest = Readonly<
|
||||
|
||||
export function AgenticRectificationChat(props: AgenticRectificationChatProps) {
|
||||
const {
|
||||
sessionId,
|
||||
initialMessages,
|
||||
models,
|
||||
selectedModelId,
|
||||
onSelectModel,
|
||||
onMessagesChange,
|
||||
onCompleted,
|
||||
pendingConsultationQuestion,
|
||||
onPendingChange,
|
||||
onProfileIncomplete,
|
||||
onSaved,
|
||||
} = props;
|
||||
const pendingQuestion = pendingConsultationQuestion?.trim();
|
||||
const [messages, setMessages] = useState<RenderMessage[]>(() => pendingQuestion ? [{
|
||||
role: "assistant",
|
||||
text: `我先陪你把出生时间范围核对清楚,之后再回到你原来的问题:“${pendingQuestion}”`,
|
||||
renderKey: "agentic-pending-consultation",
|
||||
state: "settled",
|
||||
}] : []);
|
||||
const [messages, setMessages] = useState<RenderMessage[]>(() => [
|
||||
...initialMessages.map((message, index) => ({
|
||||
...message,
|
||||
renderKey: `agentic-message-${index}`,
|
||||
state: "settled" as const,
|
||||
})),
|
||||
...(initialMessages.length === 0 && pendingQuestion ? [{
|
||||
role: "assistant" as const,
|
||||
text: `我先陪你把出生时间范围核对清楚,之后再回到你原来的问题:“${pendingQuestion}”`,
|
||||
renderKey: "agentic-pending-consultation",
|
||||
state: "settled" as const,
|
||||
}] : []),
|
||||
]);
|
||||
const [draft, setDraft] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
@@ -79,9 +94,14 @@ export function AgenticRectificationChat(props: AgenticRectificationChatProps) {
|
||||
|
||||
keyCounter.current += 1;
|
||||
const requestId = globalThis.crypto.randomUUID();
|
||||
const history = messages
|
||||
const settledMessages = messages
|
||||
.filter((message) => message.state === "settled")
|
||||
.map((message) => ({ role: message.role, text: message.text }));
|
||||
.map((message) => ({
|
||||
role: message.role,
|
||||
text: message.text,
|
||||
...(message.suggestions ? { suggestions: message.suggestions } : {}),
|
||||
}));
|
||||
const history = settledMessages.map((message) => ({ role: message.role, text: message.text }));
|
||||
const turnKey = keyCounter.current;
|
||||
const userRenderKey = `agentic-user-${turnKey}`;
|
||||
const assistantRenderKey = `agentic-assistant-${turnKey}`;
|
||||
@@ -102,6 +122,7 @@ export function AgenticRectificationChat(props: AgenticRectificationChatProps) {
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
requestId,
|
||||
sessionId,
|
||||
modelId: selectedModelId,
|
||||
history,
|
||||
action: request.action,
|
||||
@@ -111,6 +132,7 @@ export function AgenticRectificationChat(props: AgenticRectificationChatProps) {
|
||||
if (!response.ok) {
|
||||
const payload = await response.json().catch(() => null);
|
||||
const message = payload?.message || payload?.error || `请求失败(${response.status})`;
|
||||
setMessages((current) => current.filter((item) => item.renderKey !== assistantRenderKey));
|
||||
if (payload?.code === "profile_incomplete") {
|
||||
onProfileIncomplete?.();
|
||||
return;
|
||||
@@ -121,6 +143,7 @@ export function AgenticRectificationChat(props: AgenticRectificationChatProps) {
|
||||
return;
|
||||
}
|
||||
if (!response.body) {
|
||||
setMessages((current) => current.filter((message) => message.renderKey !== assistantRenderKey));
|
||||
setError("服务暂时不可用,请稍后再试。");
|
||||
return;
|
||||
}
|
||||
@@ -128,6 +151,8 @@ export function AgenticRectificationChat(props: AgenticRectificationChatProps) {
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
let completed = false;
|
||||
let streamFailed = false;
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
@@ -152,16 +177,30 @@ export function AgenticRectificationChat(props: AgenticRectificationChatProps) {
|
||||
const saved = raw.match(savedSentinel);
|
||||
if (saved) setSavedTime(saved[1]);
|
||||
} else if (event.type === "error") {
|
||||
streamFailed = true;
|
||||
setError(event.message || "生时校正暂时不可用,请稍后再试。");
|
||||
} else if (event.type === "done") {
|
||||
completed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const parsed = parseAgentReply(raw, "general");
|
||||
setMessages((current) => current.map((message) => message.renderKey === assistantRenderKey
|
||||
? { ...message, text: parsed.text, state: "settled" }
|
||||
: message));
|
||||
setSuggestions(parsed.suggestions);
|
||||
const succeeded = completed && !streamFailed && Boolean(parsed.text);
|
||||
setMessages((current) => succeeded
|
||||
? current.map((message) => message.renderKey === assistantRenderKey
|
||||
? { ...message, text: parsed.text, suggestions: parsed.suggestions, state: "settled" }
|
||||
: message)
|
||||
: current.filter((message) => message.renderKey !== assistantRenderKey));
|
||||
setSuggestions(succeeded ? parsed.suggestions : []);
|
||||
if (succeeded) {
|
||||
onMessagesChange?.([
|
||||
...settledMessages,
|
||||
...(request.action === "message" ? [{ role: "user" as const, text: trimmed }] : []),
|
||||
{ role: "assistant", text: parsed.text, suggestions: parsed.suggestions },
|
||||
]);
|
||||
onCompleted?.();
|
||||
}
|
||||
const saved = raw.match(savedSentinel);
|
||||
if (saved) {
|
||||
setSavedTime(saved[1]);
|
||||
@@ -173,13 +212,13 @@ export function AgenticRectificationChat(props: AgenticRectificationChatProps) {
|
||||
} finally {
|
||||
setPending(false);
|
||||
}
|
||||
}, [busy, messages, onProfileIncomplete, onSaved, selectedModelId, setPending]);
|
||||
}, [busy, messages, onCompleted, onMessagesChange, onProfileIncomplete, onSaved, selectedModelId, sessionId, setPending]);
|
||||
|
||||
useEffect(() => {
|
||||
if (openingStarted.current) return;
|
||||
if (initialMessages.length > 0 || openingStarted.current) return;
|
||||
openingStarted.current = true;
|
||||
void send({ action: "opening" }, false);
|
||||
}, [send]);
|
||||
}, [initialMessages.length, send]);
|
||||
|
||||
async function submit(event: React.FormEvent) {
|
||||
event.preventDefault();
|
||||
|
||||
Reference in New Issue
Block a user