feat(odin): conversation-scoped service + routes (replace /history)

Remove single-thread getChatHistory/appendChatMessages/clearChatHistory and the
three /history routes; add listConversations, createConversation, getConversation-
Messages, appendConversationMessages, renameConversation, deleteConversation with
ownership checks and auto-title; wire six /conversations[/:id] routes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-06-08 19:19:03 +02:00
parent c5cfd855ba
commit 6664e3bc4d
3 changed files with 187 additions and 41 deletions

View File

@@ -1,12 +1,14 @@
import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
import multipart from "@fastify/multipart";
import { requirePermission } from "../../middleware/auth";
import { success, error } from "../../utils/response";
import { success, error, parseId } from "../../utils/response";
import { parseBody } from "../../schemas/common";
import {
AiChatSchema,
AiBudgetSchema,
AiHistoryAppendSchema,
AiCreateConversationSchema,
AiRenameConversationSchema,
} from "../../schemas/ai.schema";
import { logAudit } from "../../services/audit";
import { config } from "../../config/env";
@@ -18,9 +20,12 @@ import {
setBudgetUsd,
chat,
extractInvoice,
getChatHistory,
appendChatMessages,
clearChatHistory,
listConversations,
createConversation,
getConversationMessages,
appendConversationMessages,
renameConversation,
deleteConversation,
type ExtractedInvoice,
} from "../../services/ai.service";
@@ -131,35 +136,95 @@ export default async function aiRoutes(app: FastifyInstance): Promise<void> {
},
);
// GET /api/admin/ai/history — this user's stored chat thread
// GET /conversations — this user's conversations (stable order)
app.get(
"/history",
"/conversations",
{ preHandler: requirePermission("ai.use") },
async (request: FastifyRequest, reply: FastifyReply) => {
const messages = await getChatHistory(request.authData!.userId);
return success(reply, { messages });
const conversations = await listConversations(request.authData!.userId);
return success(reply, { conversations });
},
);
// POST /api/admin/ai/history — append turns to this user's thread
// POST /conversations — create
app.post(
"/history",
"/conversations",
{ preHandler: requirePermission("ai.use") },
async (request: FastifyRequest, reply: FastifyReply) => {
const body = parseBody(AiCreateConversationSchema, request.body);
if ("error" in body) return error(reply, body.error, 400);
const conversation = await createConversation(
request.authData!.userId,
body.data.title,
);
return success(reply, { conversation });
},
);
// GET /conversations/:id/messages
app.get(
"/conversations/:id/messages",
{ preHandler: requirePermission("ai.use") },
async (request: FastifyRequest, reply: FastifyReply) => {
const id = parseId((request.params as { id: string }).id, reply);
if (id === null) return;
const result = await getConversationMessages(
request.authData!.userId,
id,
);
if ("error" in result) return error(reply, result.error, result.status);
return success(reply, { messages: result.data });
},
);
// POST /conversations/:id/messages — append turns
app.post(
"/conversations/:id/messages",
{ preHandler: requirePermission("ai.use") },
async (request: FastifyRequest, reply: FastifyReply) => {
const id = parseId((request.params as { id: string }).id, reply);
if (id === null) return;
const body = parseBody(AiHistoryAppendSchema, request.body);
if ("error" in body) return error(reply, body.error, 400);
await appendChatMessages(request.authData!.userId, body.data.messages);
return success(reply, { ok: true });
const result = await appendConversationMessages(
request.authData!.userId,
id,
body.data.messages,
);
if ("error" in result) return error(reply, result.error, result.status);
return success(reply, result.data);
},
);
// DELETE /api/admin/ai/history — clear this user's thread
app.delete(
"/history",
// PATCH /conversations/:id — rename
app.patch(
"/conversations/:id",
{ preHandler: requirePermission("ai.use") },
async (request: FastifyRequest, reply: FastifyReply) => {
await clearChatHistory(request.authData!.userId);
return success(reply, { ok: true });
const id = parseId((request.params as { id: string }).id, reply);
if (id === null) return;
const body = parseBody(AiRenameConversationSchema, request.body);
if ("error" in body) return error(reply, body.error, 400);
const result = await renameConversation(
request.authData!.userId,
id,
body.data.title,
);
if ("error" in result) return error(reply, result.error, result.status);
return success(reply, { conversation: result.data });
},
);
// DELETE /conversations/:id
app.delete(
"/conversations/:id",
{ preHandler: requirePermission("ai.use") },
async (request: FastifyRequest, reply: FastifyReply) => {
const id = parseId((request.params as { id: string }).id, reply);
if (id === null) return;
const result = await deleteConversation(request.authData!.userId, id);
if ("error" in result) return error(reply, result.error, result.status);
return success(reply, result.data);
},
);
}