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,17 @@
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY tsconfig.json ./
COPY src ./src
COPY drizzle ./drizzle
RUN npm run build
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=builder /app/dist ./dist
COPY drizzle ./drizzle
EXPOSE 3010
CMD ["node", "dist/index.js"]

View File

@@ -0,0 +1,12 @@
CREATE TABLE IF NOT EXISTS memory_records (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
org_id TEXT,
key TEXT NOT NULL,
value TEXT NOT NULL,
embedding JSONB,
created_at TIMESTAMP DEFAULT NOW() NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_memory_user_id ON memory_records(user_id);
CREATE INDEX IF NOT EXISTS idx_memory_org_id ON memory_records(org_id);

View File

@@ -0,0 +1,23 @@
{
"name": "memory-svc",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"db:migrate": "tsx src/db/push.ts"
},
"dependencies": {
"fastify": "^4.28.1",
"@fastify/cors": "^9.0.1",
"drizzle-orm": "^0.40.1",
"postgres": "^3.4.4"
},
"devDependencies": {
"@types/node": "^22.10.0",
"tsx": "^4.19.2",
"typescript": "^5.7.2"
}
}

View File

@@ -0,0 +1,8 @@
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema.js';
const connectionString =
process.env.POSTGRES_URL ?? 'postgresql://gooseek:gooseek@localhost:5432/gooseek';
const client = postgres(connectionString);
export const db = drizzle(client, { schema });

View File

@@ -0,0 +1,18 @@
import postgres from 'postgres';
import { readFileSync } from 'fs';
import { join } from 'path';
const connectionString =
process.env.POSTGRES_URL ?? 'postgresql://gooseek:gooseek@localhost:5432/gooseek';
const client = postgres(connectionString);
async function main() {
const sql = readFileSync(join(process.cwd(), 'drizzle/0000_init.sql'), 'utf8');
await client.unsafe(sql);
console.log('Schema applied');
await client.end();
}
main().catch((e) => {
console.error(e);
process.exit(1);
});

View File

@@ -0,0 +1,11 @@
import { jsonb, pgTable, text, timestamp } from 'drizzle-orm/pg-core';
export const memories = pgTable('memory_records', {
id: text('id').primaryKey(),
userId: text('user_id').notNull(),
orgId: text('org_id'),
key: text('key').notNull(),
value: text('value').notNull(),
embedding: jsonb('embedding').$type<number[]>(),
createdAt: timestamp('created_at').defaultNow().notNull(),
});

View File

@@ -0,0 +1,138 @@
/**
* memory-svc — персональная память AI, Enterprise Memory
* docs/architecture: 01-perplexity-analogue-design.md §2.2.K, §5.9
* API: GET/POST/DELETE /api/v1/memory
*/
import Fastify from 'fastify';
import cors from '@fastify/cors';
import { eq, and, or, desc } from 'drizzle-orm';
import { db } from './db/index.js';
import { memories } from './db/schema.js';
const PORT = parseInt(process.env.PORT ?? '3010', 10);
const AUTH_SERVICE_URL = process.env.AUTH_SERVICE_URL ?? '';
const app = Fastify({ logger: true });
await app.register(cors, { origin: true });
async function getUserIdFromToken(authHeader: string | undefined): Promise<string | null> {
if (!authHeader?.startsWith('Bearer ')) return null;
const token = authHeader.slice(7);
if (!AUTH_SERVICE_URL) {
return token ? 'guest' : null;
}
try {
const res = await fetch(`${AUTH_SERVICE_URL.replace(/\/$/, '')}/api/auth/me`, {
headers: { Authorization: `Bearer ${token}` },
signal: AbortSignal.timeout(5000),
});
if (!res.ok) return null;
const data = (await res.json()) as { id?: string };
return data.id ?? null;
} catch {
return null;
}
}
app.get('/health', async () => ({ status: 'ok' }));
app.get('/metrics', async (_req, reply) => {
reply.header('Content-Type', 'text/plain; charset=utf-8');
return reply.send('# HELP gooseek_up Service is up (1) or down (0)\n# TYPE gooseek_up gauge\ngooseek_up 1\n');
});
app.get('/ready', async () => ({ status: 'ready' }));
app.get('/api/v1/memory', async (req, reply) => {
const userId = await getUserIdFromToken(req.headers.authorization);
if (!userId) {
return reply.status(401).send({ error: 'Authorization required' });
}
const orgId = (req.query as { orgId?: string }).orgId;
try {
const conditions = orgId
? or(eq(memories.userId, userId), eq(memories.orgId, orgId))
: eq(memories.userId, userId);
const list = await db
.select()
.from(memories)
.where(conditions)
.orderBy(desc(memories.createdAt));
const items = list.map((r) => ({
id: r.id,
key: r.key,
value: r.value,
orgId: r.orgId ?? undefined,
createdAt: r.createdAt?.toISOString?.() ?? r.createdAt,
}));
return { items };
} catch (err) {
req.log.error(err);
return reply.status(500).send({ error: 'Failed to fetch memory' });
}
});
app.post<{
Body: { key?: string; value?: string; orgId?: string; embedding?: number[] };
}>('/api/v1/memory', async (req, reply) => {
const userId = await getUserIdFromToken(req.headers.authorization);
if (!userId) {
return reply.status(401).send({ error: 'Authorization required' });
}
const { key, value, orgId, embedding } = req.body ?? {};
if (!key?.trim() || !value?.trim()) {
return reply.status(400).send({ error: 'key and value are required' });
}
const id = crypto.randomUUID();
try {
await db.insert(memories).values({
id,
userId,
orgId: orgId ?? null,
key: key.trim(),
value: value.trim(),
embedding: embedding ?? null,
});
const [inserted] = await db.select().from(memories).where(eq(memories.id, id));
return reply.status(201).send({
id: inserted.id,
key: inserted.key,
value: inserted.value,
orgId: inserted.orgId ?? undefined,
createdAt: inserted.createdAt?.toISOString?.() ?? inserted.createdAt,
});
} catch (err) {
req.log.error(err);
return reply.status(500).send({ error: 'Failed to create memory' });
}
});
app.delete<{ Params: { id: string } }>('/api/v1/memory/:id', async (req, reply) => {
const userId = await getUserIdFromToken(req.headers.authorization);
if (!userId) {
return reply.status(401).send({ error: 'Authorization required' });
}
const { id } = req.params;
try {
const [existing] = await db
.select()
.from(memories)
.where(and(eq(memories.id, id), eq(memories.userId, userId)));
if (!existing) {
return reply.status(404).send({ error: 'Memory not found' });
}
await db.delete(memories).where(eq(memories.id, id));
return reply.status(204).send();
} catch (err) {
req.log.error(err);
return reply.status(500).send({ error: 'Failed to delete memory' });
}
});
try {
await app.listen({ port: PORT, host: '0.0.0.0' });
console.log('memory-svc listening on :' + PORT);
} catch (err) {
console.error(err);
process.exit(1);
}

View File

@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}