feat(chatbot): per-user history via X-User-Id + agentic tools calling

Backend:
- New chatbot.tools.ts: 4 tools (get_server_stats, get_top_channels,
  get_recent_activity, get_top_flagged) with real DB executors
- chatbot.service: agentic loop — stream:true, parse SSE, execute
  tool_calls, feed results back, up to 4 rounds
- controller: resolve userId from X-User-Id header (no-login device
  uuid) with auth middleware precedence; history/clear scoped per user

Frontend:
- use-chatbot-user: mint UUID in localStorage, send as X-User-Id
- chatbotApi.send/getHistory/clearHistory accept userId header
- client.ts: apiRequest supports custom headers per call
- provider: history load + send + clear keyed to device user id
This commit is contained in:
asepharyana
2026-08-03 06:24:19 +07:00
parent 7513681b4b
commit d1c1f3e4a7
7 changed files with 506 additions and 56 deletions
+20 -10
View File
@@ -1,17 +1,27 @@
import type { ChatbotHistoryRow, ChatbotResponse } from "@/lib/types";
import { api } from "./client";
export const chatbotApi = {
send: (message: string, guildId?: string) =>
api.post<ChatbotResponse>("/api/chat", {
message,
context: guildId ? { guildId } : undefined,
}),
function userHeader(userId?: string): Record<string, string> {
return userId && userId !== "anonymous" ? { "X-User-Id": userId } : {};
}
getHistory: () =>
api.get<{ history: ChatbotHistoryRow[]; total: number }>(
"/api/chat/history",
export const chatbotApi = {
send: (message: string, guildId?: string, userId?: string) =>
api.post<ChatbotResponse>(
"/api/chat",
{
message,
context: guildId ? { guildId } : undefined,
},
userHeader(userId),
),
clearHistory: () => api.delete<{ ok: boolean }>("/api/chat/history"),
getHistory: (userId?: string) =>
api.get<{ history: ChatbotHistoryRow[]; total: number }>(
"/api/chat/history",
userHeader(userId),
),
clearHistory: (userId?: string) =>
api.delete<{ ok: boolean }>("/api/chat/history", userHeader(userId)),
};
+10 -6
View File
@@ -31,17 +31,18 @@ export async function apiRequest<T>(
method: string,
path: string,
body?: unknown,
headers?: Record<string, string>,
): Promise<T> {
const url = `${getBaseUrl()}${path}`;
const headers: Record<string, string> = {};
const finalHeaders: Record<string, string> = { ...(headers ?? {}) };
if (body !== undefined) {
headers["Content-Type"] = "application/json";
finalHeaders["Content-Type"] ??= "application/json";
}
const response = await fetch(url, {
method,
headers,
headers: finalHeaders,
body: body !== undefined ? JSON.stringify(body) : undefined,
});
@@ -59,7 +60,10 @@ export async function apiRequest<T>(
}
export const api = {
get: <T>(path: string) => apiRequest<T>("GET", path),
post: <T>(path: string, body?: unknown) => apiRequest<T>("POST", path, body),
delete: <T>(path: string) => apiRequest<T>("DELETE", path),
get: <T>(path: string, headers?: Record<string, string>) =>
apiRequest<T>("GET", path, undefined, headers),
post: <T>(path: string, body?: unknown, headers?: Record<string, string>) =>
apiRequest<T>("POST", path, body, headers),
delete: <T>(path: string, headers?: Record<string, string>) =>
apiRequest<T>("DELETE", path, undefined, headers),
};