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:
home
2026-02-27 04:15:32 +03:00
parent 328d968f3f
commit 06fe57c765
285 changed files with 53132 additions and 1871 deletions

View File

@@ -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"]

View 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);

View File

@@ -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();
}

View File

@@ -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([]),

View File

@@ -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