feat: default locale Russian, geo determines language for other countries

- localization-svc: defaultLocale ru, resolveLocale only by geo
- web-svc: DEFAULT_LOCALE ru, layout lang=ru, embeddedTranslations fallback ru
- countryToLocale: default ru when no country or unknown country

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
home
2026-02-23 15:10:38 +03:00
parent 8fc82a3b90
commit cd6b7857ba
606 changed files with 26148 additions and 14297 deletions

View File

@@ -0,0 +1,282 @@
# Стратегия кэширования и предварительной обработки
## Принципы
1. **Не делать по запросу то, что можно сделать заранее**
2. **Кэшировать по обезличенному ключу** (query hash, topic, ticker)
3. **Фоновые воркеры** обновляют кэш по расписанию
4. **Первый запрос** — холодный, заполняет кэш для последующих
---
## 1. Discover (Новости)
### Архитектура
```
┌─────────────────────────────────────────────────────────────────────────┐
│ cache-worker (CronJob каждые 15 мин) │
│ │
│ 1. Агрегировать новости по темам (tech, finance, travel, world, ...) │
│ 2. Для каждой новости: суммаризация через LLM (batch) │
│ 3. Сохранить в Redis: discover:{topic} = JSON │
│ TTL = 30 мин │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ discover-svc (HTTP GET /discover?topic=tech) │
│ │
│ 1. Redis GET discover:tech │
│ 2. Если есть → отдать сразу │
│ 3. Если нет (cold) → синхронно выполнить агрегацию + суммаризацию, │
│ сохранить в Redis, отдать │
└─────────────────────────────────────────────────────────────────────────┘
```
### Redis схема
| Ключ | Значение | TTL |
|------|----------|-----|
| `discover:tech` | `{ items: [{ title, summary, source, url, fetched_at }] }` | 30 min |
| `discover:finance` | аналогично | 30 min |
| `discover:travel` | аналогично | 30 min |
### UX при cold start
- Skeleton карточек новостей; timeout 30 с; Retry при ошибке; предупреждение о задержке при cold
### Cron расписание
```
*/15 * * * * cache-worker --task=discover
```
---
## 2. Finance (Рынок, новости по тикерам)
### Архитектура
```
┌─────────────────────────────────────────────────────────────────────────┐
│ cache-worker (CronJob каждые 2 мин) │
│ │
│ 1. Market summary: индексы, futures, VIX │
│ 2. Heatmap S&P 500 │
│ 3. News для топ-20 тикеров (по популярности) → суммаризация │
│ 4. Redis: finance:summary, finance:heatmap, finance:news:AAPL, ... │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ finance-svc │
│ │
│ GET /finance/summary → Redis finance:summary │
│ GET /finance/heatmap → Redis finance:heatmap │
│ GET /finance/news/AAPL → Redis finance:news:AAPL (или cold fill) │
└─────────────────────────────────────────────────────────────────────────┘
```
### Redis схема
| Ключ | Значение | TTL |
|------|----------|-----|
| `finance:summary` | Market data JSON | 5 min |
| `finance:heatmap` | Heatmap JSON | 5 min |
| `finance:news:AAPL` | News + summary | 15 min |
| `finance:news:TSLA` | News + summary | 15 min |
### Cold fill для редких тикеров
Skeleton heatmap/карточки; timeout 20 с.
Если запрос на `finance:news:XOM` — в кэше нет:
1. Выполнить запрос к API + суммаризацию
2. Сохранить в Redis
3. Отдать пользователю
---
## 3. Travel (Маршруты, тренды, Inspiration Cards)
### Архитектура
```
┌─────────────────────────────────────────────────────────────────────────┐
│ cache-worker (CronJob каждые 46 ч) — task=travel │
│ │
│ 1. Trending destinations → Redis travel:trending │
│ 2. Inspiration Cards (курируемые статьи) → Redis travel:inspiration │
│ Примеры: «Лучшие тропы в Словении», «Лесные отели в Швеции», │
│ «Скрытые пляжи Португалии» — LLM генерация/суммаризация batch │
│ 3. (Опционально) Pre-warm популярных маршрутов: Paris 5d, Tokyo 7d │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ travel-svc │
│ │
│ GET /travel/inspiration → Redis travel:inspiration (отдать сразу) │
│ GET /travel/trending → Redis travel:trending │
│ POST /travel/itinerary → hash → Redis или LLM │
└─────────────────────────────────────────────────────────────────────────┘
```
### Redis схема
| Ключ | Значение | TTL |
|------|----------|-----|
| `travel:trending` | Список направлений с картинками | 6 h |
| `travel:inspiration` | `[{ title, summary, image, url }]` — Inspiration Cards | 6 h |
| `travel:itinerary:{hash}` | Сгенерированный маршрут | 24 h |
### UX при cold start
- Skeleton trending/inspiration; itinerary: прогресс по этапам, 60 с timeout
### Реализация Inspiration Cards в cache-worker
```typescript
// cache-worker/src/tasks/travel.ts
export async function runTravelPrecompute(redis: Redis) {
const trending = await fetchTrendingDestinations();
await redis.setex('travel:trending', 6 * 60 * 60, JSON.stringify(trending));
const inspirationTopics = [
'best hiking trails Slovenia',
'forest hotels Sweden solitude',
'hidden beaches Portugal',
'weekend getaways Europe',
// ... курированный список тем
];
const inspiration = await Promise.all(
inspirationTopics.map(async (topic) => {
const article = await generateOrFetchArticle(topic); // LLM или партнёрский API
return { title: article.title, summary: article.summary, image: article.image, url: article.url };
})
);
await redis.setex('travel:inspiration', 6 * 60 * 60, JSON.stringify(inspiration));
}
---
## 4. Поиск (Search / Chat)
### Архитектура
```
┌─────────────────────────────────────────────────────────────────────────┐
│ chat-svc POST /api/v1/chat │
│ │
│ 1. query_normalized = normalize(query) // lowercase, trim, etc. │
│ 2. query_hash = sha256(query_normalized + mode + sources) │
│ 3. Redis GET search:result:{query_hash} │
│ 4. Если есть и не expired → отдать (stream из сохранённого или JSON) │
│ 5. Если нет → полный pipeline → сохранить в Redis → отдать │
└─────────────────────────────────────────────────────────────────────────┘
```
### Redis схема
| Ключ | Значение | TTL |
|------|----------|-----|
| `search:result:{query_hash}` | `{ text, sources, widgets }` | 1 h (Quick) / 24 h (Pro, если low churn) |
### UX при Pro/Deep
- AssistantSteps: отображение этапов (Classifier → Researcher → Writer); оценка времени
### Pre-warm популярных запросов
cache-worker раз в час:
1. Анализ логов/метрик: топ-50 запросов за последние 24ч (без user_id, только query)
2. Для каждого: проверка кэша, если нет — выполнить pipeline, сохранить
---
## 5. Медиа (Images, Videos)
```
query_hash = sha256(search_query)
Redis: media:images:{query_hash}, media:videos:{query_hash}
TTL: 1 h
```
Одинаковые запросы на картинки/видео отдаются из кэша.
### UX
- Skeleton grid; timeout 15 с; Retry при ошибке
---
## 6. Виджеты
| Виджет | Ключ кэша | TTL |
|--------|-----------|-----|
| Погода | `widget:weather:{location}` | 15 min |
| Акции | `widget:stock:{ticker}` | 5 min |
| Калькулятор | **Не кэшируем** — на клиенте |
---
## 7. Реализация cache-worker
### Задачи (tasks)
```typescript
// cache-worker/src/tasks/discover.ts
export async function runDiscoverPrecompute(redis: Redis) {
const topics = ['tech', 'finance', 'travel', 'world', 'science'];
for (const topic of topics) {
const items = await aggregateNews(topic);
const summarized = await summarizeBatch(items); // batch LLM call
await redis.setex(
`discover:${topic}`,
30 * 60, // 30 min
JSON.stringify({ items: summarized, updated_at: Date.now() })
);
}
}
// cache-worker/src/tasks/finance.ts
export async function runFinancePrecompute(redis: Redis) {
const summary = await fetchMarketSummary();
await redis.setex('finance:summary', 5 * 60, JSON.stringify(summary));
const heatmap = await fetchHeatmap();
await redis.setex('finance:heatmap', 5 * 60, JSON.stringify(heatmap));
const topTickers = ['AAPL', 'TSLA', 'GOOGL', 'MSFT', ...];
for (const ticker of topTickers) {
const news = await fetchNewsForTicker(ticker);
const summarized = await summarizeNews(news);
await redis.setex(
`finance:news:${ticker}`,
15 * 60,
JSON.stringify(summarized)
);
}
}
// cache-worker/src/tasks/travel.ts
export async function runTravelPrecompute(redis: Redis) {
const trending = await fetchTrendingDestinations();
await redis.setex('travel:trending', 6 * 60 * 60, JSON.stringify(trending));
}
```
### CLI
```bash
cache-worker --task=discover # только discover
cache-worker --task=finance # только finance
cache-worker --task=travel # trending + Inspiration Cards
cache-worker --task=all # все задачи
```
---
## 8. Метрики кэширования
| Область | Снижение LLM вызовов |
|---------|----------------------|
| Discover | ~6080% |
| Finance | ~6080% |
| Search | ~3050% |