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 install
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,8 @@ RUN npm run build
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --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
EXPOSE 3005
ENV DATA_DIR=/app/data

View File

@@ -40,6 +40,7 @@ const PORT = parseInt(process.env.PORT ?? '3005', 10);
const MEMORY_SVC_URL = process.env.MEMORY_SVC_URL ?? '';
const LLM_SVC_URL = process.env.LLM_SVC_URL ?? '';
const MASTER_AGENTS_SVC_URL = process.env.MASTER_AGENTS_SVC_URL?.trim() ?? '';
const DISCOVER_SVC_URL = process.env.DISCOVER_SVC_URL?.trim() ?? '';
const messageSchema = z.object({
messageId: z.string().min(1),
@@ -237,7 +238,33 @@ app.post<{ Body: unknown }>('/api/v1/chat', async (req, reply) => {
}
}
const isDiscoverSummary =
body.message.content.startsWith('Summary: ') &&
body.message.content.length > 9;
const summaryUrl = isDiscoverSummary
? body.message.content.slice(9).trim()
: '';
if (isDiscoverSummary) {
req.log.info(
{ summaryUrl: summaryUrl.slice(0, 80), hasDiscoverSvc: !!DISCOVER_SVC_URL },
'Discover summary request'
);
if (DISCOVER_SVC_URL) {
fetch(`${DISCOVER_SVC_URL.replace(/\/$/, '')}/api/v1/discover/queue`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: summaryUrl, priority: Date.now() }),
signal: AbortSignal.timeout(3000),
}).catch(() => {});
}
}
try {
// Саммари статьи: всегда идём в master-agents (поиск по теме + саммари), не отдаём кэш.
// Кэш сохраняется после ответа для перегенерации; при нажатии «перегенерировать» кэш очищается в web-svc.
if (!MASTER_AGENTS_SVC_URL) {
return reply.status(503).send({ message: 'MASTER_AGENTS_SVC_URL not configured. master-agents-svc required for chat.' });
}
@@ -269,6 +296,85 @@ app.post<{ Body: unknown }>('/api/v1/chat', async (req, reply) => {
const errText = await proxyRes.text();
return reply.status(proxyRes.status).send({ message: errText || 'master-agents-svc error' });
}
if (!proxyRes.body) {
return reply.status(502).send({ message: 'No response body' });
}
if (isDiscoverSummary && summaryUrl && DISCOVER_SVC_URL) {
const collected: string[] = [];
const reader = proxyRes.body.getReader();
const decoder = new TextDecoder();
const encoder = new TextEncoder();
const discoverBase = DISCOVER_SVC_URL.replace(/\/$/, '');
const stream = new ReadableStream({
async start(controller) {
let buffer = '';
try {
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() ?? '';
for (const line of lines) {
if (line.trim()) {
collected.push(line);
controller.enqueue(encoder.encode(line + '\n'));
}
}
}
if (buffer.trim()) {
collected.push(buffer);
controller.enqueue(encoder.encode(buffer + '\n'));
}
} finally {
if (collected.length > 0) {
const urlToSave = summaryUrl;
const eventsToSave = [...collected];
req.log.info({ url: urlToSave.slice(0, 60), events: eventsToSave.length }, 'article-summary saving full payload to discover-svc');
const maxRetries = 5;
const retryDelayMs = 2000;
let lastRes: Response | null = null;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const res = await fetch(`${discoverBase}/api/v1/discover/article-summary`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: urlToSave, events: eventsToSave }),
signal: AbortSignal.timeout(120000),
});
lastRes = res;
if (res.ok) {
req.log.info({ url: urlToSave.slice(0, 60) }, 'article-summary saved');
break;
}
if (res.status === 413) {
req.log.warn({ url: urlToSave.slice(0, 60), attempt }, 'article-summary 413, retry full payload');
} else {
req.log.warn({ url: urlToSave.slice(0, 60), status: res.status, attempt }, 'article-summary save failed, retry');
}
} catch (e) {
req.log.warn({ err: e, attempt }, 'article-summary save error, retry');
}
if (attempt < maxRetries) {
await new Promise((r) => setTimeout(r, retryDelayMs));
}
}
if (lastRes && !lastRes.ok) {
req.log.warn({ url: urlToSave.slice(0, 60), status: lastRes.status }, 'article-summary save failed after all retries');
}
}
controller.close();
}
},
});
return reply
.header('Content-Type', 'application/x-ndjson')
.header('Cache-Control', 'no-cache')
.send(stream);
}
return reply
.header('Content-Type', 'application/x-ndjson')
.header('Cache-Control', 'no-cache')