fix: stream validated rectification replies
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user