164 lines
5.9 KiB
TypeScript
164 lines
5.9 KiB
TypeScript
import { type Router } from "express";
|
|
import { z } from "zod";
|
|
|
|
import { getLocalAgentContext } from "../context/localContext.js";
|
|
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 { type FrontendActionCoordinator } from "../frontendAction/coordinator.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;
|
|
frontendActionCoordinator: FrontendActionCoordinator;
|
|
};
|
|
|
|
const toSessionUiStateContext = (sessionId: string) => ({
|
|
sessionId,
|
|
});
|
|
|
|
export const registerChatAuxiliaryRoutes = (
|
|
chatRouter: Router,
|
|
{
|
|
activeRuns,
|
|
lastRunStatuses,
|
|
resultReferenceResolver,
|
|
sessionBridge,
|
|
sessionMetadataStore,
|
|
sessionUiStateStore,
|
|
frontendActionCoordinator,
|
|
}: RegisterAuxiliaryRoutesOptions,
|
|
) => {
|
|
chatRouter.get("/result-ref/:result_ref", async (req, res) => {
|
|
const resultRef = req.params.result_ref?.trim();
|
|
const context = getLocalAgentContext(req);
|
|
const clientSessionId = typeof req.query.session_id === "string" ? req.query.session_id.trim() : undefined;
|
|
const offset = Math.max(0, Number.parseInt(String(req.query.offset ?? "0"), 10) || 0);
|
|
const limit = Math.min(500, Math.max(1, Number.parseInt(String(req.query.limit ?? "100"), 10) || 100));
|
|
if (!resultRef || !clientSessionId) {
|
|
res.status(400).json({ message: "result_ref and session_id are required" });
|
|
return;
|
|
}
|
|
const result = await resultReferenceResolver.getFullAuthorized(resultRef, {
|
|
actorKey: toActorKey(context.userId), clientSessionId, projectId: context.projectId,
|
|
}, { expectedKind: RESULT_REFERENCE_KIND.serverApiPayload });
|
|
if (!result) {
|
|
res.status(404).json({ message: "result_ref not found" });
|
|
return;
|
|
}
|
|
const rows = Array.isArray(result.data)
|
|
? result.data
|
|
: result.data && typeof result.data === "object" && Array.isArray((result.data as { data?: unknown }).data)
|
|
? (result.data as { data: unknown[] }).data
|
|
: [result.data];
|
|
res.json({ ...result, data: rows.slice(offset, offset + limit), page: { offset, limit, total: rows.length, has_more: offset + limit < rows.length } });
|
|
});
|
|
|
|
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 {
|
|
frontendActionCoordinator.cancelSession(parsed.data.session_id);
|
|
const authContext = getLocalAgentContext(req);
|
|
const projectId = authContext.projectId;
|
|
const userId = authContext.userId;
|
|
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,
|
|
});
|
|
}
|
|
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,
|
|
});
|
|
}
|
|
});
|
|
};
|