// Матрица Судьбы · эндпоинт платного разбора (AI). // Бесплатный тизер генерится на странице без AI — этот сервер вызывается // ТОЛЬКО после оплаты, для полного текста. // // Запуск: ANTHROPIC_API_KEY=sk-... node razbor-server.js // Env: // ANTHROPIC_API_KEY — обязательно // PORT — по умолчанию 8787 // MODEL — по умолчанию claude-haiku-4-5 (для полного качества — claude-sonnet-5) // SKILL_PATH — путь к скиллу-интерпретатору, по умолчанию ./matrix-interpreter_v1.md // ALLOW_ORIGIN — CORS, по умолчанию * (сузить до домена сайта в проде) // // API: POST /razbor { focus: "general"|"money"|"love"|"purpose", kind: "full"|"teaser", calc: {…JSON калькулятора} } // → { text: "..." } // Кэш: пара «дата рождения + фокус + kind» (расчёт детерминирован). // Лимит: 20 запросов с IP в сутки (эндпоинт платный, лимит — страховка от абьюза). const http = require('http'); const fs = require('fs'); const path = require('path'); const PORT = +(process.env.PORT || 8787); const MODEL = process.env.MODEL || 'claude-haiku-4-5'; const API_KEY = process.env.ANTHROPIC_API_KEY; const SKILL_PATH = process.env.SKILL_PATH || path.join(__dirname, 'matrix-interpreter_v1.md'); const ALLOW_ORIGIN = process.env.ALLOW_ORIGIN || '*'; const RATE_LIMIT = 20; // запросов с IP в сутки if (!API_KEY) { console.error('ANTHROPIC_API_KEY не задан'); process.exit(1); } const SKILL = fs.readFileSync(SKILL_PATH, 'utf8'); const FOCUSES = new Set(['general', 'money', 'love', 'purpose']); const FOCUS_RU = { general: 'общий', money: 'деньги', love: 'отношения', purpose: 'предназначение' }; const cache = new Map(); // key → text const rate = new Map(); // ip → { day, count } function limited(ip) { const day = new Date().toISOString().slice(0, 10); const rec = rate.get(ip); if (!rec || rec.day !== day) { rate.set(ip, { day, count: 1 }); return false; } if (rec.count >= RATE_LIMIT) return true; rec.count++; return false; } async function generate(focus, kind, calc) { const task = kind === 'teaser' ? 'Напиши ТИЗЕР (бесплатно, 450–550 слов): одно попадание и честно открытая петля. Петля открывает только те двери, которые закрывает платный продукт этого же фокуса.' : 'Напиши ПОЛНЫЙ разбор (1200–1600 слов): имя узора → центр → узор целиком → тема фокуса развёрнуто → род → возрастное окно (самая сильная часть) → поворот → три открытых вопроса.'; const res = await fetch('https://api.anthropic.com/v1/messages', { method: 'POST', headers: { 'x-api-key': API_KEY, 'anthropic-version': '2023-06-01', 'content-type': 'application/json', }, body: JSON.stringify({ model: MODEL, max_tokens: kind === 'teaser' ? 1200 : 3000, system: SKILL, messages: [{ role: 'user', content: `Фокус: ${FOCUS_RU[focus]}.\n${task}\nОбращение — на «ты».\n\nДанные расчёта:\n${JSON.stringify(calc, null, 1)}`, }], }), }); if (!res.ok) throw new Error(`API ${res.status}: ${await res.text()}`); const data = await res.json(); return data.content.map(b => b.text || '').join(''); } function send(res, code, obj) { res.writeHead(code, { 'content-type': 'application/json; charset=utf-8', 'access-control-allow-origin': ALLOW_ORIGIN, 'access-control-allow-headers': 'content-type', 'access-control-allow-methods': 'POST, OPTIONS', }); res.end(JSON.stringify(obj)); } http.createServer(async (req, res) => { if (req.method === 'OPTIONS') return send(res, 204, {}); if (req.method !== 'POST' || req.url !== '/razbor') return send(res, 404, { error: 'not found' }); const ip = (req.headers['x-forwarded-for'] || req.socket.remoteAddress || '').split(',')[0].trim(); if (limited(ip)) return send(res, 429, { error: 'rate limit' }); let body = ''; req.on('data', ch => { body += ch; if (body.length > 200000) req.destroy(); }); req.on('end', async () => { try { const { focus = 'general', kind = 'full', calc } = JSON.parse(body || '{}'); if (!FOCUSES.has(focus) || !calc || !calc.birthDate || !calc.core) { return send(res, 400, { error: 'bad request: нужны focus и calc (JSON калькулятора)' }); } const key = `${calc.birthDate}|${focus}|${kind}`; if (cache.has(key)) return send(res, 200, { text: cache.get(key), cached: true }); const text = await generate(focus, kind, calc); cache.set(key, text); send(res, 200, { text }); } catch (e) { console.error(e); send(res, 500, { error: 'generation failed' }); } }); }).listen(PORT, () => console.log(`razbor-server: http://localhost:${PORT}/razbor · модель ${MODEL}`));