129 lines
4.5 KiB
TypeScript
129 lines
4.5 KiB
TypeScript
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<typeof normalizeChatImages>,
|
|
sendCmd: WebUiContext["rpc"]["sendCmd"],
|
|
): Promise<Awaited<ReturnType<typeof dispatchSlashCommand>> | 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;
|
|
}
|