feat: Go backend, enhanced search, new widgets, Docker deploy
Major changes: - Add Go backend (backend/) with microservices architecture - Enhanced master-agents-svc: reranker, content-classifier, stealth-crawler, proxy-manager, media-search, fastClassifier, language detection - New web-svc widgets: KnowledgeCard, ProductCard, ProfileCard, VideoCard, UnifiedCard, CardGallery, InlineImageGallery, SourcesPanel, RelatedQuestions - Improved discover-svc with discover-db integration - Docker deployment improvements (Caddyfile, vendor.sh, BUILD.md) - Library-svc: project_id schema migration - Remove deprecated finance-svc and travel-svc - Localization improvements across services Made-with: Cursor
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM node:22-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
COPY --from=npm-cache / /tmp/npm-cache
|
||||
RUN npm install --cache /tmp/npm-cache --prefer-offline --no-audit
|
||||
COPY tsconfig.json ./
|
||||
COPY src ./src
|
||||
RUN npm run build
|
||||
@@ -9,7 +11,9 @@ RUN npm run build
|
||||
FROM node:22-alpine
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci --omit=dev
|
||||
COPY --from=npm-cache / /tmp/npm-cache
|
||||
RUN npm install --omit=dev --cache /tmp/npm-cache --prefer-offline --no-audit
|
||||
COPY --from=builder /app/dist ./dist
|
||||
COPY drizzle ./drizzle
|
||||
EXPOSE 3009
|
||||
CMD ["node", "dist/index.js"]
|
||||
|
||||
2
services/library-svc/drizzle/0002_project_id.sql
Normal file
2
services/library-svc/drizzle/0002_project_id.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE library_threads ADD COLUMN IF NOT EXISTS project_id TEXT;
|
||||
CREATE INDEX IF NOT EXISTS idx_library_threads_project_id ON library_threads(project_id);
|
||||
@@ -10,6 +10,8 @@ async function main() {
|
||||
await client.unsafe(sql0);
|
||||
const sql1 = readFileSync(join(process.cwd(), 'drizzle/0001_thread_messages.sql'), 'utf8');
|
||||
await client.unsafe(sql1);
|
||||
const sql2 = readFileSync(join(process.cwd(), 'drizzle/0002_project_id.sql'), 'utf8');
|
||||
await client.unsafe(sql2);
|
||||
console.log('Schema applied');
|
||||
await client.end();
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { integer, jsonb, pgTable, text, timestamp } from 'drizzle-orm/pg-core';
|
||||
export const threads = pgTable('library_threads', {
|
||||
id: text('id').primaryKey(),
|
||||
userId: text('user_id').notNull(),
|
||||
projectId: text('project_id'),
|
||||
title: text('title').notNull(),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
sources: jsonb('sources').$type<string[]>().default([]),
|
||||
|
||||
@@ -46,20 +46,25 @@ app.get('/metrics', async (_req, reply) => {
|
||||
});
|
||||
app.get('/ready', async () => ({ status: 'ready' }));
|
||||
|
||||
app.get('/api/v1/library/threads', async (req, reply) => {
|
||||
app.get<{ Querystring: { projectId?: string } }>('/api/v1/library/threads', async (req, reply) => {
|
||||
const userId = await getUserIdFromToken(req.headers.authorization);
|
||||
if (!userId) {
|
||||
return reply.status(401).send({ error: 'Authorization required' });
|
||||
}
|
||||
const projectId = req.query.projectId;
|
||||
try {
|
||||
const where = projectId
|
||||
? and(eq(threads.userId, userId), eq(threads.projectId, projectId))
|
||||
: eq(threads.userId, userId);
|
||||
const list = await db
|
||||
.select()
|
||||
.from(threads)
|
||||
.where(eq(threads.userId, userId))
|
||||
.where(where)
|
||||
.orderBy(desc(threads.createdAt));
|
||||
const chats = list.map((r) => ({
|
||||
id: r.id,
|
||||
title: r.title,
|
||||
projectId: r.projectId ?? undefined,
|
||||
createdAt: r.createdAt?.toISOString?.() ?? r.createdAt,
|
||||
sources: (r.sources as string[]) ?? [],
|
||||
files: (r.files as { fileId: string; name: string }[]) ?? [],
|
||||
@@ -72,13 +77,13 @@ app.get('/api/v1/library/threads', async (req, reply) => {
|
||||
});
|
||||
|
||||
app.post<{
|
||||
Body: { id?: string; title: string; sources?: string[]; files?: { fileId: string; name: string }[] };
|
||||
Body: { id?: string; title: string; projectId?: string; sources?: string[]; files?: { fileId: string; name: string }[] };
|
||||
}>('/api/v1/library/threads', async (req, reply) => {
|
||||
const userId = await getUserIdFromToken(req.headers.authorization);
|
||||
if (!userId) {
|
||||
return reply.status(401).send({ error: 'Authorization required' });
|
||||
}
|
||||
const { id, title, sources = [], files = [] } = req.body ?? {};
|
||||
const { id, title, projectId, sources = [], files = [] } = req.body ?? {};
|
||||
if (!title?.trim()) {
|
||||
return reply.status(400).send({ error: 'title is required' });
|
||||
}
|
||||
@@ -89,6 +94,7 @@ app.post<{
|
||||
.values({
|
||||
id: threadId,
|
||||
userId,
|
||||
projectId: projectId ?? null,
|
||||
title: title.trim(),
|
||||
sources,
|
||||
files,
|
||||
@@ -97,6 +103,7 @@ app.post<{
|
||||
return {
|
||||
id: inserted.id,
|
||||
title: inserted.title,
|
||||
projectId: inserted.projectId ?? undefined,
|
||||
createdAt: inserted.createdAt?.toISOString?.() ?? inserted.createdAt,
|
||||
sources: (inserted.sources as string[]) ?? [],
|
||||
files: (inserted.files as { fileId: string; name: string }[]) ?? [],
|
||||
@@ -156,6 +163,7 @@ app.get<{ Params: { id: string } }>('/api/v1/library/threads/:id', async (req, r
|
||||
chat: {
|
||||
id: t.id,
|
||||
title: t.title,
|
||||
projectId: t.projectId ?? undefined,
|
||||
createdAt: t.createdAt?.toISOString?.() ?? t.createdAt,
|
||||
sources: (t.sources as string[]) ?? [],
|
||||
files: (t.files as { fileId: string; name: string }[]) ?? [],
|
||||
@@ -186,6 +194,7 @@ app.post<{
|
||||
return reply.status(401).send({ error: 'Authorization required' });
|
||||
}
|
||||
const { id } = req.params;
|
||||
const body = req.body ?? {};
|
||||
const {
|
||||
messageId,
|
||||
query,
|
||||
@@ -194,7 +203,17 @@ app.post<{
|
||||
status = 'answering',
|
||||
sources = [],
|
||||
files = [],
|
||||
} = req.body ?? {};
|
||||
projectId,
|
||||
} = body as {
|
||||
messageId?: string;
|
||||
query?: string;
|
||||
backendId?: string;
|
||||
responseBlocks?: Block[];
|
||||
status?: string;
|
||||
sources?: string[];
|
||||
files?: { fileId: string; name: string }[];
|
||||
projectId?: string;
|
||||
};
|
||||
if (!messageId?.trim() || !query?.trim()) {
|
||||
return reply.status(400).send({ error: 'messageId and query are required' });
|
||||
}
|
||||
@@ -204,6 +223,7 @@ app.post<{
|
||||
await db.insert(threads).values({
|
||||
id,
|
||||
userId,
|
||||
projectId: projectId ?? null,
|
||||
title: query.length > 100 ? query.slice(0, 100) + '...' : query,
|
||||
sources,
|
||||
files,
|
||||
@@ -211,6 +231,9 @@ app.post<{
|
||||
t = { id };
|
||||
}
|
||||
const statusVal = (['answering', 'completed', 'error'] as const).includes(status as 'answering' | 'completed' | 'error') ? (status as 'answering' | 'completed' | 'error') : 'answering';
|
||||
if (t.id) {
|
||||
await db.update(threads).set({ sources, files }).where(eq(threads.id, t.id));
|
||||
}
|
||||
const [existing] = await db.select().from(threadMessages).where(and(eq(threadMessages.threadId, t.id), eq(threadMessages.messageId, messageId)));
|
||||
if (existing) {
|
||||
await db
|
||||
|
||||
Reference in New Issue
Block a user