feat(backend,frontend): migrate data APIs from REST to native tRPC over WebSocket
Replace REST module routers with a single typed tRPC appRouter served over
/trpc (HTTP + WebSocket), and rewire the frontend to call it via
@trpc/client wsLink (browser) and httpLink (RSC data layer). Existing
/api/health + /api/metrics stay as plain Express for infra scraping.
Notable fixes surfaced by the live smoke test:
- Express 5 / path-to-regexp v8 rejects the /trpc/* wildcard route; use a
prefix middleware that computes opts.path from the URL instead.
- nodeHTTPRequestHandler treats opts.path as the literal procedure path, so
it is derived per-request from req.url.
- Two ws servers on one http.Server (the /ws voice socket + /trpc) collided
and returned 400 on upgrade; both now use noServer + a manually routed
server.on('upgrade') keyed by path.
Verified: BE tsc+biome+40 vitest green; FE tsc+biome green; live
HTTP and WebSocket calls returned real prod data.
Co-Authored-By: Claude Opus 4.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
d8552a9fb8
commit
2fa1827f17
@@ -1,96 +0,0 @@
|
||||
import type { Request, Response } from "express";
|
||||
import { createChildLogger } from "@/shared/logger/index";
|
||||
import { asyncHandler } from "../../shared/middlewares/index.js";
|
||||
import { chatbotService } from "./chatbot.service.js";
|
||||
|
||||
const logger = createChildLogger("chatbot.controller");
|
||||
|
||||
interface AuthenticatedRequest extends Request {
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the actor id for a request. Frontend (no-login) sends a per-device
|
||||
* UUID via X-User-Id so chat history stays isolated per visitor; a registered
|
||||
* auth middleware userId takes precedence when present.
|
||||
*/
|
||||
function resolveUserId(req: Request): string {
|
||||
const authId = (req as AuthenticatedRequest).userId;
|
||||
if (authId) return authId;
|
||||
const header = (req.headers["x-user-id"] as string | undefined)?.trim();
|
||||
return header || "anonymous";
|
||||
}
|
||||
|
||||
export const handleChatbotChat = asyncHandler(
|
||||
async (req: Request, res: Response) => {
|
||||
const { message, context } = req.body as {
|
||||
message: string;
|
||||
context?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
// Validate required fields
|
||||
if (!message || typeof message !== "string") {
|
||||
return res.status(400).json({
|
||||
error: "INVALID_INPUT",
|
||||
message: "Message is required and must be a string",
|
||||
});
|
||||
}
|
||||
|
||||
// Get user ID from X-User-Id header (no-login device uuid) or auth
|
||||
const userId = resolveUserId(req);
|
||||
|
||||
logger.debug(
|
||||
{ userId, messageLength: message.length, context },
|
||||
"Received chatbot chat message",
|
||||
);
|
||||
|
||||
// Process message & generate response
|
||||
const response = await chatbotService.processMessage(
|
||||
message,
|
||||
context,
|
||||
userId,
|
||||
);
|
||||
|
||||
// Save conversation to database
|
||||
await chatbotService.saveConversation({
|
||||
userId,
|
||||
userMessage: message,
|
||||
botResponse: response,
|
||||
context,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
|
||||
logger.info({ userId }, "Chatbot chat processed successfully");
|
||||
|
||||
res.status(200).json({
|
||||
response,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
export const getChatbotHistory = asyncHandler(
|
||||
async (req: Request, res: Response) => {
|
||||
const userId = resolveUserId(req);
|
||||
const limit = Math.min(parseInt(req.query.limit as string, 10) || 50, 100);
|
||||
|
||||
const history = await chatbotService.getChatHistory(userId, limit);
|
||||
|
||||
res.status(200).json({
|
||||
history,
|
||||
total: history.length,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
export const clearChatbotHistory = asyncHandler(
|
||||
async (req: Request, res: Response) => {
|
||||
const userId = resolveUserId(req);
|
||||
|
||||
await chatbotService.clearChatHistory(userId);
|
||||
|
||||
res.status(200).json({
|
||||
message: "Chat history cleared successfully",
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -1,18 +0,0 @@
|
||||
import express, { type Router } from "express";
|
||||
import { validateBody } from "../../shared/middlewares/index.js";
|
||||
import {
|
||||
clearChatbotHistory,
|
||||
getChatbotHistory,
|
||||
handleChatbotChat,
|
||||
} from "./chatbot.controller.js";
|
||||
import { chatRequestSchema } from "./chatbot.schema.js";
|
||||
|
||||
export function createChatbotRouter(): Router {
|
||||
const router = express.Router();
|
||||
|
||||
router.post("/chat", validateBody(chatRequestSchema), handleChatbotChat);
|
||||
router.get("/chat/history", getChatbotHistory);
|
||||
router.delete("/chat/history", clearChatbotHistory);
|
||||
|
||||
return router;
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { createChatbotRouter } from "./chatbot.routes.js";
|
||||
Reference in New Issue
Block a user