8ccaa824dd
- Add HMAC-based file token auth for secure AI model file access - Add file upload retry with exponential backoff (max 3 retries) - File endpoint now requires session auth or signed token - Support assistant role messages in chat input - Optimistic UI for attachments on message send - Verify topic ownership before allowing messages - Switch web scraping to Firecrawl API - Agent profile page layout fixes (proper flex overflow) - Add quick switcher (Ctrl+K) to sidenav - Clean up longcat.ts and stale comments
153 lines
4.6 KiB
TypeScript
153 lines
4.6 KiB
TypeScript
import { modelMessageSchema } from 'ai';
|
|
import * as z from 'zod';
|
|
import { attachments, messageParts, messages } from '~~/drizzle/schema';
|
|
import { db } from '~~/server/lib/db';
|
|
|
|
export default defineEventHandler(async (event) => {
|
|
await protectRoute(event);
|
|
|
|
const topicId = getRouterParam(event, 'topicId')!;
|
|
const userId = event.context.user!.id;
|
|
|
|
const topic = await db.query.topics.findFirst({
|
|
where: {
|
|
id: topicId,
|
|
userId,
|
|
},
|
|
});
|
|
|
|
if (!topic) {
|
|
throw createError({
|
|
statusCode: 404,
|
|
statusMessage: 'Not Found',
|
|
message: 'Topic not found',
|
|
});
|
|
}
|
|
|
|
const result = await readValidatedBody(event, z.object({
|
|
message: z.intersection(
|
|
z.object({
|
|
id: z.string(),
|
|
fileIds: z.array(z.string()).optional(),
|
|
}),
|
|
modelMessageSchema
|
|
),
|
|
}).safeParse);
|
|
if (!result.success) {
|
|
console.log(result.error);
|
|
throw createError({
|
|
statusCode: 400,
|
|
statusMessage: 'Bad Request',
|
|
message: result.error.issues.map(issue => issue.message).join(', '),
|
|
});
|
|
}
|
|
|
|
const { message } = result.data;
|
|
|
|
if (['user', 'assistant'].includes(message.role) === false) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
statusMessage: 'Bad Request',
|
|
message: 'Invalid role',
|
|
});
|
|
}
|
|
|
|
switch (message.role) {
|
|
case 'user': {
|
|
await db.insert(messages).values({
|
|
// @ts-ignore - drizzle bug
|
|
id: message.id,
|
|
userId,
|
|
topicId,
|
|
parentMessageId: null,
|
|
generationId: null,
|
|
role: message.role,
|
|
content: message.content,
|
|
});
|
|
|
|
for (const fileId of message.fileIds || []) {
|
|
await db.insert(attachments).values({
|
|
userId,
|
|
topicId,
|
|
messageId: message.id,
|
|
fileId,
|
|
});
|
|
}
|
|
|
|
const usermessage = await db.query.messages.findFirst({
|
|
where: {
|
|
id: message.id,
|
|
},
|
|
with: {
|
|
attachments: {
|
|
with: {
|
|
file: true,
|
|
}
|
|
}
|
|
},
|
|
});
|
|
|
|
if (!usermessage) {
|
|
throw createError({
|
|
statusCode: 500,
|
|
statusMessage: 'Failed to insert message',
|
|
message: 'Failed to insert message',
|
|
});
|
|
}
|
|
topicEvents.emit(topicId, { type: 'MESSAGE_CREATED', payload: usermessage });
|
|
|
|
return {
|
|
ok: true
|
|
};
|
|
}
|
|
case 'assistant': {
|
|
const [dbmessage] = await db.insert(messages).values({
|
|
// @ts-ignore - drizzle bug
|
|
id: message.id,
|
|
userId,
|
|
topicId,
|
|
parentMessageId: null,
|
|
generationId: null,
|
|
role: message.role,
|
|
content: message.content,
|
|
}).returning();
|
|
|
|
if (!dbmessage) {
|
|
throw createError({
|
|
statusCode: 500,
|
|
statusMessage: 'Failed to insert message',
|
|
message: 'Failed to insert message',
|
|
});
|
|
}
|
|
|
|
const [part] = await db.insert(messageParts).values({
|
|
userId,
|
|
topicId,
|
|
messageId: dbmessage.id,
|
|
type: 'text',
|
|
content: message.content,
|
|
providerOptions: null,
|
|
finished: true,
|
|
createdAt: new Date(),
|
|
lastUpdatedAt: new Date(),
|
|
}).returning();
|
|
|
|
// TODO: transaction
|
|
if (!part) {
|
|
throw createError({
|
|
statusCode: 500,
|
|
statusMessage: 'Failed to insert message part',
|
|
message: 'Failed to insert message part',
|
|
});
|
|
}
|
|
|
|
topicEvents.emit(topicId, { type: 'MESSAGE_CREATED', payload: dbmessage });
|
|
topicEvents.emit(topicId, { type: 'text-start', payload: { messageId: dbmessage.id, part } });
|
|
topicEvents.emit(topicId, { type: 'text-end', payload: { messageId: dbmessage.id, partId: part.id, lastUpdatedAt: new Date(), content: message.content } });
|
|
|
|
return {
|
|
ok: true
|
|
};
|
|
}
|
|
}
|
|
}) |