Files
writing-agent/src/server/direct-chat-handlers.ts
T
chenmoranzhiandCursor 18f034bc2d 直聊边生成边把回复写入卡片并落盘,方便对照多个模型。
应用上下文后不再停在临时会话;卡片可拖动阅读、盲测时隐藏型号并打乱位置,用户气泡按正文收高。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-15 01:38:40 +08:00

613 lines
18 KiB
TypeScript

import { randomUUID } from "node:crypto";
import type { IncomingMessage, ServerResponse } from "node:http";
import {
createConversation,
createShell,
deleteConversation,
deleteShell,
EMPTY_SHELL_SLOTS,
getConversation,
getShell,
listConversations,
listShells,
promoteTemporary,
runDirectChatMultiTurn,
saveConversation,
updateConversation,
updateShell,
type DirectChatAssistantReply,
type DirectChatConversation,
type DirectChatMessage,
type DirectChatMultiTurnResult,
type DirectChatMultiWindowResult,
type DirectChatShellSlots,
type DirectChatStreamDelta,
type DirectChatTurn,
type DirectChatWindow,
} from "../direct-chat/index.js";
import { listProfilesForCompare } from "./model-compare.js";
async function readBody(req: IncomingMessage): Promise<string> {
const chunks: Buffer[] = [];
for await (const chunk of req) {
chunks.push(chunk as Buffer);
}
return Buffer.concat(chunks).toString("utf8");
}
function json(res: ServerResponse, status: number, data: unknown): void {
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
res.end(JSON.stringify(data));
}
function wantsNdjson(req: IncomingMessage): boolean {
return String(req.headers.accept || "").includes("application/x-ndjson");
}
type MultiStreamHooks = {
onDelta: (event: DirectChatStreamDelta) => void;
onWindow: (result: DirectChatMultiWindowResult) => void;
};
function replyFromWindowResult(
result: DirectChatMultiWindowResult,
): DirectChatAssistantReply {
return {
windowId: result.windowId,
content: result.reply ?? "",
...(result.reasoning ? { thinking: result.reasoning } : {}),
...(result.model ? { model: result.model } : {}),
...(result.error ? { error: result.error } : {}),
...(result.contextTrace ? { contextTrace: result.contextTrace } : {}),
};
}
/** 流式增量按窗口累加,并定期写入会话,避免刷新后丢掉已生成的正文 */
function createStreamingSaver(
conversation: DirectChatConversation,
windows: DirectChatWindow[],
text: string,
turnId: string,
) {
const ts = new Date().toISOString();
const replies: DirectChatAssistantReply[] = [];
let timer: ReturnType<typeof setTimeout> | null = null;
let closed = false;
const saveNow = () => {
if (timer) {
clearTimeout(timer);
timer = null;
}
if (closed) return;
const turn: DirectChatTurn = {
id: turnId,
user: { content: text, createdAt: ts },
replies: replies.map((r) => ({ ...r })),
createdAt: ts,
};
saveConversation({
...conversation,
windows,
turns: [...conversation.turns.filter((t) => t.id !== turnId), turn],
});
};
const saveSoon = () => {
if (closed || timer) return;
timer = setTimeout(() => {
timer = null;
saveNow();
}, 400);
};
return {
onDelta(event: DirectChatStreamDelta) {
let reply = replies.find((r) => r.windowId === event.windowId);
if (!reply) {
reply = { windowId: event.windowId, content: "" };
replies.push(reply);
}
if (event.field === "thinking") {
reply.thinking = `${reply.thinking || ""}${event.delta}`;
} else {
reply.content += event.delta;
}
saveSoon();
},
onWindow(result: DirectChatMultiWindowResult) {
const next = replyFromWindowResult(result);
const idx = replies.findIndex((r) => r.windowId === result.windowId);
if (idx >= 0) replies[idx] = next;
else replies.push(next);
saveNow();
},
close() {
closed = true;
if (timer) {
clearTimeout(timer);
timer = null;
}
},
};
}
async function writeMultiStream(
res: ServerResponse,
run: (hooks: MultiStreamHooks) => Promise<DirectChatMultiTurnResult>,
finish: (multi: DirectChatMultiTurnResult) => Record<string, unknown>,
): Promise<void> {
res.writeHead(200, {
"Content-Type": "application/x-ndjson; charset=utf-8",
"Cache-Control": "no-cache, no-transform",
"X-Accel-Buffering": "no",
});
const writeEvent = (event: unknown) => {
res.write(`${JSON.stringify(event)}\n`);
};
try {
const multi = await run({
onDelta: (event) => writeEvent({ type: "delta", ...event }),
onWindow: (result) => writeEvent({ type: "window", result }),
});
writeEvent({ type: "done", multi, ...finish(multi) });
res.end();
} catch (err) {
const error = err instanceof Error ? err.message : String(err);
try {
writeEvent({ type: "error", error });
res.end();
} catch {
res.destroy();
}
}
}
function slotsFromBody(body: Record<string, unknown>): DirectChatShellSlots {
return {
systemPrompt:
typeof body.systemPrompt === "string"
? body.systemPrompt
: EMPTY_SHELL_SLOTS.systemPrompt,
loreBefore:
typeof body.loreBefore === "string"
? body.loreBefore
: EMPTY_SHELL_SLOTS.loreBefore,
seedHistory:
typeof body.seedHistory === "string"
? body.seedHistory
: EMPTY_SHELL_SLOTS.seedHistory,
loreAfter:
typeof body.loreAfter === "string"
? body.loreAfter
: EMPTY_SHELL_SLOTS.loreAfter,
postTurn:
typeof body.postTurn === "string"
? body.postTurn
: EMPTY_SHELL_SLOTS.postTurn,
};
}
export async function handleDirectChatApi(
req: IncomingMessage,
res: ServerResponse,
pathname: string,
): Promise<boolean> {
if (!pathname.startsWith("/api/direct-chat")) return false;
try {
if (req.method === "GET" && pathname === "/api/direct-chat/shells") {
json(res, 200, { shells: listShells() });
return true;
}
if (req.method === "POST" && pathname === "/api/direct-chat/shells") {
const body = JSON.parse(await readBody(req)) as Record<string, unknown>;
const shell = createShell({
name: typeof body.name === "string" ? body.name : undefined,
slots: slotsFromBody(body),
presetId:
body.presetId === null
? null
: typeof body.presetId === "string"
? body.presetId
: undefined,
});
json(res, 201, { shell });
return true;
}
const shellMatch = pathname.match(
/^\/api\/direct-chat\/shells\/([^/]+)(\/.*)?$/,
);
if (shellMatch) {
const shellId = decodeURIComponent(shellMatch[1]!);
const sub = shellMatch[2] ?? "";
if (req.method === "GET" && sub === "") {
const shell = getShell(shellId);
if (!shell) {
json(res, 404, { error: "上下文壳不存在" });
return true;
}
json(res, 200, {
shell,
conversations: listConversations(shellId),
});
return true;
}
if (
(req.method === "PATCH" || req.method === "PUT") &&
sub === ""
) {
const body = JSON.parse(await readBody(req)) as Record<string, unknown>;
try {
const shell = updateShell(shellId, {
name: typeof body.name === "string" ? body.name : undefined,
systemPrompt:
typeof body.systemPrompt === "string"
? body.systemPrompt
: undefined,
loreBefore:
typeof body.loreBefore === "string" ? body.loreBefore : undefined,
seedHistory:
typeof body.seedHistory === "string"
? body.seedHistory
: undefined,
loreAfter:
typeof body.loreAfter === "string" ? body.loreAfter : undefined,
postTurn:
typeof body.postTurn === "string" ? body.postTurn : undefined,
presetId:
body.presetId === null
? null
: typeof body.presetId === "string"
? body.presetId
: undefined,
});
json(res, 200, { shell });
} catch (err) {
json(res, 404, {
error: err instanceof Error ? err.message : String(err),
});
}
return true;
}
if (req.method === "DELETE" && sub === "") {
try {
deleteShell(shellId);
json(res, 200, { ok: true });
} catch (err) {
json(res, 404, {
error: err instanceof Error ? err.message : String(err),
});
}
return true;
}
if (req.method === "GET" && sub === "/conversations") {
if (!getShell(shellId)) {
json(res, 404, { error: "上下文壳不存在" });
return true;
}
json(res, 200, { conversations: listConversations(shellId) });
return true;
}
if (req.method === "POST" && sub === "/conversations") {
const body = JSON.parse(await readBody(req)) as {
name?: string;
messages?: DirectChatMessage[];
};
try {
const conversation = createConversation({
shellId,
name: body.name,
messages: body.messages,
});
json(res, 201, { conversation });
} catch (err) {
json(res, 400, {
error: err instanceof Error ? err.message : String(err),
});
}
return true;
}
}
const convMatch = pathname.match(
/^\/api\/direct-chat\/conversations\/([^/]+)(\/.*)?$/,
);
if (convMatch) {
const conversationId = decodeURIComponent(convMatch[1]!);
const sub = convMatch[2] ?? "";
if (req.method === "GET" && sub === "") {
const conversation = getConversation(conversationId);
if (!conversation) {
json(res, 404, { error: "会话不存在" });
return true;
}
const shell = getShell(conversation.shellId);
json(res, 200, { conversation, shell });
return true;
}
if (
(req.method === "PATCH" || req.method === "PUT") &&
sub === ""
) {
const body = JSON.parse(await readBody(req)) as {
name?: string;
messages?: DirectChatMessage[];
};
try {
const conversation = updateConversation(conversationId, {
name: body.name,
messages: body.messages,
});
json(res, 200, { conversation });
} catch (err) {
json(res, 404, {
error: err instanceof Error ? err.message : String(err),
});
}
return true;
}
if (req.method === "DELETE" && sub === "") {
try {
deleteConversation(conversationId);
json(res, 200, { ok: true });
} catch (err) {
json(res, 404, {
error: err instanceof Error ? err.message : String(err),
});
}
return true;
}
if (req.method === "POST" && sub === "/messages") {
const body = JSON.parse(await readBody(req)) as {
text?: string;
windows?: DirectChatWindow[];
complete?: boolean;
};
const text = typeof body.text === "string" ? body.text.trim() : "";
if (!text) {
json(res, 400, { error: "text 不能为空" });
return true;
}
const conversation = getConversation(conversationId);
if (!conversation) {
json(res, 404, { error: "会话不存在" });
return true;
}
const shell = getShell(conversation.shellId);
if (!shell) {
json(res, 404, { error: "上下文壳不存在" });
return true;
}
const windows =
Array.isArray(body.windows) && body.windows.length
? body.windows
: conversation.windows;
if (!windows.length) {
json(res, 400, { error: "请先新增至少一个模型窗口" });
return true;
}
const turnId = randomUUID();
const saver = createStreamingSaver(conversation, windows, text, turnId);
const run = (hooks?: MultiStreamHooks) =>
runDirectChatMultiTurn({
message: text,
slots: {
systemPrompt: shell.systemPrompt,
loreBefore: shell.loreBefore,
seedHistory: shell.seedHistory,
loreAfter: shell.loreAfter,
postTurn: shell.postTurn,
},
presetId: shell.presetId,
windows,
turns: conversation.turns,
complete: body.complete,
turnId,
onDelta: (event) => {
saver.onDelta(event);
hooks?.onDelta(event);
},
onWindow: (result) => {
saver.onWindow(result);
hooks?.onWindow(result);
},
});
try {
if (wantsNdjson(req)) {
await writeMultiStream(res, run, (multi) => {
saver.close();
const updated = saveConversation({
...conversation,
windows,
turns: [
...conversation.turns.filter((t) => t.id !== multi.turn.id),
multi.turn,
],
});
return { conversation: updated, shell };
});
return true;
}
const multi = await run();
saver.close();
const updated = saveConversation({
...conversation,
windows,
turns: [
...conversation.turns.filter((t) => t.id !== multi.turn.id),
multi.turn,
],
});
json(res, 200, {
conversation: updated,
shell,
multi,
});
} catch (err) {
json(res, 400, {
error: err instanceof Error ? err.message : String(err),
});
}
return true;
}
const selectMatch = sub.match(/^\/turns\/([^/]+)\/select$/);
if (selectMatch && req.method === "POST") {
const turnId = decodeURIComponent(selectMatch[1]!);
const body = JSON.parse(await readBody(req)) as {
windowId?: string | null;
};
const conversation = getConversation(conversationId);
if (!conversation) {
json(res, 404, { error: "会话不存在" });
return true;
}
const windowId =
body.windowId === null || body.windowId === undefined
? null
: String(body.windowId).trim() || null;
if (windowId) {
const turn = conversation.turns.find((t) => t.id === turnId);
if (!turn?.replies.some((r) => r.windowId === windowId)) {
json(res, 400, { error: "该窗口没有本轮回复" });
return true;
}
}
const turns = conversation.turns.map((t) =>
t.id === turnId ? { ...t, selectedWindowId: windowId } : t,
);
const updated = saveConversation({ ...conversation, turns });
json(res, 200, { conversation: updated });
return true;
}
}
if (
req.method === "POST" &&
pathname === "/api/direct-chat/ephemeral/complete"
) {
const body = JSON.parse(await readBody(req)) as Record<string, unknown> & {
text?: string;
complete?: boolean;
presetId?: string | null;
windows?: DirectChatWindow[];
turns?: DirectChatTurn[];
};
const text = typeof body.text === "string" ? body.text.trim() : "";
if (!text) {
json(res, 400, { error: "text 不能为空" });
return true;
}
const windows = Array.isArray(body.windows) ? body.windows : [];
const turns = Array.isArray(body.turns) ? body.turns : [];
if (!windows.length) {
json(res, 400, { error: "请先新增至少一个模型窗口" });
return true;
}
const run = (hooks?: MultiStreamHooks) =>
runDirectChatMultiTurn({
message: text,
slots: slotsFromBody(body),
presetId:
body.presetId === null
? null
: typeof body.presetId === "string"
? body.presetId
: undefined,
windows,
turns,
complete: body.complete,
onDelta: hooks?.onDelta,
onWindow: hooks?.onWindow,
});
try {
if (wantsNdjson(req)) {
await writeMultiStream(res, run, () => ({}));
return true;
}
const multi = await run();
json(res, 200, { multi });
} catch (err) {
json(res, 400, {
error: err instanceof Error ? err.message : String(err),
});
}
return true;
}
if (req.method === "POST" && pathname === "/api/direct-chat/promote") {
const body = JSON.parse(await readBody(req)) as {
shellId?: string;
shell?: {
name?: string;
systemPrompt?: string;
loreBefore?: string;
seedHistory?: string;
loreAfter?: string;
postTurn?: string;
presetId?: string | null;
};
conversationName?: string;
messages?: DirectChatMessage[];
windows?: DirectChatWindow[];
turns?: DirectChatTurn[];
};
try {
const promoted = promoteTemporary({
shellId: body.shellId,
shell: body.shell
? {
name: body.shell.name,
slots: {
systemPrompt: body.shell.systemPrompt ?? "",
loreBefore: body.shell.loreBefore ?? "",
seedHistory: body.shell.seedHistory ?? "",
loreAfter: body.shell.loreAfter ?? "",
postTurn: body.shell.postTurn ?? "",
},
presetId: body.shell.presetId,
}
: undefined,
conversationName: body.conversationName,
messages: Array.isArray(body.messages) ? body.messages : [],
windows: Array.isArray(body.windows) ? body.windows : [],
turns: Array.isArray(body.turns) ? body.turns : [],
});
json(res, 201, promoted);
} catch (err) {
json(res, 400, {
error: err instanceof Error ? err.message : String(err),
});
}
return true;
}
if (
req.method === "GET" &&
pathname === "/api/direct-chat/compare-profiles"
) {
json(res, 200, { profiles: listProfilesForCompare() });
return true;
}
return false;
} catch (err) {
json(res, 500, {
error: err instanceof Error ? err.message : String(err),
});
return true;
}
}