fix(chat): handle question and todo state
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
import { type Router } from "express";
|
||||
import { z } from "zod";
|
||||
|
||||
import { type ChatSessionBridge } from "../chat/sessionBridge.js";
|
||||
import { logger } from "../logger.js";
|
||||
import { type ResultReferenceResolver } from "../results/resolver.js";
|
||||
import { RESULT_REFERENCE_KIND } from "../results/store.js";
|
||||
import { type SessionMetadataStore } from "../sessions/metadataStore.js";
|
||||
import { type SessionUiStateStore } from "../sessions/uiStateStore.js";
|
||||
import { toActorKey, toProjectKey } from "../utils/fileStore.js";
|
||||
import {
|
||||
type ActiveRun,
|
||||
type RunStatus,
|
||||
cancelBackendTodos,
|
||||
completeBackendProgress,
|
||||
updateLastAssistantMessage,
|
||||
} from "./chatUiState.js";
|
||||
|
||||
const abortPayloadSchema = z.object({
|
||||
session_id: z.string().max(128),
|
||||
});
|
||||
|
||||
type RegisterAuxiliaryRoutesOptions = {
|
||||
activeRuns: Map<string, ActiveRun>;
|
||||
lastRunStatuses: Map<string, RunStatus>;
|
||||
resultReferenceResolver: ResultReferenceResolver;
|
||||
sessionBridge: ChatSessionBridge;
|
||||
sessionMetadataStore: SessionMetadataStore;
|
||||
sessionUiStateStore: SessionUiStateStore;
|
||||
};
|
||||
|
||||
const toSessionUiStateContext = (sessionId: string) => ({
|
||||
sessionId,
|
||||
});
|
||||
|
||||
export const registerChatAuxiliaryRoutes = (
|
||||
chatRouter: Router,
|
||||
{
|
||||
activeRuns,
|
||||
lastRunStatuses,
|
||||
resultReferenceResolver,
|
||||
sessionBridge,
|
||||
sessionMetadataStore,
|
||||
sessionUiStateStore,
|
||||
}: RegisterAuxiliaryRoutesOptions,
|
||||
) => {
|
||||
chatRouter.get("/render-ref/:renderRef", async (req, res) => {
|
||||
const renderRef = req.params.renderRef?.trim();
|
||||
const userId = req.header("x-user-id")?.trim();
|
||||
const projectId = req.header("x-project-id") ?? undefined;
|
||||
const clientSessionId =
|
||||
typeof req.query.session_id === "string"
|
||||
? req.query.session_id.trim()
|
||||
: undefined;
|
||||
|
||||
if (!userId) {
|
||||
res.status(400).json({
|
||||
message: "x-user-id is required",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!renderRef) {
|
||||
res.status(400).json({
|
||||
message: "render_ref is required",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await resultReferenceResolver.getFullAuthorized(
|
||||
renderRef,
|
||||
{
|
||||
actorKey: toActorKey(userId),
|
||||
clientSessionId,
|
||||
projectId,
|
||||
},
|
||||
{
|
||||
expectedKind: RESULT_REFERENCE_KIND.renderJunctionsPayload,
|
||||
},
|
||||
);
|
||||
|
||||
if (!result) {
|
||||
res.status(404).json({ message: "render_ref not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json(result);
|
||||
});
|
||||
|
||||
chatRouter.post("/abort", async (req, res) => {
|
||||
const parsed = abortPayloadSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
res.status(400).json({
|
||||
message: "invalid request payload",
|
||||
detail: parsed.error.flatten(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const projectId = req.header("x-project-id") ?? undefined;
|
||||
const userId = req.header("x-user-id") ?? undefined;
|
||||
const actorKey = toActorKey(userId);
|
||||
const projectKey = toProjectKey(projectId);
|
||||
const sessionRecord = await sessionMetadataStore.get(
|
||||
{ actorKey, projectId, projectKey, userId },
|
||||
parsed.data.session_id,
|
||||
);
|
||||
const binding = sessionRecord
|
||||
? await sessionBridge.abort({
|
||||
clientSessionId: sessionRecord.sessionId,
|
||||
sessionId: sessionRecord.sessionId,
|
||||
})
|
||||
: null;
|
||||
const run = activeRuns.get(parsed.data.session_id);
|
||||
if (run && run.status === "running") {
|
||||
run.status = "aborted";
|
||||
lastRunStatuses.set(parsed.data.session_id, "aborted");
|
||||
run.controller.abort();
|
||||
run.messages = updateLastAssistantMessage(run.messages, (message) => ({
|
||||
...message,
|
||||
content:
|
||||
typeof message.content === "string" && message.content.trim()
|
||||
? message.content
|
||||
: "⚠️ **请求已中断**",
|
||||
isError: true,
|
||||
progress: completeBackendProgress(message.progress),
|
||||
todos: cancelBackendTodos(message.todos),
|
||||
}));
|
||||
if (sessionRecord) {
|
||||
const currentState = await sessionUiStateStore.read(
|
||||
toSessionUiStateContext(sessionRecord.sessionId),
|
||||
);
|
||||
await sessionUiStateStore.write(toSessionUiStateContext(sessionRecord.sessionId), {
|
||||
sessionId: sessionRecord.sessionId,
|
||||
isTitleManuallyEdited: currentState?.isTitleManuallyEdited ?? false,
|
||||
messages: run.messages,
|
||||
branchGroups: currentState?.branchGroups ?? [],
|
||||
});
|
||||
}
|
||||
for (const subscriber of run.subscribers) {
|
||||
subscriber.write("error", {
|
||||
session_id: parsed.data.session_id,
|
||||
message: "请求已中断",
|
||||
});
|
||||
subscriber.close();
|
||||
}
|
||||
run.subscribers.clear();
|
||||
}
|
||||
|
||||
if (!binding && !run) {
|
||||
res.status(204).end();
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(
|
||||
{
|
||||
clientSessionId: parsed.data.session_id,
|
||||
sessionId: binding?.sessionId ?? parsed.data.session_id,
|
||||
},
|
||||
"aborted chat session by client request",
|
||||
);
|
||||
res.status(202).json({
|
||||
session_id: parsed.data.session_id,
|
||||
aborted: true,
|
||||
});
|
||||
} catch (error) {
|
||||
const detail = error instanceof Error ? error.message : String(error);
|
||||
logger.error({ err: error }, "chat abort failed");
|
||||
res.status(500).json({
|
||||
message: "chat abort failed",
|
||||
detail,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user