import type { IncomingMessage, ServerResponse } from "node:http"; import { dispatchSlashCommand } from "../slash/dispatch.ts"; import { normalizeChatImages } from "../services/chat-images.ts"; import type { WebUiContext } from "../types/context.ts"; import { json, readBody } from "../http/request.ts"; async function trySlashDispatch( msg: string, imgs: ReturnType, sendCmd: WebUiContext["rpc"]["sendCmd"], ): Promise> | null> { const trimmed = msg.trim(); if (!trimmed.startsWith("/")) return null; const slash = await dispatchSlashCommand(trimmed, sendCmd); return slash?.handled ? slash : null; } export function handleChatRoute( req: IncomingMessage, res: ServerResponse, ctx: WebUiContext, pathname: string, ): boolean { const { sendCmd, submitPrompt } = ctx.rpc; if (req.method === "POST" && pathname === "/api/chat") { void readBody(req) .then(async ({ message, images, streamingBehavior }) => { const msg = typeof message === "string" ? message : ""; const imgs = normalizeChatImages(images); if (!msg.trim() && !imgs?.length) throw new Error("消息不能为空"); if (imgs?.length) console.log(`[webui] chat: ${imgs.length} image(s), message=${msg.length} chars`); const slash = await trySlashDispatch(msg, imgs, sendCmd); if (slash) { return json(res, { ok: true, slash: true, ...slash }); } const behavior = streamingBehavior === "steer" || streamingBehavior === "followUp" ? streamingBehavior : undefined; submitPrompt({ message: msg, images: imgs, streamingBehavior: behavior }); return json(res, { ok: true, accepted: true }, 202); }) .catch((err) => json(res, { error: err.message }, 500)); return true; } if (req.method === "POST" && pathname === "/api/steer") { void readBody(req) .then(async ({ message, images }) => { const msg = typeof message === "string" ? message.trim() : ""; const imgs = normalizeChatImages(images); if (!msg && !imgs?.length) throw new Error("消息不能为空"); const result = await sendCmd({ type: "steer", message: msg, images: imgs }); if (!result.success) throw new Error(result.error || "steer 失败"); json(res, { ok: true }); }) .catch((err) => json(res, { error: err.message }, 500)); return true; } if (req.method === "POST" && pathname === "/api/follow-up") { void readBody(req) .then(async ({ message, images }) => { const msg = typeof message === "string" ? message.trim() : ""; const imgs = normalizeChatImages(images); if (!msg && !imgs?.length) throw new Error("消息不能为空"); const result = await sendCmd({ type: "follow_up", message: msg, images: imgs }); if (!result.success) throw new Error(result.error || "follow_up 失败"); json(res, { ok: true }); }) .catch((err) => json(res, { error: err.message }, 500)); return true; } if (req.method === "POST" && pathname === "/api/bash") { void readBody(req) .then(async ({ command, excludeFromContext }) => { const cmd = typeof command === "string" ? command.trim() : ""; if (!cmd) throw new Error("命令不能为空"); const result = await sendCmd({ type: "bash", command: cmd, excludeFromContext: excludeFromContext === true, }); if (!result.success) throw new Error(result.error || "命令执行失败"); json(res, result.data); }) .catch((err) => json(res, { error: err.message }, 500)); return true; } if (req.method === "GET" && pathname === "/api/events") { res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive", }); ctx.rpc.connectSseClient(res); req.on("close", () => ctx.rpc.removeSseClient(res)); return true; } if (req.method === "POST" && pathname === "/api/messages") { void sendCmd({ type: "get_messages" }) .then((r: any) => (r.success ? json(res, r.data) : json(res, { error: r.error }, 500))) .catch((err) => json(res, { error: err.message }, 500)); return true; } if (req.method === "POST" && pathname === "/api/abort") { void sendCmd({ type: "abort" }) .then(() => json(res, { ok: true })) .catch((err) => json(res, { error: err.message }, 500)); return true; } if (req.method === "POST" && pathname === "/api/abort-retry") { void sendCmd({ type: "abort_retry" }) .then(() => json(res, { ok: true })) .catch((err) => json(res, { error: err.message }, 500)); return true; } return false; }