fix: stream validated rectification replies

This commit is contained in:
Jesse_Chen
2026-07-23 15:45:42 +08:00
parent 0850619eaf
commit 73cafeb6e0
11 changed files with 348 additions and 26 deletions
@@ -3,6 +3,7 @@ import path from "node:path";
import {
conversationalRectificationCommandSchema,
type ConversationalRectificationCommand,
type ConversationalRectificationResponse,
type ConversationalRectificationTurn,
} from "../../../lib/conversational-rectification/contracts.ts";
import {
@@ -42,6 +43,53 @@ import {
export const runtime = "nodejs";
export const maxDuration = 60;
const RECTIFICATION_STREAM_CONTENT_TYPE = "application/x-ndjson; charset=utf-8";
const RECTIFICATION_STREAM_CHUNK_SIZE = 10;
const RECTIFICATION_STREAM_CHUNK_DELAY_MS = 18;
function rectificationNarrativeChunks(narrative: string): string[] {
const characters = Array.from(narrative);
const chunks: string[] = [];
for (let index = 0; index < characters.length; index += RECTIFICATION_STREAM_CHUNK_SIZE) {
chunks.push(characters.slice(index, index + RECTIFICATION_STREAM_CHUNK_SIZE).join(""));
}
return chunks;
}
export function streamConversationalRectificationResponse(
turn: ConversationalRectificationResponse,
chunkDelayMs = RECTIFICATION_STREAM_CHUNK_DELAY_MS,
): Response {
const encoder = new TextEncoder();
let cancelled = false;
const body = new ReadableStream<Uint8Array>({
async start(controller) {
try {
for (const text of rectificationNarrativeChunks(turn.narrative)) {
if (cancelled) return;
controller.enqueue(encoder.encode(`${JSON.stringify({ type: "delta", text })}\n`));
if (chunkDelayMs > 0) await new Promise((resolve) => setTimeout(resolve, chunkDelayMs));
}
if (cancelled) return;
controller.enqueue(encoder.encode(`${JSON.stringify({ type: "turn", turn })}\n`));
controller.close();
} catch (error) {
if (!cancelled) controller.error(error);
}
},
cancel() {
cancelled = true;
},
});
return new Response(body, {
headers: {
"Cache-Control": "no-cache, no-transform",
"Content-Type": RECTIFICATION_STREAM_CONTENT_TYPE,
"X-Accel-Buffering": "no",
},
});
}
const jyotishSkillPath = process.env.JYOTISH_SKILL_PATH?.trim()
|| path.resolve(process.cwd(), "..", "skills", "jyotish-vedic-astrology");
@@ -892,7 +940,9 @@ export function createBirthTimeConversationPostHandler(
errorCategory: "none",
deploymentSha,
});
return Response.json(turn);
return request.headers.get("accept")?.includes("application/x-ndjson")
? streamConversationalRectificationResponse(turn)
: Response.json(turn);
} catch (error) {
const publicError = toConversationalRectificationPublicError(error);
const outcome = service ? conversationalRectificationTelemetryOutcome(service) : null;
+13
View File
@@ -822,6 +822,7 @@ export default function Home() {
const [rectificationSessionId, setRectificationSessionId] = useState<string | null>(null);
const [rectificationReturnSessionId, setRectificationReturnSessionId] = useState<string | null>(null);
const [rectificationInitialTurn, setRectificationInitialTurn] = useState<ConversationalRectificationResponse | null>(null);
const [rectificationOpeningAssistantText, setRectificationOpeningAssistantText] = useState("");
const [rectificationPendingQuestion, setRectificationPendingQuestion] = useState<string | null>(null);
const [rectificationLoading, setRectificationLoading] = useState(false);
const [rectificationMutationPending, setRectificationMutationPending] = useState(false);
@@ -1951,6 +1952,7 @@ export default function Home() {
setDraftEntrypoint(null);
setRectificationPendingQuestion(requestedQuestion);
setRectificationInitialTurn(null);
setRectificationOpeningAssistantText("");
setRectificationError("");
setRectificationLoading(true);
if (!reusingRectificationSession) {
@@ -1975,6 +1977,10 @@ export default function Home() {
type: "start",
actionId: globalThis.crypto.randomUUID(),
pendingConsultationQuestion: requestedQuestion,
}, {
onNarrativeDelta(text) {
setRectificationOpeningAssistantText((current) => current + text);
},
});
} else {
let current = resumeTarget;
@@ -2007,6 +2013,10 @@ export default function Home() {
caseId: current.caseId,
actionId: globalThis.crypto.randomUUID(),
turnVersion: current.turnVersion,
}, {
onNarrativeDelta(text) {
setRectificationOpeningAssistantText((value) => value + text);
},
});
}
}
@@ -2031,6 +2041,7 @@ export default function Home() {
}
}
setRectificationInitialTurn(turn);
setRectificationOpeningAssistantText("");
synchronizeRectificationQuestion(turn, sourceSession);
setComposerNotice(sessionSyncFailed ? "校正已经开始,但会话关联暂时未同步到云端。" : "");
} catch (caught) {
@@ -2038,6 +2049,7 @@ export default function Home() {
? caught.message
: "生时校正暂时无法继续,请稍后重试。";
setRectificationError(message);
setRectificationOpeningAssistantText("");
setComposerNotice(message);
if (!reusingRectificationSession) {
setSessions((current) => current.filter((session) => session.id !== rectificationSession.id));
@@ -3084,6 +3096,7 @@ export default function Home() {
) : (
<ConversationalBirthTimeRectification
initialTurn={visibleRectificationTurn}
openingAssistantText={rectificationOpeningAssistantText}
pendingConsultationQuestion={rectificationPendingQuestion}
continuationPending={rectificationContinuationPending}
onPendingChange={setRectificationMutationPending}