1 line
130 KiB
JSON
1 line
130 KiB
JSON
{"repo": "bilarius1-tech/proektmap", "files": {"src/app/api/blog/auto-publish/route.ts": {"3e832a87dce00ff53a610489a1b0edf5226ba106": {"t": 1785745598.9472103, "n": 6617, "h": "a2da58cef74bda3a", "c": "import { NextResponse } from \"next/server\";\r\nimport { getDb } from \"@/lib/db/index\";\r\n\r\nfunction translit(text: string): string {\r\n const map: any = { а:\"a\",б:\"b\",в:\"v\",г:\"g\",д:\"d\",е:\"e\",ё:\"yo\",ж:\"zh\",з:\"z\",и:\"i\",й:\"y\",к:\"k\",л:\"l\",м:\"m\",н:\"n\",о:\"o\",п:\"p\",р:\"r\",с:\"s\",т:\"t\",у:\"u\",ф:\"f\",х:\"h\",ц:\"ts\",ч:\"ch\",ш:\"sh\",щ:\"sch\",ъ:\"\",ы:\"y\",ь:\"\",э:\"e\",ю:\"yu\",я:\"ya\" };\r\n return text.toLowerCase().split(\"\").map((c: string) => map[c] || c).join(\"\");\r\n}\r\n\r\nexport async function POST() {\r\n const db = await getDb();\r\n const feeds = await db.blogFeed.findMany({ where: { isActive: true } });\r\n const admin = await db.user.findFirst({ where: { role: \"admin\" } });\r\n if (!admin) return NextResponse.json({ error: \"Админ не найден\" }, { status: 500 });\r\n\r\n let key = process.env.DEEPSEEK_API_KEY;\r\n try { const s = await db.siteSettings.findUnique({ where: { id: \"main\" } }); if (s?.deepseekApiKey) key = s.deepseekApiKey; } catch {}\r\n if (!key) return NextResponse.json({ error: \"Нет DEEPSEEK_API_KEY\" }, { status: 500 });\r\n\r\n const results: any[] = [];\r\n\r\n for (const feed of feeds) {\r\n try {\r\n const res = await fetch(feed.url, {\r\n headers: { \"Accept\": \"application/json\", \"User-Agent\": \"ProektMap/1.0\" },\r\n signal: AbortSignal.timeout(15000),\r\n });\r\n const raw = await res.text();\r\n\r\n let items: { title: string; link: string; description: string; image?: string }[] = [];\r\n if (feed.type === \"json\") {\r\n try {\r\n const json = JSON.parse(raw);\r\n if (json.hits) items = json.hits.map((h: any) => ({ title: h.title || h.story_title || \"\", link: h.url || `https://news.ycombinator.com/item?id=${h.objectID}`, description: h.story_text || h.comment_text || \"\" }));\r\n else if (json.data?.children) items = json.data.children.map((c: any) => { const d = c.data; let img = d.thumbnail?.startsWith(\"http\") ? d.thumbnail : d.preview?.images?.[0]?.source?.url?.replace(/&/g, \"&\"); return { title: d.title, link: `https://reddit.com${d.permalink}`, description: d.selftext || \"\", image: img }; });\r\n else if (json.items) items = json.items.slice(0, 5).map((i: any) => ({ title: i.full_name || i.name, link: i.html_url || i.url, description: i.description || \"\", image: i.owner?.avatar_url || null }));\r\n } catch {}\r\n } else if (feed.type === \"xml\") {\r\n const entries = raw.match(/<entry>[\\s\\S]*?<\\/entry>/g) || [];\r\n items = entries.slice(0, 3).map((e: any) => ({ title: (e.match(/<title>(.*?)<\\/title>/)?.[1] || \"\").replace(/\\s+/g, \" \").trim(), link: e.match(/<id>(.*?)<\\/id>/)?.[1] || \"\", description: (e.match(/<summary>(.*?)<\\/summary>/)?.[1] || \"\").replace(/\\s+/g, \" \").trim() }));\r\n }\r\n\r\n if (items.length === 0) { results.push({ feed: feed.name, status: \"empty\" }); continue; }\r\n\r\n for (const item of items.slice(0, 2)) {\r\n if (!item.title || item.title.length < 15) continue;\r\n const exists = await db.blogPost.findFirst({ where: { metaDesc: { contains: item.link.slice(0, 50) } } });\r\n if (exists) continue;\r\n\r\n // Download image\r\n let coverImage = `https://proektmap.ru/api/og?title=${encodeURIComponent(item.title)}&category=${encodeURIComponent(feed.category)}`;\r\n if (item.image) {\r\n try {\r\n const imgRes = await fetch(item.image, { signal: AbortSignal.timeout(5000) });\r\n if (imgRes.ok && imgRes.headers.get(\"content-type\")?.startsWith(\"image\")) {\r\n const buffer = Buffer.from(await imgRes.arrayBuffer());\r\n const ext = item.image.split(\".\").pop()?.split(\"?\")[0] || \"jpg\";\r\n const filename = `blog-${Date.now()}-${Math.random().toString(36).slice(2, 6)}.${ext}`;\r\n const { writeFile, mkdir } = await import(\"fs/promises\");\r\n const { join } = await import(\"path\");\r\n await mkdir(join(process.cwd(), \"public\", \"uploads\"), { recursive: true });\r\n await writeFile(join(process.cwd(), \"public\", \"uploads\", filename), buffer);\r\n coverImage = `/uploads/${filename}`;\r\n }\r\n } catch {}\r\n }\r\n\r\n // AI translates\r\n const prompt = `Ты редактор блога об AI. Напиши заметку на РУССКОМ языке (250-350 слов).\r\n\r\nЗаголовок оригинала: ${item.title}\r\nИсточник: ${item.link}\r\n\r\nВАЖНО:\r\n1. Начни с заголовка строго в формате: \"ЗАГОЛОВОК: русский перевод\"\r\n2. Заголовок должен быть ПОЛНОСТЬЮ на русском, без английских слов\r\n3. Затем сам текст заметки\r\n4. В конце добавь: \"📎 [Источник](${item.link})\"\r\n5. Все ссылки на GitHub делай в формате [название](url)`;\r\n\r\n const aiRes = await fetch(\"https://api.deepseek.com/v1/chat/completions\", {\r\n method: \"POST\", headers: { \"Content-Type\": \"application/json\", \"Authorization\": `Bearer ${key}` },\r\n body: JSON.stringify({ model: \"deepseek-chat\", messages: [{ role: \"user\", content: prompt }], max_tokens: 1000 }),\r\n });\r\n\r\n const aiData = await aiRes.json();\r\n const fullText = aiData.choices?.[0]?.message?.content;\r\n if (!fullText) continue;\r\n\r\n // Extract title\r\n let title = item.title;\r\n const tm = fullText.match(/ЗАГОЛОВОК:\\s*(.+)/);\r\n if (tm) title = tm[1].trim();\r\n\r\n // Remove title line from content\r\n const content = fullText.replace(/ЗАГОЛОВОК:\\s*.+(\\n|$)/, \"\").trim();\r\n const excerpt = content.replace(/[#*\\[\\]()]/g, \"\").slice(0, 200).replace(/\\n/g, \" \");\r\n\r\n // Generate clean transliterated slug\r\n const clean = title.replace(/[^a-zа-я0-9\\s-]/gi, \"\").trim();\r\n const slug = translit(clean).replace(/[^a-z0-9]+/g, \"-\").replace(/^-+|-+$/g, \"\").slice(0, 70);\r\n\r\n let cat = await db.blogCategory.findFirst({ where: { name: feed.category } });\r\n if (!cat) cat = await db.blogCategory.create({ data: { name: feed.category, slug: feed.category.toLowerCase().replace(/[^a-zа-я0-9]+/g, \"-\") } });\r\n\r\n await db.blogPost.create({\r\n data: { title, slug, content, excerpt, coverImage, status: \"published\", authorId: admin.id, categoryId: cat.id, tags: \"AI,новости\", aiGenerated: true, aiModel: \"deepseek-chat\", metaTitle: title + \" — Карта роста\", metaDesc: excerpt, publishedAt: new Date() },\r\n });\r\n\r\n results.push({ feed: feed.name, title: title.slice(0, 60), slug, status: \"published\" });\r\n }\r\n\r\n await db.blogFeed.update({ where: { id: feed.id }, data: { lastFetched: new Date() } });\r\n } catch (e: any) {\r\n results.push({ feed: feed.name, status: \"error\", reason: e.message.slice(0, 80) });\r\n }\r\n }\r\n\r\n return NextResponse.json({ results });\r\n}\r\n"}, "0142b33de5140d1eb6b54545800e5944302dd4c2": {"t": 1785754644.7276707, "n": 6617, "h": "a2da58cef74bda3a", "c": "import { NextResponse } from \"next/server\";\r\nimport { getDb } from \"@/lib/db/index\";\r\n\r\nfunction translit(text: string): string {\r\n const map: any = { а:\"a\",б:\"b\",в:\"v\",г:\"g\",д:\"d\",е:\"e\",ё:\"yo\",ж:\"zh\",з:\"z\",и:\"i\",й:\"y\",к:\"k\",л:\"l\",м:\"m\",н:\"n\",о:\"o\",п:\"p\",р:\"r\",с:\"s\",т:\"t\",у:\"u\",ф:\"f\",х:\"h\",ц:\"ts\",ч:\"ch\",ш:\"sh\",щ:\"sch\",ъ:\"\",ы:\"y\",ь:\"\",э:\"e\",ю:\"yu\",я:\"ya\" };\r\n return text.toLowerCase().split(\"\").map((c: string) => map[c] || c).join(\"\");\r\n}\r\n\r\nexport async function POST() {\r\n const db = await getDb();\r\n const feeds = await db.blogFeed.findMany({ where: { isActive: true } });\r\n const admin = await db.user.findFirst({ where: { role: \"admin\" } });\r\n if (!admin) return NextResponse.json({ error: \"Админ не найден\" }, { status: 500 });\r\n\r\n let key = process.env.DEEPSEEK_API_KEY;\r\n try { const s = await db.siteSettings.findUnique({ where: { id: \"main\" } }); if (s?.deepseekApiKey) key = s.deepseekApiKey; } catch {}\r\n if (!key) return NextResponse.json({ error: \"Нет DEEPSEEK_API_KEY\" }, { status: 500 });\r\n\r\n const results: any[] = [];\r\n\r\n for (const feed of feeds) {\r\n try {\r\n const res = await fetch(feed.url, {\r\n headers: { \"Accept\": \"application/json\", \"User-Agent\": \"ProektMap/1.0\" },\r\n signal: AbortSignal.timeout(15000),\r\n });\r\n const raw = await res.text();\r\n\r\n let items: { title: string; link: string; description: string; image?: string }[] = [];\r\n if (feed.type === \"json\") {\r\n try {\r\n const json = JSON.parse(raw);\r\n if (json.hits) items = json.hits.map((h: any) => ({ title: h.title || h.story_title || \"\", link: h.url || `https://news.ycombinator.com/item?id=${h.objectID}`, description: h.story_text || h.comment_text || \"\" }));\r\n else if (json.data?.children) items = json.data.children.map((c: any) => { const d = c.data; let img = d.thumbnail?.startsWith(\"http\") ? d.thumbnail : d.preview?.images?.[0]?.source?.url?.replace(/&/g, \"&\"); return { title: d.title, link: `https://reddit.com${d.permalink}`, description: d.selftext || \"\", image: img }; });\r\n else if (json.items) items = json.items.slice(0, 5).map((i: any) => ({ title: i.full_name || i.name, link: i.html_url || i.url, description: i.description || \"\", image: i.owner?.avatar_url || null }));\r\n } catch {}\r\n } else if (feed.type === \"xml\") {\r\n const entries = raw.match(/<entry>[\\s\\S]*?<\\/entry>/g) || [];\r\n items = entries.slice(0, 3).map((e: any) => ({ title: (e.match(/<title>(.*?)<\\/title>/)?.[1] || \"\").replace(/\\s+/g, \" \").trim(), link: e.match(/<id>(.*?)<\\/id>/)?.[1] || \"\", description: (e.match(/<summary>(.*?)<\\/summary>/)?.[1] || \"\").replace(/\\s+/g, \" \").trim() }));\r\n }\r\n\r\n if (items.length === 0) { results.push({ feed: feed.name, status: \"empty\" }); continue; }\r\n\r\n for (const item of items.slice(0, 2)) {\r\n if (!item.title || item.title.length < 15) continue;\r\n const exists = await db.blogPost.findFirst({ where: { metaDesc: { contains: item.link.slice(0, 50) } } });\r\n if (exists) continue;\r\n\r\n // Download image\r\n let coverImage = `https://proektmap.ru/api/og?title=${encodeURIComponent(item.title)}&category=${encodeURIComponent(feed.category)}`;\r\n if (item.image) {\r\n try {\r\n const imgRes = await fetch(item.image, { signal: AbortSignal.timeout(5000) });\r\n if (imgRes.ok && imgRes.headers.get(\"content-type\")?.startsWith(\"image\")) {\r\n const buffer = Buffer.from(await imgRes.arrayBuffer());\r\n const ext = item.image.split(\".\").pop()?.split(\"?\")[0] || \"jpg\";\r\n const filename = `blog-${Date.now()}-${Math.random().toString(36).slice(2, 6)}.${ext}`;\r\n const { writeFile, mkdir } = await import(\"fs/promises\");\r\n const { join } = await import(\"path\");\r\n await mkdir(join(process.cwd(), \"public\", \"uploads\"), { recursive: true });\r\n await writeFile(join(process.cwd(), \"public\", \"uploads\", filename), buffer);\r\n coverImage = `/uploads/${filename}`;\r\n }\r\n } catch {}\r\n }\r\n\r\n // AI translates\r\n const prompt = `Ты редактор блога об AI. Напиши заметку на РУССКОМ языке (250-350 слов).\r\n\r\nЗаголовок оригинала: ${item.title}\r\nИсточник: ${item.link}\r\n\r\nВАЖНО:\r\n1. Начни с заголовка строго в формате: \"ЗАГОЛОВОК: русский перевод\"\r\n2. Заголовок должен быть ПОЛНОСТЬЮ на русском, без английских слов\r\n3. Затем сам текст заметки\r\n4. В конце добавь: \"📎 [Источник](${item.link})\"\r\n5. Все ссылки на GitHub делай в формате [название](url)`;\r\n\r\n const aiRes = await fetch(\"https://api.deepseek.com/v1/chat/completions\", {\r\n method: \"POST\", headers: { \"Content-Type\": \"application/json\", \"Authorization\": `Bearer ${key}` },\r\n body: JSON.stringify({ model: \"deepseek-chat\", messages: [{ role: \"user\", content: prompt }], max_tokens: 1000 }),\r\n });\r\n\r\n const aiData = await aiRes.json();\r\n const fullText = aiData.choices?.[0]?.message?.content;\r\n if (!fullText) continue;\r\n\r\n // Extract title\r\n let title = item.title;\r\n const tm = fullText.match(/ЗАГОЛОВОК:\\s*(.+)/);\r\n if (tm) title = tm[1].trim();\r\n\r\n // Remove title line from content\r\n const content = fullText.replace(/ЗАГОЛОВОК:\\s*.+(\\n|$)/, \"\").trim();\r\n const excerpt = content.replace(/[#*\\[\\]()]/g, \"\").slice(0, 200).replace(/\\n/g, \" \");\r\n\r\n // Generate clean transliterated slug\r\n const clean = title.replace(/[^a-zа-я0-9\\s-]/gi, \"\").trim();\r\n const slug = translit(clean).replace(/[^a-z0-9]+/g, \"-\").replace(/^-+|-+$/g, \"\").slice(0, 70);\r\n\r\n let cat = await db.blogCategory.findFirst({ where: { name: feed.category } });\r\n if (!cat) cat = await db.blogCategory.create({ data: { name: feed.category, slug: feed.category.toLowerCase().replace(/[^a-zа-я0-9]+/g, \"-\") } });\r\n\r\n await db.blogPost.create({\r\n data: { title, slug, content, excerpt, coverImage, status: \"published\", authorId: admin.id, categoryId: cat.id, tags: \"AI,новости\", aiGenerated: true, aiModel: \"deepseek-chat\", metaTitle: title + \" — Карта роста\", metaDesc: excerpt, publishedAt: new Date() },\r\n });\r\n\r\n results.push({ feed: feed.name, title: title.slice(0, 60), slug, status: \"published\" });\r\n }\r\n\r\n await db.blogFeed.update({ where: { id: feed.id }, data: { lastFetched: new Date() } });\r\n } catch (e: any) {\r\n results.push({ feed: feed.name, status: \"error\", reason: e.message.slice(0, 80) });\r\n }\r\n }\r\n\r\n return NextResponse.json({ results });\r\n}\r\n"}, "66dae74d590d7ae68e46caf1938ed51cfd4f154e": {"t": 1785754646.055078, "n": 6498, "h": "8c8f457d9a6a5f7e", "c": "import { NextResponse } from \"next/server\";\nimport { getDb } from \"@/lib/db/index\";\n\nfunction translit(text: string): string {\n const map: any = { а:\"a\",б:\"b\",в:\"v\",г:\"g\",д:\"d\",е:\"e\",ё:\"yo\",ж:\"zh\",з:\"z\",и:\"i\",й:\"y\",к:\"k\",л:\"l\",м:\"m\",н:\"n\",о:\"o\",п:\"p\",р:\"r\",с:\"s\",т:\"t\",у:\"u\",ф:\"f\",х:\"h\",ц:\"ts\",ч:\"ch\",ш:\"sh\",щ:\"sch\",ъ:\"\",ы:\"y\",ь:\"\",э:\"e\",ю:\"yu\",я:\"ya\" };\n return text.toLowerCase().split(\"\").map((c: string) => map[c] || c).join(\"\");\n}\n\nexport async function POST() {\n const db = await getDb();\n const feeds = await db.blogFeed.findMany({ where: { isActive: true } });\n const admin = await db.user.findFirst({ where: { role: \"admin\" } });\n if (!admin) return NextResponse.json({ error: \"Админ не найден\" }, { status: 500 });\n\n let key = process.env.DEEPSEEK_API_KEY;\n try { const s = await db.siteSettings.findUnique({ where: { id: \"main\" } }); if (s?.deepseekApiKey) key = s.deepseekApiKey; } catch {}\n if (!key) return NextResponse.json({ error: \"Нет DEEPSEEK_API_KEY\" }, { status: 500 });\n\n const results: any[] = [];\n\n for (const feed of feeds) {\n try {\n const res = await fetch(feed.url, {\n headers: { \"Accept\": \"application/json\", \"User-Agent\": \"ProektMap/1.0\" },\n signal: AbortSignal.timeout(15000),\n });\n const raw = await res.text();\n\n let items: { title: string; link: string; description: string; image?: string }[] = [];\n if (feed.type === \"json\") {\n try {\n const json = JSON.parse(raw);\n if (json.hits) items = json.hits.map((h: any) => ({ title: h.title || h.story_title || \"\", link: h.url || `https://news.ycombinator.com/item?id=${h.objectID}`, description: h.story_text || h.comment_text || \"\" }));\n else if (json.data?.children) items = json.data.children.map((c: any) => { const d = c.data; let img = d.thumbnail?.startsWith(\"http\") ? d.thumbnail : d.preview?.images?.[0]?.source?.url?.replace(/&/g, \"&\"); return { title: d.title, link: `https://reddit.com${d.permalink}`, description: d.selftext || \"\", image: img }; });\n else if (json.items) items = json.items.slice(0, 5).map((i: any) => ({ title: i.full_name || i.name, link: i.html_url || i.url, description: i.description || \"\", image: i.owner?.avatar_url || null }));\n } catch {}\n } else if (feed.type === \"xml\") {\n const entries = raw.match(/<entry>[\\s\\S]*?<\\/entry>/g) || [];\n items = entries.slice(0, 3).map((e: any) => ({ title: (e.match(/<title>(.*?)<\\/title>/)?.[1] || \"\").replace(/\\s+/g, \" \").trim(), link: e.match(/<id>(.*?)<\\/id>/)?.[1] || \"\", description: (e.match(/<summary>(.*?)<\\/summary>/)?.[1] || \"\").replace(/\\s+/g, \" \").trim() }));\n }\n\n if (items.length === 0) { results.push({ feed: feed.name, status: \"empty\" }); continue; }\n\n for (const item of items.slice(0, 2)) {\n if (!item.title || item.title.length < 15) continue;\n const exists = await db.blogPost.findFirst({ where: { metaDesc: { contains: item.link.slice(0, 50) } } });\n if (exists) continue;\n\n // Download image\n let coverImage = `https://proektmap.ru/api/og?title=${encodeURIComponent(item.title)}&category=${encodeURIComponent(feed.category)}`;\n if (item.image) {\n try {\n const imgRes = await fetch(item.image, { signal: AbortSignal.timeout(5000) });\n if (imgRes.ok && imgRes.headers.get(\"content-type\")?.startsWith(\"image\")) {\n const buffer = Buffer.from(await imgRes.arrayBuffer());\n const ext = item.image.split(\".\").pop()?.split(\"?\")[0] || \"jpg\";\n const filename = `blog-${Date.now()}-${Math.random().toString(36).slice(2, 6)}.${ext}`;\n const { writeFile, mkdir } = await import(\"fs/promises\");\n const { join } = await import(\"path\");\n await mkdir(join(process.cwd(), \"public\", \"uploads\"), { recursive: true });\n await writeFile(join(process.cwd(), \"public\", \"uploads\", filename), buffer);\n coverImage = `/uploads/${filename}`;\n }\n } catch {}\n }\n\n // AI translates\n const prompt = `Ты редактор блога об AI. Напиши заметку на РУССКОМ языке (250-350 слов).\n\nЗаголовок оригинала: ${item.title}\nИсточник: ${item.link}\n\nВАЖНО:\n1. Начни с заголовка строго в формате: \"ЗАГОЛОВОК: русский перевод\"\n2. Заголовок должен быть ПОЛНОСТЬЮ на русском, без английских слов\n3. Затем сам текст заметки\n4. В конце добавь: \"📎 [Источник](${item.link})\"\n5. Все ссылки на GitHub делай в формате [название](url)`;\n\n const aiRes = await fetch(\"https://api.deepseek.com/v1/chat/completions\", {\n method: \"POST\", headers: { \"Content-Type\": \"application/json\", \"Authorization\": `Bearer ${key}` },\n body: JSON.stringify({ model: \"deepseek-chat\", messages: [{ role: \"user\", content: prompt }], max_tokens: 1000 }),\n });\n\n const aiData = await aiRes.json();\n const fullText = aiData.choices?.[0]?.message?.content;\n if (!fullText) continue;\n\n // Extract title\n let title = item.title;\n const tm = fullText.match(/ЗАГОЛОВОК:\\s*(.+)/);\n if (tm) title = tm[1].trim();\n\n // Remove title line from content\n const content = fullText.replace(/ЗАГОЛОВОК:\\s*.+(\\n|$)/, \"\").trim();\n const excerpt = content.replace(/[#*\\[\\]()]/g, \"\").slice(0, 200).replace(/\\n/g, \" \");\n\n // Generate clean transliterated slug\n const clean = title.replace(/[^a-zа-я0-9\\s-]/gi, \"\").trim();\n const slug = translit(clean).replace(/[^a-z0-9]+/g, \"-\").replace(/^-+|-+$/g, \"\").slice(0, 70);\n\n let cat = await db.blogCategory.findFirst({ where: { name: feed.category } });\n if (!cat) cat = await db.blogCategory.create({ data: { name: feed.category, slug: feed.category.toLowerCase().replace(/[^a-zа-я0-9]+/g, \"-\") } });\n\n await db.blogPost.create({\n data: { title, slug, content, excerpt, coverImage, status: \"published\", authorId: admin.id, categoryId: cat.id, tags: \"AI,новости\", aiGenerated: true, aiModel: \"deepseek-chat\", metaTitle: title + \" — Карта роста\", metaDesc: excerpt, publishedAt: new Date() },\n });\n\n results.push({ feed: feed.name, title: title.slice(0, 60), slug, status: \"published\" });\n }\n\n await db.blogFeed.update({ where: { id: feed.id }, data: { lastFetched: new Date() } });\n } catch (e: any) {\n results.push({ feed: feed.name, status: \"error\", reason: e.message.slice(0, 80) });\n }\n }\n\n return NextResponse.json({ results });\n}\n"}, "603b931ce7945175f6029cd555486e1272a0276a": {"t": 1785754647.4200015, "n": 6526, "h": "cdd104fa2630a243", "c": "import { NextResponse } from \"next/server\";\nimport { getDb } from \"@/lib/db/index\";\n\nfunction translit(text: string): string {\n const map: any = { а:\"a\",б:\"b\",в:\"v\",г:\"g\",д:\"d\",е:\"e\",ё:\"yo\",ж:\"zh\",з:\"z\",и:\"i\",й:\"y\",к:\"k\",л:\"l\",м:\"m\",н:\"n\",о:\"o\",п:\"p\",р:\"r\",с:\"s\",т:\"t\",у:\"u\",ф:\"f\",х:\"h\",ц:\"ts\",ч:\"ch\",ш:\"sh\",щ:\"sch\",ъ:\"\",ы:\"y\",ь:\"\",э:\"e\",ю:\"yu\",я:\"ya\" };\n return text.toLowerCase().split(\"\").map((c: string) => map[c] || c).join(\"\");\n}\n\nexport async function POST() {\n const db = await getDb();\n const feeds = await db.blogFeed.findMany({ where: { isActive: true } });\n const admin = await db.user.findFirst({ where: { role: \"admin\" } });\n if (!admin) return NextResponse.json({ error: \"Админ не найден\" }, { status: 500 });\n\n let key = process.env.DEEPSEEK_API_KEY;\n try { const s = await db.siteSettings.findUnique({ where: { id: \"main\" } }); if (s?.deepseekApiKey) key = s.deepseekApiKey; } catch {}\n if (!key) return NextResponse.json({ error: \"Нет DEEPSEEK_API_KEY\" }, { status: 500 });\n\n const results: any[] = [];\n\n for (const feed of feeds) {\n try {\n const res = await fetch(feed.url, {\n headers: feed.type === \"json\" ? { \"Accept\": \"application/json\" } : { \"User-Agent\": \"ProektMap/1.0\" },\n signal: AbortSignal.timeout(15000),\n });\n const raw = await res.text();\n\n let items: { title: string; link: string; description: string; image?: string }[] = [];\n if (feed.type === \"json\") {\n try {\n const json = JSON.parse(raw);\n if (json.hits) items = json.hits.map((h: any) => ({ title: h.title || h.story_title || \"\", link: h.url || `https://news.ycombinator.com/item?id=${h.objectID}`, description: h.story_text || h.comment_text || \"\" }));\n else if (json.data?.children) items = json.data.children.map((c: any) => { const d = c.data; let img = d.thumbnail?.startsWith(\"http\") ? d.thumbnail : d.preview?.images?.[0]?.source?.url?.replace(/&/g, \"&\"); return { title: d.title, link: `https://reddit.com${d.permalink}`, description: d.selftext || \"\", image: img }; });\n else if (json.items) items = json.items.slice(0, 5).map((i: any) => ({ title: i.full_name || i.name, link: i.html_url || i.url, description: i.description || \"\", image: i.owner?.avatar_url || null }));\n } catch {}\n } else if (feed.type === \"xml\") {\n const entries = raw.match(/<entry>[\\s\\S]*?<\\/entry>/g) || [];\n items = entries.slice(0, 3).map((e: any) => ({ title: (e.match(/<title>(.*?)<\\/title>/)?.[1] || \"\").replace(/\\s+/g, \" \").trim(), link: e.match(/<id>(.*?)<\\/id>/)?.[1] || \"\", description: (e.match(/<summary>(.*?)<\\/summary>/)?.[1] || \"\").replace(/\\s+/g, \" \").trim() }));\n }\n\n if (items.length === 0) { results.push({ feed: feed.name, status: \"empty\" }); continue; }\n\n for (const item of items.slice(0, 2)) {\n if (!item.title || item.title.length < 15) continue;\n const exists = await db.blogPost.findFirst({ where: { metaDesc: { contains: item.link.slice(0, 50) } } });\n if (exists) continue;\n\n // Download image\n let coverImage = `https://proektmap.ru/api/og?title=${encodeURIComponent(item.title)}&category=${encodeURIComponent(feed.category)}`;\n if (item.image) {\n try {\n const imgRes = await fetch(item.image, { signal: AbortSignal.timeout(5000) });\n if (imgRes.ok && imgRes.headers.get(\"content-type\")?.startsWith(\"image\")) {\n const buffer = Buffer.from(await imgRes.arrayBuffer());\n const ext = item.image.split(\".\").pop()?.split(\"?\")[0] || \"jpg\";\n const filename = `blog-${Date.now()}-${Math.random().toString(36).slice(2, 6)}.${ext}`;\n const { writeFile, mkdir } = await import(\"fs/promises\");\n const { join } = await import(\"path\");\n await mkdir(join(process.cwd(), \"public\", \"uploads\"), { recursive: true });\n await writeFile(join(process.cwd(), \"public\", \"uploads\", filename), buffer);\n coverImage = `/uploads/${filename}`;\n }\n } catch {}\n }\n\n // AI translates\n const prompt = `Ты редактор блога об AI. Напиши заметку на РУССКОМ языке (250-350 слов).\n\nЗаголовок оригинала: ${item.title}\nИсточник: ${item.link}\n\nВАЖНО:\n1. Начни с заголовка строго в формате: \"ЗАГОЛОВОК: русский перевод\"\n2. Заголовок должен быть ПОЛНОСТЬЮ на русском, без английских слов\n3. Затем сам текст заметки\n4. В конце добавь: \"📎 [Источник](${item.link})\"\n5. Все ссылки на GitHub делай в формате [название](url)`;\n\n const aiRes = await fetch(\"https://api.deepseek.com/v1/chat/completions\", {\n method: \"POST\", headers: { \"Content-Type\": \"application/json\", \"Authorization\": `Bearer ${key}` },\n body: JSON.stringify({ model: \"deepseek-chat\", messages: [{ role: \"user\", content: prompt }], max_tokens: 1000 }),\n });\n\n const aiData = await aiRes.json();\n const fullText = aiData.choices?.[0]?.message?.content;\n if (!fullText) continue;\n\n // Extract title\n let title = item.title;\n const tm = fullText.match(/ЗАГОЛОВОК:\\s*(.+)/);\n if (tm) title = tm[1].trim();\n\n // Remove title line from content\n const content = fullText.replace(/ЗАГОЛОВОК:\\s*.+(\\n|$)/, \"\").trim();\n const excerpt = content.replace(/[#*\\[\\]()]/g, \"\").slice(0, 200).replace(/\\n/g, \" \");\n\n // Generate clean transliterated slug\n const clean = title.replace(/[^a-zа-я0-9\\s-]/gi, \"\").trim();\n const slug = translit(clean).replace(/[^a-z0-9]+/g, \"-\").replace(/^-+|-+$/g, \"\").slice(0, 70);\n\n let cat = await db.blogCategory.findFirst({ where: { name: feed.category } });\n if (!cat) cat = await db.blogCategory.create({ data: { name: feed.category, slug: feed.category.toLowerCase().replace(/[^a-zа-я0-9]+/g, \"-\") } });\n\n await db.blogPost.create({\n data: { title, slug, content, excerpt, coverImage, status: \"published\", authorId: admin.id, categoryId: cat.id, tags: \"AI,новости\", aiGenerated: true, aiModel: \"deepseek-chat\", metaTitle: title + \" — Карта роста\", metaDesc: excerpt, publishedAt: new Date() },\n });\n\n results.push({ feed: feed.name, title: title.slice(0, 60), slug, status: \"published\" });\n }\n\n await db.blogFeed.update({ where: { id: feed.id }, data: { lastFetched: new Date() } });\n } catch (e: any) {\n results.push({ feed: feed.name, status: \"error\", reason: e.message.slice(0, 80) });\n }\n }\n\n return NextResponse.json({ results });\n}\n"}, "5aad9fde474155560362eb39471caf495294ee95": {"t": 1785754650.9834259, "n": 6391, "h": "9eb10980cd9f3f42", "c": "import { NextResponse } from \"next/server\";\nimport { getDb } from \"@/lib/db/index\";\n\nfunction translit(text: string): string {\n const map: any = { а:\"a\",б:\"b\",в:\"v\",г:\"g\",д:\"d\",е:\"e\",ё:\"yo\",ж:\"zh\",з:\"z\",и:\"i\",й:\"y\",к:\"k\",л:\"l\",м:\"m\",н:\"n\",о:\"o\",п:\"p\",р:\"r\",с:\"s\",т:\"t\",у:\"u\",ф:\"f\",х:\"h\",ц:\"ts\",ч:\"ch\",ш:\"sh\",щ:\"sch\",ъ:\"\",ы:\"y\",ь:\"\",э:\"e\",ю:\"yu\",я:\"ya\" };\n return text.toLowerCase().split(\"\").map((c: string) => map[c] || c).join(\"\");\n}\n\nexport async function POST() {\n const db = await getDb();\n const feeds = await db.blogFeed.findMany({ where: { isActive: true } });\n const admin = await db.user.findFirst({ where: { role: \"admin\" } });\n if (!admin) return NextResponse.json({ error: \"Админ не найден\" }, { status: 500 });\n\n const key = process.env.DEEPSEEK_API_KEY;\n if (!key) return NextResponse.json({ error: \"Нет DEEPSEEK_API_KEY\" }, { status: 500 });\n\n const results: any[] = [];\n\n for (const feed of feeds) {\n try {\n const res = await fetch(feed.url, {\n headers: feed.type === \"json\" ? { \"Accept\": \"application/json\" } : { \"User-Agent\": \"ProektMap/1.0\" },\n signal: AbortSignal.timeout(15000),\n });\n const raw = await res.text();\n\n let items: { title: string; link: string; description: string; image?: string }[] = [];\n if (feed.type === \"json\") {\n try {\n const json = JSON.parse(raw);\n if (json.hits) items = json.hits.map((h: any) => ({ title: h.title || h.story_title || \"\", link: h.url || `https://news.ycombinator.com/item?id=${h.objectID}`, description: h.story_text || h.comment_text || \"\" }));\n else if (json.data?.children) items = json.data.children.map((c: any) => { const d = c.data; let img = d.thumbnail?.startsWith(\"http\") ? d.thumbnail : d.preview?.images?.[0]?.source?.url?.replace(/&/g, \"&\"); return { title: d.title, link: `https://reddit.com${d.permalink}`, description: d.selftext || \"\", image: img }; });\n else if (json.items) items = json.items.slice(0, 5).map((i: any) => ({ title: i.full_name || i.name, link: i.html_url || i.url, description: i.description || \"\", image: i.owner?.avatar_url || null }));\n } catch {}\n } else if (feed.type === \"xml\") {\n const entries = raw.match(/<entry>[\\s\\S]*?<\\/entry>/g) || [];\n items = entries.slice(0, 3).map((e: any) => ({ title: (e.match(/<title>(.*?)<\\/title>/)?.[1] || \"\").replace(/\\s+/g, \" \").trim(), link: e.match(/<id>(.*?)<\\/id>/)?.[1] || \"\", description: (e.match(/<summary>(.*?)<\\/summary>/)?.[1] || \"\").replace(/\\s+/g, \" \").trim() }));\n }\n\n if (items.length === 0) { results.push({ feed: feed.name, status: \"empty\" }); continue; }\n\n for (const item of items.slice(0, 2)) {\n if (!item.title || item.title.length < 15) continue;\n const exists = await db.blogPost.findFirst({ where: { metaDesc: { contains: item.link.slice(0, 50) } } });\n if (exists) continue;\n\n // Download image\n let coverImage = `https://proektmap.ru/api/og?title=${encodeURIComponent(item.title)}&category=${encodeURIComponent(feed.category)}`;\n if (item.image) {\n try {\n const imgRes = await fetch(item.image, { signal: AbortSignal.timeout(5000) });\n if (imgRes.ok && imgRes.headers.get(\"content-type\")?.startsWith(\"image\")) {\n const buffer = Buffer.from(await imgRes.arrayBuffer());\n const ext = item.image.split(\".\").pop()?.split(\"?\")[0] || \"jpg\";\n const filename = `blog-${Date.now()}-${Math.random().toString(36).slice(2, 6)}.${ext}`;\n const { writeFile, mkdir } = await import(\"fs/promises\");\n const { join } = await import(\"path\");\n await mkdir(join(process.cwd(), \"public\", \"uploads\"), { recursive: true });\n await writeFile(join(process.cwd(), \"public\", \"uploads\", filename), buffer);\n coverImage = `/uploads/${filename}`;\n }\n } catch {}\n }\n\n // AI translates\n const prompt = `Ты редактор блога об AI. Напиши заметку на РУССКОМ языке (250-350 слов).\n\nЗаголовок оригинала: ${item.title}\nИсточник: ${item.link}\n\nВАЖНО:\n1. Начни с заголовка строго в формате: \"ЗАГОЛОВОК: русский перевод\"\n2. Заголовок должен быть ПОЛНОСТЬЮ на русском, без английских слов\n3. Затем сам текст заметки\n4. В конце добавь: \"📎 [Источник](${item.link})\"\n5. Все ссылки на GitHub делай в формате [название](url)`;\n\n const aiRes = await fetch(\"https://api.deepseek.com/v1/chat/completions\", {\n method: \"POST\", headers: { \"Content-Type\": \"application/json\", \"Authorization\": `Bearer ${key}` },\n body: JSON.stringify({ model: \"deepseek-chat\", messages: [{ role: \"user\", content: prompt }], max_tokens: 1000 }),\n });\n\n const aiData = await aiRes.json();\n const fullText = aiData.choices?.[0]?.message?.content;\n if (!fullText) continue;\n\n // Extract title\n let title = item.title;\n const tm = fullText.match(/ЗАГОЛОВОК:\\s*(.+)/);\n if (tm) title = tm[1].trim();\n\n // Remove title line from content\n const content = fullText.replace(/ЗАГОЛОВОК:\\s*.+(\\n|$)/, \"\").trim();\n const excerpt = content.replace(/[#*\\[\\]()]/g, \"\").slice(0, 200).replace(/\\n/g, \" \");\n\n // Generate clean transliterated slug\n const clean = title.replace(/[^a-zа-я0-9\\s-]/gi, \"\").trim();\n const slug = translit(clean).replace(/[^a-z0-9]+/g, \"-\").replace(/^-+|-+$/g, \"\").slice(0, 70);\n\n let cat = await db.blogCategory.findFirst({ where: { name: feed.category } });\n if (!cat) cat = await db.blogCategory.create({ data: { name: feed.category, slug: feed.category.toLowerCase().replace(/[^a-zа-я0-9]+/g, \"-\") } });\n\n await db.blogPost.create({\n data: { title, slug, content, excerpt, coverImage, status: \"published\", authorId: admin.id, categoryId: cat.id, tags: \"AI,новости\", aiGenerated: true, aiModel: \"deepseek-chat\", metaTitle: title + \" — Карта роста\", metaDesc: excerpt, publishedAt: new Date() },\n });\n\n results.push({ feed: feed.name, title: title.slice(0, 60), slug, status: \"published\" });\n }\n\n await db.blogFeed.update({ where: { id: feed.id }, data: { lastFetched: new Date() } });\n } catch (e: any) {\n results.push({ feed: feed.name, status: \"error\", reason: e.message.slice(0, 80) });\n }\n }\n\n return NextResponse.json({ results });\n}\n"}, "eaae1de86424e4f63e308998f4ccc3d4d6f4e508": {"t": 1785754652.3774946, "n": 6865, "h": "cca429143dfd3c7c", "c": "import { NextResponse } from \"next/server\";\nimport { getDb } from \"@/lib/db/index\";\n\nexport async function POST() {\n const db = await getDb();\n const feeds = await db.blogFeed.findMany({ where: { isActive: true } });\n const admin = await db.user.findFirst({ where: { role: \"admin\" } });\n if (!admin) return NextResponse.json({ error: \"Админ не найден\" }, { status: 500 });\n\n const key = process.env.DEEPSEEK_API_KEY;\n if (!key) return NextResponse.json({ error: \"Нет DEEPSEEK_API_KEY\" }, { status: 500 });\n\n const results: any[] = [];\n\n for (const feed of feeds) {\n try {\n const res = await fetch(feed.url, {\n headers: feed.type === \"json\" ? { \"Accept\": \"application/json\" } : { \"User-Agent\": \"ProektMap/1.0\" },\n signal: AbortSignal.timeout(15000),\n });\n const raw = await res.text();\n\n let items: { title: string; link: string; description: string; image?: string }[] = [];\n\n if (feed.type === \"json\") {\n try {\n const json = JSON.parse(raw);\n if (json.hits) {\n items = json.hits.map((h: any) => ({ title: h.title || h.story_title || \"\", link: h.url || `https://news.ycombinator.com/item?id=${h.objectID}`, description: h.story_text || h.comment_text || \"\" }));\n } else if (json.data?.children) {\n items = json.data.children.map((c: any) => {\n const d = c.data;\n let img: string | undefined;\n if (d.thumbnail && d.thumbnail.startsWith(\"http\")) img = d.thumbnail;\n else if (d.preview?.images?.[0]?.source?.url) img = d.preview.images[0].source.url.replace(/&/g, \"&\");\n return { title: d.title, link: `https://reddit.com${d.permalink}`, description: d.selftext || \"\", image: img };\n });\n } else if (json.items) {\n items = json.items.slice(0, 5).map((i: any) => ({\n title: i.full_name || i.name,\n link: i.html_url || i.url,\n description: i.description || \"\",\n image: i.owner?.avatar_url || null,\n }));\n }\n } catch {}\n } else if (feed.type === \"xml\") {\n const entries = raw.match(/<entry>[\\s\\S]*?<\\/entry>/g) || [];\n items = entries.slice(0, 3).map(e => {\n const title = (e.match(/<title>(.*?)<\\/title>/)?.[1] || \"\").replace(/\\s+/g, \" \").trim();\n const link = e.match(/<id>(.*?)<\\/id>/)?.[1] || \"\";\n const summary = (e.match(/<summary>(.*?)<\\/summary>/)?.[1] || \"\").replace(/\\s+/g, \" \").trim();\n return { title, link, description: summary };\n });\n }\n\n if (items.length === 0) { results.push({ feed: feed.name, status: \"empty\" }); continue; }\n\n for (const item of items.slice(0, 2)) {\n if (!item.title || item.title.length < 15) continue;\n\n const exists = await db.blogPost.findFirst({ where: { metaDesc: { contains: item.link.slice(0, 50) } } });\n if (exists) continue;\n\n // Download image if available\n let coverImage = `https://proektmap.ru/api/og?title=${encodeURIComponent(item.title)}&category=${encodeURIComponent(feed.category)}`;\n if (item.image) {\n try {\n const imgRes = await fetch(item.image, { signal: AbortSignal.timeout(5000) });\n if (imgRes.ok && imgRes.headers.get(\"content-type\")?.startsWith(\"image\")) {\n const buffer = Buffer.from(await imgRes.arrayBuffer());\n const ext = item.image.split(\".\").pop()?.split(\"?\")[0] || \"jpg\";\n const filename = `blog-${Date.now()}-${Math.random().toString(36).slice(2, 6)}.${ext}`;\n const { writeFile, mkdir } = await import(\"fs/promises\");\n const { join } = await import(\"path\");\n await mkdir(join(process.cwd(), \"public\", \"uploads\"), { recursive: true });\n await writeFile(join(process.cwd(), \"public\", \"uploads\", filename), buffer);\n coverImage = `/uploads/${filename}`;\n }\n } catch {}\n }\n\n // AI translates and writes in Russian\n const prompt = `Ты — редактор блога об AI на русском языке. Переведи И заголовок И содержание на русский. Сделай пересказ на 250-350 слов живым языком.\n\nЗаголовок оригинала: ${item.title}\nИсточник: ${item.link}\nОписание: ${(item.description || \"\").slice(0, 500)}\n\nВАЖНО:\n1. СНАЧАЛА переведи заголовок на русский. Формат: \"ЗАГОЛОВОК: <перевод>\"\n2. Затем напиши саму заметку.\n3. В конце добавь: \"📎 [Источник](${item.link})\" — это ДОЛЖНО быть в markdown-формате с квадратными и круглыми скобками, чтобы получилась кликабельная ссылка.\n4. Если в тексте упоминаешь репозиторий на GitHub — вставляй его как ссылку в формате [название](url).`;\n\n const aiRes = await fetch(\"https://api.deepseek.com/v1/chat/completions\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", \"Authorization\": `Bearer ${key}` },\n body: JSON.stringify({ model: \"deepseek-chat\", messages: [{ role: \"user\", content: prompt }], max_tokens: 1000 }),\n });\n\n const aiData = await aiRes.json();\n const fullResponse = aiData.choices?.[0]?.message?.content;\n if (!fullResponse) continue;\n\n // Extract translated title from AI response\n let title = item.title;\n const titleMatch = fullResponse.match(/ЗАГОЛОВОК:\\s*(.+)/);\n if (titleMatch) title = titleMatch[1].trim();\n\n // Remove the title line from content and use the rest as content\n let content = fullResponse.replace(/ЗАГОЛОВОК:\\s*.+(\\n|$)/, \"\").trim();\n\n const slug = title.replace(/[^a-zа-я0-9]+/g, \"-\").replace(/^-+|-+$/g, \"\").toLowerCase().slice(0, 70) + \"-\" + Date.now().toString(36).slice(-4);\n const excerpt = content.replace(/[#*\\[\\]()]/g, \"\").slice(0, 200).replace(/\\n/g, \" \");\n\n let cat = await db.blogCategory.findFirst({ where: { name: feed.category } });\n if (!cat) cat = await db.blogCategory.create({ data: { name: feed.category, slug: feed.category.toLowerCase().replace(/[^a-zа-я0-9]+/g, \"-\") } });\n\n // PUBLISH immediately, not draft\n await db.blogPost.create({\n data: {\n title, slug, content, excerpt, coverImage, status: \"published\", authorId: admin.id,\n categoryId: cat.id, tags: \"AI,новости\", aiGenerated: true, aiModel: \"deepseek-chat\",\n metaTitle: title + \" — Карта роста\", metaDesc: excerpt, publishedAt: new Date(),\n },\n });\n\n results.push({ feed: feed.name, title: title.slice(0, 60), image: coverImage ? \"✅\" : \"—\", status: \"published\" });\n }\n\n await db.blogFeed.update({ where: { id: feed.id }, data: { lastFetched: new Date() } });\n } catch (e: any) {\n results.push({ feed: feed.name, status: \"error\", reason: e.message.slice(0, 80) });\n }\n }\n\n return NextResponse.json({ results, total: results.length });\n}\n"}, "305060ee158f0fbc5b3b65fbd2c8b0718704bc19": {"t": 1785754653.8113222, "n": 6720, "h": "5905052869a47665", "c": "import { NextResponse } from \"next/server\";\nimport { getDb } from \"@/lib/db/index\";\n\nexport async function POST() {\n const db = await getDb();\n const feeds = await db.blogFeed.findMany({ where: { isActive: true } });\n const admin = await db.user.findFirst({ where: { role: \"admin\" } });\n if (!admin) return NextResponse.json({ error: \"Админ не найден\" }, { status: 500 });\n\n const key = process.env.DEEPSEEK_API_KEY;\n if (!key) return NextResponse.json({ error: \"Нет DEEPSEEK_API_KEY\" }, { status: 500 });\n\n const results: any[] = [];\n\n for (const feed of feeds) {\n try {\n const res = await fetch(feed.url, {\n headers: feed.type === \"json\" ? { \"Accept\": \"application/json\" } : { \"User-Agent\": \"ProektMap/1.0\" },\n signal: AbortSignal.timeout(15000),\n });\n const raw = await res.text();\n\n let items: { title: string; link: string; description: string; image?: string }[] = [];\n\n if (feed.type === \"json\") {\n try {\n const json = JSON.parse(raw);\n if (json.hits) {\n items = json.hits.map((h: any) => ({ title: h.title || h.story_title || \"\", link: h.url || `https://news.ycombinator.com/item?id=${h.objectID}`, description: h.story_text || h.comment_text || \"\" }));\n } else if (json.data?.children) {\n items = json.data.children.map((c: any) => {\n const d = c.data;\n let img: string | undefined;\n if (d.thumbnail && d.thumbnail.startsWith(\"http\")) img = d.thumbnail;\n else if (d.preview?.images?.[0]?.source?.url) img = d.preview.images[0].source.url.replace(/&/g, \"&\");\n return { title: d.title, link: `https://reddit.com${d.permalink}`, description: d.selftext || \"\", image: img };\n });\n } else if (json.items) {\n items = json.items.slice(0, 5).map((i: any) => ({\n title: i.full_name || i.name,\n link: i.html_url || i.url,\n description: i.description || \"\",\n image: i.owner?.avatar_url || null,\n }));\n }\n } catch {}\n } else if (feed.type === \"xml\") {\n const entries = raw.match(/<entry>[\\s\\S]*?<\\/entry>/g) || [];\n items = entries.slice(0, 3).map(e => {\n const title = (e.match(/<title>(.*?)<\\/title>/)?.[1] || \"\").replace(/\\s+/g, \" \").trim();\n const link = e.match(/<id>(.*?)<\\/id>/)?.[1] || \"\";\n const summary = (e.match(/<summary>(.*?)<\\/summary>/)?.[1] || \"\").replace(/\\s+/g, \" \").trim();\n return { title, link, description: summary };\n });\n }\n\n if (items.length === 0) { results.push({ feed: feed.name, status: \"empty\" }); continue; }\n\n for (const item of items.slice(0, 2)) {\n if (!item.title || item.title.length < 15) continue;\n\n const exists = await db.blogPost.findFirst({ where: { metaDesc: { contains: item.link.slice(0, 50) } } });\n if (exists) continue;\n\n // Download image if available\n let coverImage = \"\";\n if (item.image) {\n try {\n const imgRes = await fetch(item.image, { signal: AbortSignal.timeout(5000) });\n if (imgRes.ok && imgRes.headers.get(\"content-type\")?.startsWith(\"image\")) {\n const buffer = Buffer.from(await imgRes.arrayBuffer());\n const ext = item.image.split(\".\").pop()?.split(\"?\")[0] || \"jpg\";\n const filename = `blog-${Date.now()}-${Math.random().toString(36).slice(2, 6)}.${ext}`;\n const { writeFile, mkdir } = await import(\"fs/promises\");\n const { join } = await import(\"path\");\n await mkdir(join(process.cwd(), \"public\", \"uploads\"), { recursive: true });\n await writeFile(join(process.cwd(), \"public\", \"uploads\", filename), buffer);\n coverImage = `/uploads/${filename}`;\n }\n } catch {}\n }\n\n // AI translates and writes in Russian\n const prompt = `Ты — редактор блога об AI на русском языке. Переведи И заголовок И содержание на русский. Сделай пересказ на 250-350 слов живым языком.\n\nЗаголовок оригинала: ${item.title}\nИсточник: ${item.link}\nОписание: ${(item.description || \"\").slice(0, 500)}\n\nВАЖНО:\n1. СНАЧАЛА переведи заголовок на русский. Формат: \"ЗАГОЛОВОК: <перевод>\"\n2. Затем напиши саму заметку.\n3. В конце добавь: \"📎 [Источник](${item.link})\" — это ДОЛЖНО быть в markdown-формате с квадратными и круглыми скобками, чтобы получилась кликабельная ссылка.\n4. Если в тексте упоминаешь репозиторий на GitHub — вставляй его как ссылку в формате [название](url).`;\n\n const aiRes = await fetch(\"https://api.deepseek.com/v1/chat/completions\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", \"Authorization\": `Bearer ${key}` },\n body: JSON.stringify({ model: \"deepseek-chat\", messages: [{ role: \"user\", content: prompt }], max_tokens: 1000 }),\n });\n\n const aiData = await aiRes.json();\n const fullResponse = aiData.choices?.[0]?.message?.content;\n if (!fullResponse) continue;\n\n // Extract translated title from AI response\n let title = item.title;\n const titleMatch = fullResponse.match(/ЗАГОЛОВОК:\\s*(.+)/);\n if (titleMatch) title = titleMatch[1].trim();\n\n // Remove the title line from content and use the rest as content\n let content = fullResponse.replace(/ЗАГОЛОВОК:\\s*.+(\\n|$)/, \"\").trim();\n\n const slug = (title.replace(/[^a-zа-я0-9]+/g, \"-\") + \"-\" + Date.now().toString(36)).toLowerCase().slice(0, 80);\n const excerpt = content.replace(/[#*\\[\\]()]/g, \"\").slice(0, 200).replace(/\\n/g, \" \");\n\n let cat = await db.blogCategory.findFirst({ where: { name: feed.category } });\n if (!cat) cat = await db.blogCategory.create({ data: { name: feed.category, slug: feed.category.toLowerCase().replace(/[^a-zа-я0-9]+/g, \"-\") } });\n\n // PUBLISH immediately, not draft\n await db.blogPost.create({\n data: {\n title, slug, content, excerpt, coverImage, status: \"published\", authorId: admin.id,\n categoryId: cat.id, tags: \"AI,новости\", aiGenerated: true, aiModel: \"deepseek-chat\",\n metaTitle: title + \" — Карта роста\", metaDesc: excerpt, publishedAt: new Date(),\n },\n });\n\n results.push({ feed: feed.name, title: title.slice(0, 60), image: coverImage ? \"✅\" : \"—\", status: \"published\" });\n }\n\n await db.blogFeed.update({ where: { id: feed.id }, data: { lastFetched: new Date() } });\n } catch (e: any) {\n results.push({ feed: feed.name, status: \"error\", reason: e.message.slice(0, 80) });\n }\n }\n\n return NextResponse.json({ results, total: results.length });\n}\n"}, "88aba9728f2a86bc0706defbbbe4dd289fc07df5": {"t": 1785754656.281343, "n": 6378, "h": "d9768278ffb70f93", "c": "import { NextResponse } from \"next/server\";\nimport { getDb } from \"@/lib/db/index\";\n\nexport async function POST() {\n const db = await getDb();\n const feeds = await db.blogFeed.findMany({ where: { isActive: true } });\n const admin = await db.user.findFirst({ where: { role: \"admin\" } });\n if (!admin) return NextResponse.json({ error: \"Админ не найден\" }, { status: 500 });\n\n const key = process.env.DEEPSEEK_API_KEY;\n if (!key) return NextResponse.json({ error: \"Нет DEEPSEEK_API_KEY\" }, { status: 500 });\n\n const results: any[] = [];\n\n for (const feed of feeds) {\n try {\n const res = await fetch(feed.url, {\n headers: feed.type === \"json\" ? { \"Accept\": \"application/json\" } : { \"User-Agent\": \"ProektMap/1.0\" },\n signal: AbortSignal.timeout(15000),\n });\n const raw = await res.text();\n\n let items: { title: string; link: string; description: string; image?: string }[] = [];\n \n if (feed.type === \"json\") {\n try {\n const json = JSON.parse(raw);\n \n if (json.hits) {\n // Hacker News\n items = json.hits.map((h: any) => ({\n title: h.title || h.story_title || \"\",\n link: h.url || `https://news.ycombinator.com/item?id=${h.objectID}`,\n description: h.story_text || h.comment_text || \"\",\n image: null,\n }));\n } else if (json.data?.children) {\n // Reddit\n items = json.data.children.map((c: any) => {\n const d = c.data;\n let img: string | undefined;\n if (d.thumbnail && d.thumbnail.startsWith(\"http\")) img = d.thumbnail;\n else if (d.preview?.images?.[0]?.source?.url) img = d.preview.images[0].source.url.replace(/&/g, \"&\");\n else if (d.url && /\\.(jpg|jpeg|png|gif|webp)(\\?|$)/i.test(d.url)) img = d.url;\n return { title: d.title, link: `https://reddit.com${d.permalink}`, description: d.selftext || \"\", image: img };\n });\n } else if (json.items) {\n // GitHub\n items = json.items.slice(0, 5).map((i: any) => ({\n title: i.full_name || i.name,\n link: i.html_url || i.url,\n description: i.description || \"\",\n image: i.owner?.avatar_url || null,\n }));\n }\n } catch {}\n } else if (feed.type === \"xml\") {\n // arXiv\n const entries = raw.match(/<entry>[\\s\\S]*?<\\/entry>/g) || [];\n items = entries.slice(0, 3).map(e => {\n const title = (e.match(/<title>(.*?)<\\/title>/)?.[1] || \"\").replace(/\\s+/g, \" \").trim();\n const link = e.match(/<id>(.*?)<\\/id>/)?.[1] || \"\";\n const summary = (e.match(/<summary>(.*?)<\\/summary>/)?.[1] || \"\").replace(/\\s+/g, \" \").trim();\n return { title, link, description: summary };\n });\n }\n\n if (items.length === 0) { results.push({ feed: feed.name, status: \"empty\" }); continue; }\n\n for (const item of items.slice(0, 2)) {\n if (!item.title || item.title.length < 15) continue;\n\n // Check duplicate\n const exists = await db.blogPost.findFirst({ where: { metaDesc: { contains: item.link.slice(0, 50) } } });\n if (exists) continue;\n\n // Try to download image to our server\n let coverImage = \"\";\n if (item.image) {\n try {\n const imgRes = await fetch(item.image, { signal: AbortSignal.timeout(5000) });\n if (imgRes.ok && imgRes.headers.get(\"content-type\")?.startsWith(\"image\")) {\n const buffer = Buffer.from(await imgRes.arrayBuffer());\n const ext = item.image.split(\".\").pop()?.split(\"?\")[0] || \"jpg\";\n const filename = `blog-${Date.now()}-${Math.random().toString(36).slice(2,6)}.${ext}`;\n const { writeFile, mkdir } = await import(\"fs/promises\");\n const { join } = await import(\"path\");\n const uploadDir = join(process.cwd(), \"public\", \"uploads\");\n await mkdir(uploadDir, { recursive: true });\n await writeFile(join(uploadDir, filename), buffer);\n coverImage = `/uploads/${filename}`;\n }\n } catch {}\n }\n\n // AI generates Russian summary\n const prompt = `Ты — редактор блога об AI. Напиши краткую заметку на русском (250-350 слов) на основе этой новости:\n\nЗаголовок: ${item.title}\nИсточник: ${item.link}\nОписание: ${(item.description || \"\").slice(0, 500)}\n\nФормат: краткий пересказ сути новости + 1-2 предложения почему это важно для AI-инженеров + ссылка на источник.\n\nПиши живым языком, как для коллеги. Без шаблонных фраз.`;\n\n const aiRes = await fetch(\"https://api.deepseek.com/v1/chat/completions\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", \"Authorization\": `Bearer ${key}` },\n body: JSON.stringify({ model: \"deepseek-chat\", messages: [{ role: \"user\", content: prompt }], max_tokens: 800 }),\n });\n\n const aiData = await aiRes.json();\n const content = aiData.choices?.[0]?.message?.content;\n if (!content) continue;\n\n const title = item.title.slice(0, 120);\n const slug = (title.replace(/[^a-zа-я0-9]+/g, \"-\") + \"-\" + Date.now().toString(36)).toLowerCase().slice(0, 80);\n const excerpt = content.slice(0, 200).replace(/\\n/g, \" \");\n\n let cat = await db.blogCategory.findFirst({ where: { name: feed.category } });\n if (!cat) cat = await db.blogCategory.create({ data: { name: feed.category, slug: feed.category.toLowerCase().replace(/[^a-zа-я0-9]+/g, \"-\") } });\n\n await db.blogPost.create({\n data: {\n title, slug, content, excerpt, coverImage, status: \"draft\", authorId: admin.id,\n categoryId: cat.id, tags: `AI,${feed.category}`, aiGenerated: true, aiModel: \"deepseek-chat\",\n metaTitle: title + \" — Карта роста\", metaDesc: excerpt,\n },\n });\n\n results.push({ feed: feed.name, title: title.slice(0, 60), image: coverImage ? \"✅\" : \"—\", status: \"draft\" });\n }\n\n await db.blogFeed.update({ where: { id: feed.id }, data: { lastFetched: new Date() } });\n } catch (e: any) {\n results.push({ feed: feed.name, status: \"error\", reason: e.message.slice(0, 80) });\n }\n }\n\n return NextResponse.json({ results, total: results.length });\n}\n"}, "c411d3d4cdfc0b2921a8b2cfda2d97dca03a47ca": {"t": 1785754657.6593282, "n": 6366, "h": "8b9db036b9a9874e", "c": "import { NextResponse } from \"next/server\";\nimport { getDb } from \"@/lib/db/index\";\n\nexport async function POST() {\n const db = await getDb();\n const feeds = await db.blogFeed.findMany({ where: { isActive: true } });\n const admin = await db.user.findFirst({ where: { role: \"admin\" } });\n if (!admin) return NextResponse.json({ error: \"Админ не найден\" }, { status: 500 });\n\n const key = process.env.DEEPSEEK_API_KEY;\n if (!key) return NextResponse.json({ error: \"Нет DEEPSEEK_API_KEY\" }, { status: 500 });\n\n const results: any[] = [];\n\n for (const feed of feeds) {\n try {\n const res = await fetch(feed.url, {\n headers: feed.type === \"json\" ? { \"Accept\": \"application/json\" } : { \"User-Agent\": \"ProektMap/1.0\" },\n signal: AbortSignal.timeout(15000),\n });\n const raw = await res.text();\n\n let items: { title: string; link: string; description: string; image?: string }[] = [];\n \n if (feed.type === \"json\") {\n try {\n const json = JSON.parse(raw);\n \n if (json.hits) {\n // Hacker News\n items = json.hits.map((h: any) => ({\n title: h.title || h.story_title || \"\",\n link: h.url || `https://news.ycombinator.com/item?id=${h.objectID}`,\n description: h.story_text || h.comment_text || \"\",\n image: null,\n }));\n } else if (json.data?.children) {\n // Reddit\n items = json.data.children.map((c: any) => {\n const d = c.data;\n let img: string | undefined;\n if (d.thumbnail && d.thumbnail.startsWith(\"http\")) img = d.thumbnail;\n else if (d.preview?.images?.[0]?.source?.url) img = d.preview.images[0].source.url.replace(/&/g, \"&\");\n else if (d.url && /\\.(jpg|jpeg|png|gif|webp)(\\?|$)/i.test(d.url)) img = d.url;\n return { title: d.title, link: `https://reddit.com${d.permalink}`, description: d.selftext || \"\", image: img };\n });\n } else if (json.items) {\n // GitHub\n items = json.items.slice(0, 5).map((i: any) => ({\n title: i.full_name || i.name,\n link: i.html_url || i.url,\n description: i.description || \"\",\n image: i.owner?.avatar_url || null,\n }));\n }\n } catch {}\n } else if (feed.type === \"xml\") {\n // arXiv\n const entries = raw.match(/<entry>[\\s\\S]*?<\\/entry>/g) || [];\n items = entries.slice(0, 3).map(e => {\n const title = (e.match(/<title>(.*?)<\\/title>/)?.[1] || \"\").replace(/\\s+/g, \" \").trim();\n const link = e.match(/<id>(.*?)<\\/id>/)?.[1] || \"\";\n const summary = (e.match(/<summary>(.*?)<\\/summary>/)?.[1] || \"\").replace(/\\s+/g, \" \").trim();\n return { title, link, description: summary };\n });\n }\n\n if (items.length === 0) { results.push({ feed: feed.name, status: \"empty\" }); continue; }\n\n for (const item of items.slice(0, 2)) {\n if (!item.title || item.title.length < 15) continue;\n\n // Check duplicate\n const exists = await db.blogPost.findFirst({ where: { metaDesc: { contains: item.link.slice(0, 50) } } });\n if (exists) continue;\n\n // Try to download image to our server\n let coverImage = \"\";\n if (item.image) {\n try {\n const imgRes = await fetch(item.image, { signal: AbortSignal.timeout(5000) });\n if (imgRes.ok && imgRes.headers.get(\"content-type\")?.startsWith(\"image\")) {\n const buffer = Buffer.from(await imgRes.arrayBuffer());\n const ext = item.image.split(\".\").pop()?.split(\"?\")[0] || \"jpg\";\n const filename = `blog-${Date.now()}-${Math.random().toString(36).slice(2,6)}.${ext}`;\n const { writeFile, mkdir } = await import(\"fs/promises\");\n const { join } = await import(\"path\");\n const uploadDir = join(process.cwd(), \"public\", \"uploads\");\n await mkdir(uploadDir, { recursive: true });\n await writeFile(join(uploadDir, filename), buffer);\n coverImage = `/uploads/${filename}`;\n }\n } catch {}\n }\n\n // AI generates Russian summary\n const prompt = `Ты — редактор блога об AI. Напиши краткую заметку на русском (250-350 слов) на основе этой новости:\n\nЗаголовок: ${item.title}\nИсточник: ${item.link}\nОписание: ${(item.description || \"\").slice(0, 500)}\n\nФормат: краткий пересказ сути новости + 1-2 предложения почему это важно для AI-инженеров + ссылка на источник.\n\nПиши живым языком, как для коллеги. Без шаблонных фраз.`;\n\n const aiRes = await fetch(\"https://api.deepseek.com/v1/chat/completions\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", \"Authorization\": `Bearer ${key}` },\n body: JSON.stringify({ model: \"deepseek-chat\", messages: [{ role: \"user\", content: prompt }], max_tokens: 800 }),\n });\n\n const aiData = await aiRes.json();\n const content = aiData.choices?.[0]?.message?.content;\n if (!content) continue;\n\n const title = item.title.slice(0, 120);\n const slug = title.toLowerCase().replace(/[^a-zа-я0-9]+/g, \"-\").replace(/^-|-$/g, \"\").slice(0, 80);\n const excerpt = content.slice(0, 200).replace(/\\n/g, \" \");\n\n let cat = await db.blogCategory.findFirst({ where: { name: feed.category } });\n if (!cat) cat = await db.blogCategory.create({ data: { name: feed.category, slug: feed.category.toLowerCase().replace(/[^a-zа-я0-9]+/g, \"-\") } });\n\n await db.blogPost.create({\n data: {\n title, slug, content, excerpt, coverImage, status: \"draft\", authorId: admin.id,\n categoryId: cat.id, tags: `AI,${feed.category}`, aiGenerated: true, aiModel: \"deepseek-chat\",\n metaTitle: title + \" — Карта роста\", metaDesc: excerpt,\n },\n });\n\n results.push({ feed: feed.name, title: title.slice(0, 60), image: coverImage ? \"✅\" : \"—\", status: \"draft\" });\n }\n\n await db.blogFeed.update({ where: { id: feed.id }, data: { lastFetched: new Date() } });\n } catch (e: any) {\n results.push({ feed: feed.name, status: \"error\", reason: e.message.slice(0, 80) });\n }\n }\n\n return NextResponse.json({ results, total: results.length });\n}\n"}, "2d29ca17e9fec87aec0801fad2e16fa1020608d4": {"t": 1785754659.0390773, "n": 4137, "h": "2ed8d198a414c369", "c": "import { NextResponse } from \"next/server\";\nimport { getDb } from \"@/lib/db/index\";\n\nexport async function POST() {\n const db = await getDb();\n const feeds = await db.blogFeed.findMany({ where: { isActive: true } });\n const admin = await db.user.findFirst({ where: { role: \"admin\" } });\n if (!admin) return NextResponse.json({ error: \"Админ не найден\" }, { status: 500 });\n\n const key = process.env.DEEPSEEK_API_KEY;\n if (!key) return NextResponse.json({ error: \"Нет DEEPSEEK_API_KEY\" }, { status: 500 });\n\n const results: any[] = [];\n\n for (const feed of feeds) {\n try {\n // Fetch feed\n const res = await fetch(feed.url, {\n headers: feed.type === \"json\" ? { \"Accept\": \"application/json\" } : {},\n signal: AbortSignal.timeout(15000),\n });\n const raw = await res.text();\n\n // Extract items based on type\n let items: { title: string; link: string; description: string }[] = [];\n if (feed.type === \"json\") {\n try {\n const json = JSON.parse(raw);\n if (json.hits) items = json.hits.map((h: any) => ({ title: h.title || h.story_title || \"\", link: h.url || `https://news.ycombinator.com/item?id=${h.objectID}`, description: h.story_text || h.comment_text || \"\" }));\n else if (json.data?.children) items = json.data.children.map((c: any) => ({ title: c.data.title, link: `https://reddit.com${c.data.permalink}`, description: c.data.selftext }));\n else if (json.items) items = json.items.slice(0, 5).map((i: any) => ({ title: i.name || i.full_name, link: i.html_url || i.url, description: i.description || \"\" }));\n } catch {}\n }\n\n if (items.length === 0) { results.push({ feed: feed.name, status: \"empty\" }); continue; }\n\n // Pick top 2 items\n for (const item of items.slice(0, 2)) {\n if (!item.title || item.title.length < 20) continue;\n\n // Check if already exists\n const exists = await db.blogPost.findFirst({ where: { metaDesc: { contains: item.link.slice(0, 50) } } });\n if (exists) continue;\n\n // AI generates Russian summary\n const prompt = `Ты — редактор блога об AI. Напиши краткую заметку на русском (200-300 слов) на основе этой новости:\n\nЗаголовок: ${item.title}\nИсточник: ${item.link}\nОписание: ${(item.description || \"\").slice(0, 500)}\n\nФормат: краткий пересказ сути новости + 1-2 предложения почему это важно для AI-инженеров + ссылка на источник.\n\nПиши живым языком, как для коллеги. Не используй шаблонные фразы.`;\n\n const aiRes = await fetch(\"https://api.deepseek.com/v1/chat/completions\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", \"Authorization\": `Bearer ${key}` },\n body: JSON.stringify({ model: \"deepseek-chat\", messages: [{ role: \"user\", content: prompt }], max_tokens: 800 }),\n });\n\n const aiData = await aiRes.json();\n const content = aiData.choices?.[0]?.message?.content;\n if (!content) continue;\n\n const title = item.title.slice(0, 120);\n const slug = title.toLowerCase().replace(/[^a-zа-я0-9]+/g, \"-\").replace(/^-|-$/g, \"\").slice(0, 80);\n const excerpt = content.slice(0, 200);\n\n // Find category\n let cat = await db.blogCategory.findFirst({ where: { name: feed.category } });\n if (!cat) cat = await db.blogCategory.create({ data: { name: feed.category, slug: feed.category.toLowerCase().replace(/[^a-zа-я0-9]+/g, \"-\") } });\n\n await db.blogPost.create({\n data: {\n title, slug, content, excerpt, status: \"draft\", authorId: admin.id,\n categoryId: cat.id, tags: `AI,${feed.category}`, aiGenerated: true, aiModel: \"deepseek-chat\",\n metaTitle: title + \" — Карта роста\", metaDesc: excerpt,\n },\n });\n\n results.push({ feed: feed.name, title: title.slice(0, 60), status: \"draft\" });\n }\n\n await db.blogFeed.update({ where: { id: feed.id }, data: { lastFetched: new Date() } });\n } catch (e: any) {\n results.push({ feed: feed.name, status: \"error\", reason: e.message.slice(0, 80) });\n }\n }\n\n return NextResponse.json({ results });\n}\n"}, "f3206e171e953d7a5c92e36a442815c2790f74fe": {"t": 1785754661.3881485, "n": 2539, "h": "25b23e7c5adcd50c", "c": "import { NextResponse } from \"next/server\";\nimport { getDb } from \"@/lib/db/index\";\n\nexport async function POST() {\n const db = await getDb();\n const jobs = await db.autoPublish.findMany({ where: { isActive: true } });\n \n if (jobs.length === 0) {\n return NextResponse.json({ message: \"Нет активных заданий\" });\n }\n\n const results: any[] = [];\n const admin = await db.user.findFirst({ where: { role: \"admin\" } });\n if (!admin) return NextResponse.json({ error: \"Админ не найден\" }, { status: 500 });\n\n for (const job of jobs) {\n try {\n const prompt = `Напиши статью для блога на русском языке. Тема: ${job.topic}. 800-1200 слов, 3-5 H2, практическая польза. Markdown.`;\n\n const key = process.env.DEEPSEEK_API_KEY;\n if (!key) { results.push({ topic: job.topic, status: \"skipped\", reason: \"Нет DEEPSEEK_API_KEY\" }); continue; }\n\n const res = await fetch(\"https://api.deepseek.com/v1/chat/completions\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", \"Authorization\": `Bearer ${key}` },\n body: JSON.stringify({ model: \"deepseek-chat\", messages: [{ role: \"user\", content: prompt }], max_tokens: 3000 }),\n });\n\n const data = await res.json();\n const content = data.choices?.[0]?.message?.content;\n if (!content) { results.push({ topic: job.topic, status: \"error\" }); continue; }\n\n const titleMatch = content.match(/^# (.+)/m);\n const title = titleMatch ? titleMatch[1] : job.topic;\n const slug = title.toLowerCase().replace(/[^a-zа-я0-9]+/g, \"-\").replace(/^-|-$/g, \"\");\n const excerpt = content.split(\"\\n\").find((l: string) => l.length > 50 && !l.startsWith(\"#\"))?.slice(0, 200) || \"\";\n\n let cat = await db.blogCategory.findFirst({ where: { name: job.category } });\n if (!cat) cat = await db.blogCategory.create({ data: { name: job.category, slug: job.category.toLowerCase().replace(/[^a-zа-я0-9]+/g, \"-\") } });\n\n await db.blogPost.create({\n data: { title, slug, content, excerpt, status: \"draft\", authorId: admin.id,\n categoryId: cat.id, tags: job.category, aiGenerated: true, aiModel: \"deepseek-chat\",\n metaTitle: title + \" — Карта роста\", metaDesc: excerpt },\n });\n\n await db.autoPublish.update({ where: { id: job.id }, data: { lastRunAt: new Date() } });\n results.push({ topic: job.topic, status: \"draft\", title });\n } catch (e: any) {\n results.push({ topic: job.topic, status: \"error\", reason: e.message });\n }\n }\n\n return NextResponse.json({ results });\n}\n"}}, "src/app/api/architect/route.ts": {"3e832a87dce00ff53a610489a1b0edf5226ba106": {"t": 1785745605.710367, "n": 5106, "h": "184866b661b12057", "c": "import { NextRequest, NextResponse } from \"next/server\";\nimport { getDb } from \"@/lib/db/index\";\n\nconst SYSTEM_PROMPT = `Ты — Senior Product Manager и Staff Engineer. Создай детальный архитектурный план для бизнес-идеи. План должен быть настолько конкретным, чтобы AI-агент (Cursor/Claude) смог по нему писать код без галлюцинаций.\n\nЖЁСТКИЕ ПРАВИЛА:\n1. Сущности БД — с ПОЛНЫМИ полями: имя, тип, описание. Пример: CrawlTarget (id UUID PK, url string, depth number, status enum, createdAt timestamp).\n2. Стек технологий — каждая технология с ОБОСНОВАНИЕМ. Пример: \"BullMQ + Redis — фоновый парсинг тысяч URL без блокировки запросов\".\n3. План — по НЕДЕЛЯМ с конкретными файлами.\n4. Ошибки — специфичные для ниши.\n5. Атомарные промпты — 3 конкретных промпта для Cursor/Claude Code, каждый для одного файла/функции.\n6. Итоговый промпт — один большой промпт для старта проекта.\n\nВерни ТОЛЬКО JSON:\n\nТАКЖЕ ПРОВЕДИ АНАЛИЗ РЫНКА И ДОБАВЬ ПОЛЯ:\n \"marketAnalysis\": {\n \"marketSize\": \"Оценка рынка (1-2 предложения)\",\n \"targetAudience\": \"Целевая аудитория\",\n \"competitors\": [\"Конкурент 1: описание\", \"Конкурент 2: описание\", \"Конкурент 3: описание\"],\n \"differentiation\": \"Чем можно выделиться (2-3 предложения)\",\n \"risks\": [\"Риск 1\", \"Риск 2\", \"Риск 3\"],\n \"opportunities\": [\"Возможность 1\", \"Возможность 2\"],\n \"monetizationStrategy\": \"Стратегия монетизации (1-2 предложения)\",\n \"goToMarket\": \"Как выйти на рынок (1-2 предложения)\"\n },\n{\n \"productType\": \"Тип\",\n \"expertRecommendation\": \"Рекомендация\",\n \"recommendedStack\": \"Стек одной строкой\",\n \"options\": [{\n \"name\": \"Название\",\n \"description\": \"Описание\",\n \"complexity\": 5,\n \"mvpDays\": \"7-10 дней\",\n \"pros\": [\"+\"],\n \"cons\": [\"-\"],\n \"monetization\": \"Модель\",\n \"costDev\": \"30-40ч\",\n \"costAi\": \"15/мес\",\n \"costServer\": \"5/мес\",\n \"entities\": [\"Entity (id UUID PK, field1 type, field2 type) - описание\"],\n \"stack\": [{\"tech\": \"Технология\", \"reason\": \"Обоснование\"}],\n \"plan\": [\"Неделя 1: ...\", \"Неделя 2: ...\"],\n \"mcpServers\": [\"slug\"],\n \"patternSlugs\": [\"slug\"],\n \"promptTypes\": [\"тип\"],\n \"mistakes\": [\"Ошибка 1\"],\n \"atomicPrompts\": [\"Промпт 1: В файле lib/X.ts создай функцию...\", \"Промпт 2: ...\", \"Промпт 3: ...\"],\n \"masterPrompt\": \"Ты — senior разработчик. Реализуй проект... (полный промпт)\",\n \"summary\": \"Резюме\",\n \"toolRecommendation\": \"Инструмент\",\n \"aiModelRecommendation\": \"Модель\"\n }]\n}\n3 варианта. Минимум 3 недели в плане. 3 атомарных промпта.`\n\nexport async function POST(req: NextRequest) {\n const { idea, isPro } = await req.json();\n // Free users get limited analysis — only 3 option names/pros/cons, no details\n const isLimited = !isPro;\n if (!idea || idea.length < 10) return NextResponse.json({ error: \"Too short\" }, { status: 400 });\n\n const db = await getDb();\n const settings = await db.siteSettings.findUnique({ where: { id: \"main\" } });\n const key = settings?.deepseekApiKey || process.env.DEEPSEEK_API_KEY || \"\";\n if (!key) return NextResponse.json({ error: \"No key\" }, { status: 500 });\n\n try {\n const res = await fetch(\"https://api.deepseek.com/v1/chat/completions\", { signal: AbortSignal.timeout(35000),\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: \"Bearer \" + key },\n body: JSON.stringify({ model: \"deepseek-chat\", messages: [{ role: \"system\", content: SYSTEM_PROMPT }, { role: \"user\", content: idea }], max_tokens: 8000, temperature: 0.4 }),\n });\n const data = await res.json();\n const text = data.choices?.[0]?.message?.content || \"\";\n // Clean JSON\n let cleaned = text.replace(/```json\\s*/gi, \"\").replace(/\\s*```$/gi, \"\");\n cleaned = cleaned.replace(/\\\\(?![\"\\\\/bfnrtu])/g, \"\\\\\");\n const jsonMatch = cleaned.match(/\\{[\\s\\S]*\\}/);\n let result;\n if (jsonMatch) {\n try { result = JSON.parse(jsonMatch[0]); } catch(e) {\n const c2 = jsonMatch[0].replace(/[\u0000-\u001f]/g, \" \");\n try { result = JSON.parse(c2); } catch(e2) { result = null; }\n }\n }\n if (!result) result = { error: \"JSON parse failed\", raw: text.substring(0, 500) };\n\n if (result.options) {\n for (const opt of result.options) {\n if (opt.patternSlugs?.length) opt.patterns = await db.buildPattern.findMany({ where: { slug: { in: opt.patternSlugs }, isPublished: true }, select: { title: true, slug: true, difficulty: true } });\n else opt.patterns = [];\n if (opt.mcpServers?.length) opt.mcp = await db.mCPServer.findMany({ where: { slug: { in: opt.mcpServers } }, select: { name: true, slug: true, category: true } });\n else opt.mcp = [];\n if (opt.promptTypes?.length) opt.prompts = await db.promptBlueprint.findMany({ where: { OR: opt.promptTypes.map((t: string) => ({ category: { contains: t, mode: \"insensitive\" as const } })) }, select: { title: true, slug: true, category: true }, take: 5 });\n else opt.prompts = [];\n }\n }\n\n return NextResponse.json(result);\n } catch (err: any) {\n return NextResponse.json({ error: \"Ошибка генерации: \" + (err.message || \"неизвестная\"), raw: \"\" }, { status: 200 });\n }\n}\n"}, "e69321b1765365a5a3b382c556d8b56d15c3cd7a": {"t": 1785754643.763084, "n": 4673, "h": "2c9dd89c39517bd9", "c": "import { NextRequest, NextResponse } from \"next/server\";\nimport { getDb } from \"@/lib/db/index\";\n\nconst SYSTEM_PROMPT = `Ты — Senior Product Manager и Staff Engineer. Создай детальный архитектурный план для бизнес-идеи. План должен быть настолько конкретным, чтобы AI-агент (Cursor/Claude) смог по нему писать код без галлюцинаций.\n\nЖЁСТКИЕ ПРАВИЛА:\n1. Сущности БД — с ПОЛНЫМИ полями: имя, тип, описание. Пример: CrawlTarget (id UUID PK, url string, depth number, status enum, createdAt timestamp).\n2. Стек технологий — каждая технология с ОБОСНОВАНИЕМ. Пример: \"BullMQ + Redis — фоновый парсинг тысяч URL без блокировки запросов\".\n3. План — по НЕДЕЛЯМ с конкретными файлами.\n4. Ошибки — специфичные для ниши.\n5. Атомарные промпты — 3 конкретных промпта для Cursor/Claude Code, каждый для одного файла/функции.\n6. Итоговый промпт — один большой промпт для старта проекта.\n\nВерни ТОЛЬКО JSON:\n\nТАКЖЕ ПРОВЕДИ АНАЛИЗ РЫНКА И ДОБАВЬ ПОЛЯ:\n \"marketAnalysis\": {\n \"marketSize\": \"Оценка рынка (1-2 предложения)\",\n \"targetAudience\": \"Целевая аудитория\",\n \"competitors\": [\"Конкурент 1: описание\", \"Конкурент 2: описание\", \"Конкурент 3: описание\"],\n \"differentiation\": \"Чем можно выделиться (2-3 предложения)\",\n \"risks\": [\"Риск 1\", \"Риск 2\", \"Риск 3\"],\n \"opportunities\": [\"Возможность 1\", \"Возможность 2\"],\n \"monetizationStrategy\": \"Стратегия монетизации (1-2 предложения)\",\n \"goToMarket\": \"Как выйти на рынок (1-2 предложения)\"\n },\n{\n \"productType\": \"Тип\",\n \"expertRecommendation\": \"Рекомендация\",\n \"recommendedStack\": \"Стек одной строкой\",\n \"options\": [{\n \"name\": \"Название\",\n \"description\": \"Описание\",\n \"complexity\": 5,\n \"mvpDays\": \"7-10 дней\",\n \"pros\": [\"+\"],\n \"cons\": [\"-\"],\n \"monetization\": \"Модель\",\n \"costDev\": \"30-40ч\",\n \"costAi\": \"15/мес\",\n \"costServer\": \"5/мес\",\n \"entities\": [\"Entity (id UUID PK, field1 type, field2 type) - описание\"],\n \"stack\": [{\"tech\": \"Технология\", \"reason\": \"Обоснование\"}],\n \"plan\": [\"Неделя 1: ...\", \"Неделя 2: ...\"],\n \"mcpServers\": [\"slug\"],\n \"patternSlugs\": [\"slug\"],\n \"promptTypes\": [\"тип\"],\n \"mistakes\": [\"Ошибка 1\"],\n \"atomicPrompts\": [\"Промпт 1: В файле lib/X.ts создай функцию...\", \"Промпт 2: ...\", \"Промпт 3: ...\"],\n \"masterPrompt\": \"Ты — senior разработчик. Реализуй проект... (полный промпт)\",\n \"summary\": \"Резюме\",\n \"toolRecommendation\": \"Инструмент\",\n \"aiModelRecommendation\": \"Модель\"\n }]\n}\n3 варианта. Минимум 3 недели в плане. 3 атомарных промпта.`\n\nexport async function POST(req: NextRequest) {\n const { idea, isPro } = await req.json();\n // Free users get limited analysis — only 3 option names/pros/cons, no details\n const isLimited = !isPro;\n if (!idea || idea.length < 10) return NextResponse.json({ error: \"Too short\" }, { status: 400 });\n\n const db = await getDb();\n const settings = await db.siteSettings.findUnique({ where: { id: \"main\" } });\n const key = settings?.deepseekApiKey || process.env.DEEPSEEK_API_KEY || \"\";\n if (!key) return NextResponse.json({ error: \"No key\" }, { status: 500 });\n\n try {\n const res = await fetch(\"https://api.deepseek.com/v1/chat/completions\", { signal: AbortSignal.timeout(35000),\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: \"Bearer \" + key },\n body: JSON.stringify({ model: \"deepseek-v4-flash\", messages: [{ role: \"system\", content: SYSTEM_PROMPT }, { role: \"user\", content: idea }], max_tokens: 8000, temperature: 0.4 }),\n });\n const data = await res.json();\n const text = data.choices?.[0]?.message?.content || \"\";\n const jsonMatch = text.match(/\\{[\\s\\S]*\\}/);\n const result = jsonMatch ? JSON.parse(jsonMatch[0]) : { error: \"Parse failed\", raw: text };\n\n if (result.options) {\n for (const opt of result.options) {\n if (opt.patternSlugs?.length) opt.patterns = await db.buildPattern.findMany({ where: { slug: { in: opt.patternSlugs }, isPublished: true }, select: { title: true, slug: true, difficulty: true } });\n else opt.patterns = [];\n if (opt.mcpServers?.length) opt.mcp = await db.mCPServer.findMany({ where: { slug: { in: opt.mcpServers } }, select: { name: true, slug: true, category: true } });\n else opt.mcp = [];\n if (opt.promptTypes?.length) opt.prompts = await db.promptBlueprint.findMany({ where: { OR: opt.promptTypes.map((t: string) => ({ category: { contains: t, mode: \"insensitive\" as const } })) }, select: { title: true, slug: true, category: true }, take: 5 });\n else opt.prompts = [];\n }\n }\n\n return NextResponse.json(result);\n } catch (err: any) {\n return NextResponse.json({ error: err.message }, { status: 500 });\n }\n}\n"}, "fe85813f6cd5875d776fb37b95ab60da08844b35": {"t": 1785754646.0010245, "n": 4637, "h": "74f38f0f6602bbed", "c": "import { NextRequest, NextResponse } from \"next/server\";\nimport { getDb } from \"@/lib/db/index\";\n\nconst SYSTEM_PROMPT = `Ты — Senior Product Manager и Staff Engineer. Создай детальный архитектурный план для бизнес-идеи. План должен быть настолько конкретным, чтобы AI-агент (Cursor/Claude) смог по нему писать код без галлюцинаций.\n\nЖЁСТКИЕ ПРАВИЛА:\n1. Сущности БД — с ПОЛНЫМИ полями: имя, тип, описание. Пример: CrawlTarget (id UUID PK, url string, depth number, status enum, createdAt timestamp).\n2. Стек технологий — каждая технология с ОБОСНОВАНИЕМ. Пример: \"BullMQ + Redis — фоновый парсинг тысяч URL без блокировки запросов\".\n3. План — по НЕДЕЛЯМ с конкретными файлами.\n4. Ошибки — специфичные для ниши.\n5. Атомарные промпты — 3 конкретных промпта для Cursor/Claude Code, каждый для одного файла/функции.\n6. Итоговый промпт — один большой промпт для старта проекта.\n\nВерни ТОЛЬКО JSON:\n\nТАКЖЕ ПРОВЕДИ АНАЛИЗ РЫНКА И ДОБАВЬ ПОЛЯ:\n \"marketAnalysis\": {\n \"marketSize\": \"Оценка рынка (1-2 предложения)\",\n \"targetAudience\": \"Целевая аудитория\",\n \"competitors\": [\"Конкурент 1: описание\", \"Конкурент 2: описание\", \"Конкурент 3: описание\"],\n \"differentiation\": \"Чем можно выделиться (2-3 предложения)\",\n \"risks\": [\"Риск 1\", \"Риск 2\", \"Риск 3\"],\n \"opportunities\": [\"Возможность 1\", \"Возможность 2\"],\n \"monetizationStrategy\": \"Стратегия монетизации (1-2 предложения)\",\n \"goToMarket\": \"Как выйти на рынок (1-2 предложения)\"\n },\n{\n \"productType\": \"Тип\",\n \"expertRecommendation\": \"Рекомендация\",\n \"recommendedStack\": \"Стек одной строкой\",\n \"options\": [{\n \"name\": \"Название\",\n \"description\": \"Описание\",\n \"complexity\": 5,\n \"mvpDays\": \"7-10 дней\",\n \"pros\": [\"+\"],\n \"cons\": [\"-\"],\n \"monetization\": \"Модель\",\n \"costDev\": \"30-40ч\",\n \"costAi\": \"15/мес\",\n \"costServer\": \"5/мес\",\n \"entities\": [\"Entity (id UUID PK, field1 type, field2 type) - описание\"],\n \"stack\": [{\"tech\": \"Технология\", \"reason\": \"Обоснование\"}],\n \"plan\": [\"Неделя 1: ...\", \"Неделя 2: ...\"],\n \"mcpServers\": [\"slug\"],\n \"patternSlugs\": [\"slug\"],\n \"promptTypes\": [\"тип\"],\n \"mistakes\": [\"Ошибка 1\"],\n \"atomicPrompts\": [\"Промпт 1: В файле lib/X.ts создай функцию...\", \"Промпт 2: ...\", \"Промпт 3: ...\"],\n \"masterPrompt\": \"Ты — senior разработчик. Реализуй проект... (полный промпт)\",\n \"summary\": \"Резюме\",\n \"toolRecommendation\": \"Инструмент\",\n \"aiModelRecommendation\": \"Модель\"\n }]\n}\n3 варианта. Минимум 3 недели в плане. 3 атомарных промпта.`\n\nexport async function POST(req: NextRequest) {\n const { idea, isPro } = await req.json();\n // Free users get limited analysis — only 3 option names/pros/cons, no details\n const isLimited = !isPro;\n if (!idea || idea.length < 10) return NextResponse.json({ error: \"Too short\" }, { status: 400 });\n\n const db = await getDb();\n const settings = await db.siteSettings.findUnique({ where: { id: \"main\" } });\n const key = settings?.deepseekApiKey || process.env.DEEPSEEK_API_KEY || \"\";\n if (!key) return NextResponse.json({ error: \"No key\" }, { status: 500 });\n\n try {\n const res = await fetch(\"https://api.deepseek.com/v1/chat/completions\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: \"Bearer \" + key },\n body: JSON.stringify({ model: \"deepseek-v4-flash\", messages: [{ role: \"system\", content: SYSTEM_PROMPT }, { role: \"user\", content: idea }], max_tokens: 8000, temperature: 0.4 }),\n });\n const data = await res.json();\n const text = data.choices?.[0]?.message?.content || \"\";\n const jsonMatch = text.match(/\\{[\\s\\S]*\\}/);\n const result = jsonMatch ? JSON.parse(jsonMatch[0]) : { error: \"Parse failed\", raw: text };\n\n if (result.options) {\n for (const opt of result.options) {\n if (opt.patternSlugs?.length) opt.patterns = await db.buildPattern.findMany({ where: { slug: { in: opt.patternSlugs }, isPublished: true }, select: { title: true, slug: true, difficulty: true } });\n else opt.patterns = [];\n if (opt.mcpServers?.length) opt.mcp = await db.mCPServer.findMany({ where: { slug: { in: opt.mcpServers } }, select: { name: true, slug: true, category: true } });\n else opt.mcp = [];\n if (opt.promptTypes?.length) opt.prompts = await db.promptBlueprint.findMany({ where: { OR: opt.promptTypes.map((t: string) => ({ category: { contains: t, mode: \"insensitive\" as const } })) }, select: { title: true, slug: true, category: true }, take: 5 });\n else opt.prompts = [];\n }\n }\n\n return NextResponse.json(result);\n } catch (err: any) {\n return NextResponse.json({ error: err.message }, { status: 500 });\n }\n}\n"}, "00b5046e2c720b0a2190bee39eb7553e26ab9754": {"t": 1785754647.271821, "n": 4081, "h": "1f8d3d603ddbda66", "c": "import { NextRequest, NextResponse } from \"next/server\";\nimport { getDb } from \"@/lib/db/index\";\n\nconst SYSTEM_PROMPT = `Ты — Senior Product Manager и Staff Engineer. Создай детальный архитектурный план для бизнес-идеи. План должен быть настолько конкретным, чтобы AI-агент (Cursor/Claude) смог по нему писать код без галлюцинаций.\n\nЖЁСТКИЕ ПРАВИЛА:\n1. Сущности БД — с ПОЛНЫМИ полями: имя, тип, описание. Пример: CrawlTarget (id UUID PK, url string, depth number, status enum, createdAt timestamp).\n2. Стек технологий — каждая технология с ОБОСНОВАНИЕМ. Пример: \"BullMQ + Redis — фоновый парсинг тысяч URL без блокировки запросов\".\n3. План — по НЕДЕЛЯМ с конкретными файлами.\n4. Ошибки — специфичные для ниши.\n5. Атомарные промпты — 3 конкретных промпта для Cursor/Claude Code, каждый для одного файла/функции.\n6. Итоговый промпт — один большой промпт для старта проекта.\n\nВерни ТОЛЬКО JSON:\n{\n \"productType\": \"Тип\",\n \"expertRecommendation\": \"Рекомендация\",\n \"recommendedStack\": \"Стек одной строкой\",\n \"options\": [{\n \"name\": \"Название\",\n \"description\": \"Описание\",\n \"complexity\": 5,\n \"mvpDays\": \"7-10 дней\",\n \"pros\": [\"+\"],\n \"cons\": [\"-\"],\n \"monetization\": \"Модель\",\n \"costDev\": \"30-40ч\",\n \"costAi\": \"15/мес\",\n \"costServer\": \"5/мес\",\n \"entities\": [\"Entity (id UUID PK, field1 type, field2 type) - описание\"],\n \"stack\": [{\"tech\": \"Технология\", \"reason\": \"Обоснование\"}],\n \"plan\": [\"Неделя 1: ...\", \"Неделя 2: ...\"],\n \"mcpServers\": [\"slug\"],\n \"patternSlugs\": [\"slug\"],\n \"promptTypes\": [\"тип\"],\n \"mistakes\": [\"Ошибка 1\"],\n \"atomicPrompts\": [\"Промпт 1: В файле lib/X.ts создай функцию...\", \"Промпт 2: ...\", \"Промпт 3: ...\"],\n \"masterPrompt\": \"Ты — senior разработчик. Реализуй проект... (полный промпт)\",\n \"summary\": \"Резюме\",\n \"toolRecommendation\": \"Инструмент\",\n \"aiModelRecommendation\": \"Модель\"\n }]\n}\n3 варианта. Минимум 3 недели в плане. 3 атомарных промпта.`\n\nexport async function POST(req: NextRequest) {\n const { idea, isPro } = await req.json();\n // Free users get limited analysis — only 3 option names/pros/cons, no details\n const isLimited = !isPro;\n if (!idea || idea.length < 10) return NextResponse.json({ error: \"Too short\" }, { status: 400 });\n\n const db = await getDb();\n const settings = await db.siteSettings.findUnique({ where: { id: \"main\" } });\n const key = settings?.deepseekApiKey || process.env.DEEPSEEK_API_KEY || \"\";\n if (!key) return NextResponse.json({ error: \"No key\" }, { status: 500 });\n\n try {\n const res = await fetch(\"https://api.deepseek.com/v1/chat/completions\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: \"Bearer \" + key },\n body: JSON.stringify({ model: \"deepseek-v4-flash\", messages: [{ role: \"system\", content: SYSTEM_PROMPT }, { role: \"user\", content: idea }], max_tokens: 8000, temperature: 0.4 }),\n });\n const data = await res.json();\n const text = data.choices?.[0]?.message?.content || \"\";\n const jsonMatch = text.match(/\\{[\\s\\S]*\\}/);\n const result = jsonMatch ? JSON.parse(jsonMatch[0]) : { error: \"Parse failed\", raw: text };\n\n if (result.options) {\n for (const opt of result.options) {\n if (opt.patternSlugs?.length) opt.patterns = await db.buildPattern.findMany({ where: { slug: { in: opt.patternSlugs }, isPublished: true }, select: { title: true, slug: true, difficulty: true } });\n else opt.patterns = [];\n if (opt.mcpServers?.length) opt.mcp = await db.mCPServer.findMany({ where: { slug: { in: opt.mcpServers } }, select: { name: true, slug: true, category: true } });\n else opt.mcp = [];\n if (opt.promptTypes?.length) opt.prompts = await db.promptBlueprint.findMany({ where: { OR: opt.promptTypes.map((t: string) => ({ category: { contains: t, mode: \"insensitive\" as const } })) }, select: { title: true, slug: true, category: true }, take: 5 });\n else opt.prompts = [];\n }\n }\n\n return NextResponse.json(result);\n } catch (err: any) {\n return NextResponse.json({ error: err.message }, { status: 500 });\n }\n}\n"}, "9fe36be870eb44555a72c1cabe6340fff333c0e1": {"t": 1785754649.203, "n": 4081, "h": "34688fdca198554e", "c": "import { NextRequest, NextResponse } from \"next/server\";\nimport { getDb } from \"@/lib/db/index\";\n\nconst SYSTEM_PROMPT = `Ты — Senior Product Manager и Staff Engineer. Создай детальный архитектурный план для бизнес-идеи. План должен быть настолько конкретным, чтобы AI-агент (Cursor/Claude) смог по нему писать код без галлюцинаций.\n\nЖЁСТКИЕ ПРАВИЛА:\n1. Сущности БД — с ПОЛНЫМИ полями: имя, тип, описание. Пример: CrawlTarget (id UUID PK, url string, depth number, status enum, createdAt timestamp).\n2. Стек технологий — каждая технология с ОБОСНОВАНИЕМ. Пример: \"BullMQ + Redis — фоновый парсинг тысяч URL без блокировки запросов\".\n3. План — по НЕДЕЛЯМ с конкретными файлами.\n4. Ошибки — специфичные для ниши.\n5. Атомарные промпты — 3 конкретных промпта для Cursor/Claude Code, каждый для одного файла/функции.\n6. Итоговый промпт — один большой промпт для старта проекта.\n\nВерни ТОЛЬКО JSON:\n{\n \"productType\": \"Тип\",\n \"expertRecommendation\": \"Рекомендация\",\n \"recommendedStack\": \"Стек одной строкой\",\n \"options\": [{\n \"name\": \"Название\",\n \"description\": \"Описание\",\n \"complexity\": 5,\n \"mvpDays\": \"7-10 дней\",\n \"pros\": [\"+\"],\n \"cons\": [\"-\"],\n \"monetization\": \"Модель\",\n \"costDev\": \"30-40ч\",\n \"costAi\": \"15/мес\",\n \"costServer\": \"5/мес\",\n \"entities\": [\"Entity (id UUID PK, field1 type, field2 type) - описание\"],\n \"stack\": [{\"tech\": \"Технология\", \"reason\": \"Обоснование\"}],\n \"plan\": [\"Неделя 1: ...\", \"Неделя 2: ...\"],\n \"mcpServers\": [\"slug\"],\n \"patternSlugs\": [\"slug\"],\n \"promptTypes\": [\"тип\"],\n \"mistakes\": [\"Ошибка 1\"],\n \"atomicPrompts\": [\"Промпт 1: В файле lib/X.ts создай функцию...\", \"Промпт 2: ...\", \"Промпт 3: ...\"],\n \"masterPrompt\": \"Ты — senior разработчик. Реализуй проект... (полный промпт)\",\n \"summary\": \"Резюме\",\n \"toolRecommendation\": \"Инструмент\",\n \"aiModelRecommendation\": \"Модель\"\n }]\n}\n3 варианта. Минимум 3 недели в плане. 3 атомарных промпта.`\n\nexport async function POST(req: NextRequest) {\n const { idea, isPro } = await req.json();\n // Free users get limited analysis — only 3 option names/pros/cons, no details\n const isLimited = !isPro;\n if (!idea || idea.length < 10) return NextResponse.json({ error: \"Too short\" }, { status: 400 });\n\n const db = await getDb();\n const settings = await db.siteSettings.findUnique({ where: { id: \"main\" } });\n const key = settings?.deepseekApiKey || process.env.DEEPSEEK_API_KEY || \"\";\n if (!key) return NextResponse.json({ error: \"No key\" }, { status: 500 });\n\n try {\n const res = await fetch(\"https://api.deepseek.com/v1/chat/completions\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: \"Bearer \" + key },\n body: JSON.stringify({ model: \"deepseek-v4-flash\", messages: [{ role: \"system\", content: SYSTEM_PROMPT }, { role: \"user\", content: idea }], max_tokens: 6000, temperature: 0.4 }),\n });\n const data = await res.json();\n const text = data.choices?.[0]?.message?.content || \"\";\n const jsonMatch = text.match(/\\{[\\s\\S]*\\}/);\n const result = jsonMatch ? JSON.parse(jsonMatch[0]) : { error: \"Parse failed\", raw: text };\n\n if (result.options) {\n for (const opt of result.options) {\n if (opt.patternSlugs?.length) opt.patterns = await db.buildPattern.findMany({ where: { slug: { in: opt.patternSlugs }, isPublished: true }, select: { title: true, slug: true, difficulty: true } });\n else opt.patterns = [];\n if (opt.mcpServers?.length) opt.mcp = await db.mCPServer.findMany({ where: { slug: { in: opt.mcpServers } }, select: { name: true, slug: true, category: true } });\n else opt.mcp = [];\n if (opt.promptTypes?.length) opt.prompts = await db.promptBlueprint.findMany({ where: { OR: opt.promptTypes.map((t: string) => ({ category: { contains: t, mode: \"insensitive\" as const } })) }, select: { title: true, slug: true, category: true }, take: 5 });\n else opt.prompts = [];\n }\n }\n\n return NextResponse.json(result);\n } catch (err: any) {\n return NextResponse.json({ error: err.message }, { status: 500 });\n }\n}\n"}, "ec85868795e72213a7181351c2a67ce757a2e3af": {"t": 1785754650.4705367, "n": 4203, "h": "693b4380eeb4f4f1", "c": "import { NextRequest, NextResponse } from \"next/server\";\nimport { getDb } from \"@/lib/db/index\";\n\nconst SYSTEM_PROMPT = `Ты — Senior Product Manager и Staff Engineer. Создай детальный архитектурный план. Он должен быть настолько конкретным, чтобы новичок понял суть, а AI-агент смог по нему писать код без галлюцинаций.\n\nЖЁСТКИЕ ПРАВИЛА:\n1. Сущности БД — бизнес-специфичные имена с типами полей. НЕ использовать User/Project/Settings. Пример: CrawlTarget (id, url, lastCrawled, status), AuditReport (id, siteId, score, errors[], createdAt).\n2. Не противоречить в стеке. Выбрал Next.js — весь документ про Next.js.\n3. Ошибки — нишевые. Не \"пишите тесты\", а \"Блокировка IP парсера через Cloudflare\".\n4. План — 5-8 КОНКРЕТНЫХ шагов с файлами/таблицами/пакетами. Не \"Основной функционал\", а \"Создать коллекцию Article в Strapi: поля title, content, category. Связь one-to-many с Category. Права: editor read/write\".\n5. Каждый шаг: (а) что создаётся, (б) ключевые пакеты, (в) что работает после шага.\n\nВерни ТОЛЬКО JSON:\n{\n \"productType\": \"Тип\",\n \"expertRecommendation\": \"Какой вариант выбрать и почему. 2-3 предложения.\",\n \"recommendedStack\": \"Стек (Next.js + Prisma + PostgreSQL + Tailwind)\",\n \"options\": [{\n \"name\": \"Название\",\n \"description\": \"Описание подхода\",\n \"complexity\": 5,\n \"mvpDays\": \"7-10 дней\",\n \"pros\": [\"Плюс 1\", \"Плюс 2\"],\n \"cons\": [\"Минус 1\"],\n \"monetization\": \"Модель\",\n \"costDev\": \"30-40 часов\",\n \"costAi\": \"15/мес\",\n \"costServer\": \"5/мес\",\n \"entities\": [\"EntityName (id, field1, field2) - описание\", \"...\"],\n \"plan\": [\n \"Шаг 1: Конкретно что создаётся, какие пакеты, что заработает\",\n \"Шаг 2: ...\"\n ],\n \"mcpServers\": [\"slug1\"],\n \"patternSlugs\": [\"slug1\"],\n \"promptTypes\": [\"тип1\"],\n \"mistakes\": [\"Нишевая ошибка 1\", \"Нишевая ошибка 2\"],\n \"summary\": \"Резюме, 2-3 предложения\",\n \"toolRecommendation\": \"Каким инструментом делать и почему\",\n \"aiModelRecommendation\": \"Какую модель использовать и почему\"\n }]\n}\n3 варианта. От простого к сложному. План — 5-8 шагов.`;\n\nexport async function POST(req: NextRequest) {\n const { idea, isPro } = await req.json();\n // Free users get limited analysis — only 3 option names/pros/cons, no details\n const isLimited = !isPro;\n if (!idea || idea.length < 10) return NextResponse.json({ error: \"Too short\" }, { status: 400 });\n\n const db = await getDb();\n const settings = await db.siteSettings.findUnique({ where: { id: \"main\" } });\n const key = settings?.deepseekApiKey || process.env.DEEPSEEK_API_KEY || \"\";\n if (!key) return NextResponse.json({ error: \"No key\" }, { status: 500 });\n\n try {\n const res = await fetch(\"https://api.deepseek.com/v1/chat/completions\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: \"Bearer \" + key },\n body: JSON.stringify({ model: \"deepseek-v4-flash\", messages: [{ role: \"system\", content: SYSTEM_PROMPT }, { role: \"user\", content: idea }], max_tokens: 6000, temperature: 0.4 }),\n });\n const data = await res.json();\n const text = data.choices?.[0]?.message?.content || \"\";\n const jsonMatch = text.match(/\\{[\\s\\S]*\\}/);\n const result = jsonMatch ? JSON.parse(jsonMatch[0]) : { error: \"Parse failed\", raw: text };\n\n if (result.options) {\n for (const opt of result.options) {\n if (opt.patternSlugs?.length) opt.patterns = await db.buildPattern.findMany({ where: { slug: { in: opt.patternSlugs }, isPublished: true }, select: { title: true, slug: true, difficulty: true } });\n else opt.patterns = [];\n if (opt.mcpServers?.length) opt.mcp = await db.mCPServer.findMany({ where: { slug: { in: opt.mcpServers } }, select: { name: true, slug: true, category: true } });\n else opt.mcp = [];\n if (opt.promptTypes?.length) opt.prompts = await db.promptBlueprint.findMany({ where: { OR: opt.promptTypes.map((t: string) => ({ category: { contains: t, mode: \"insensitive\" as const } })) }, select: { title: true, slug: true, category: true }, take: 5 });\n else opt.prompts = [];\n }\n }\n\n return NextResponse.json(result);\n } catch (err: any) {\n return NextResponse.json({ error: err.message }, { status: 500 });\n }\n}\n"}, "f46d66cf537c89a97b2b89337fee7ea4cc89362f": {"t": 1785754652.0823817, "n": 4692, "h": "af35d38df94bb89d", "c": "import { NextRequest, NextResponse } from \"next/server\";\nimport { getDb } from \"@/lib/db/index\";\n\nconst SYSTEM_PROMPT = `Ты — Senior Product Manager и Staff Engineer. Создай детальный архитектурный план. Он должен быть настолько конкретным, чтобы новичок понял суть, а AI-агент смог по нему писать код без галлюцинаций.\n\nЖЁСТКИЕ ПРАВИЛА:\n1. Сущности БД — бизнес-специфичные имена с типами полей. НЕ использовать User/Project/Settings. Пример: CrawlTarget (id, url, lastCrawled, status), AuditReport (id, siteId, score, errors[], createdAt).\n2. Не противоречить в стеке. Выбрал Next.js — весь документ про Next.js.\n3. Ошибки — нишевые. Не \"пишите тесты\", а \"Блокировка IP парсера через Cloudflare\".\n4. План — 5-8 КОНКРЕТНЫХ шагов с файлами/таблицами/пакетами. Не \"Основной функционал\", а \"Создать коллекцию Article в Strapi: поля title, content, category. Связь one-to-many с Category. Права: editor read/write\".\n5. Каждый шаг: (а) что создаётся, (б) ключевые пакеты, (в) что работает после шага.\n\nВерни ТОЛЬКО JSON:\n{\n \"productType\": \"Тип\",\n \"expertRecommendation\": \"Какой вариант выбрать и почему. 2-3 предложения.\",\n \"recommendedStack\": \"Стек (Next.js + Prisma + PostgreSQL + Tailwind)\",\n \"options\": [{\n \"name\": \"Название\",\n \"description\": \"Описание подхода\",\n \"complexity\": 5,\n \"mvpDays\": \"7-10 дней\",\n \"pros\": [\"Плюс 1\", \"Плюс 2\"],\n \"cons\": [\"Минус 1\"],\n \"monetization\": \"Модель\",\n \"costDev\": \"30-40 часов\",\n \"costAi\": \"15/мес\",\n \"costServer\": \"5/мес\",\n \"entities\": [\"EntityName (id, field1, field2) - описание\", \"...\"],\n \"plan\": [\n \"Шаг 1: Конкретно что создаётся, какие пакеты, что заработает\",\n \"Шаг 2: ...\"\n ],\n \"mcpServers\": [\"slug1\"],\n \"patternSlugs\": [\"slug1\"],\n \"promptTypes\": [\"тип1\"],\n \"mistakes\": [\"Нишевая ошибка 1\", \"Нишевая ошибка 2\"],\n \"summary\": \"Резюме, 2-3 предложения\",\n \"toolRecommendation\": \"Каким инструментом делать и почему\",\n \"aiModelRecommendation\": \"Какую модель использовать и почему\"\n }]\n}\n3 варианта. От простого к сложному. План — 5-8 шагов.`;\n\nexport async function POST(req: NextRequest) {\n const { idea, isPro } = await req.json();\n // Free users get limited analysis — only 3 option names/pros/cons, no details\n const isLimited = !isPro;\n if (!idea || idea.length < 10) return NextResponse.json({ error: \"Too short\" }, { status: 400 });\n\n const db = await getDb();\n const settings = await db.siteSettings.findUnique({ where: { id: \"main\" } });\n const key = settings?.deepseekApiKey || process.env.DEEPSEEK_API_KEY || \"\";\n if (!key) return NextResponse.json({ error: \"No key\" }, { status: 500 });\n\n try {\n const res = await fetch(\"https://api.deepseek.com/v1/chat/completions\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: \"Bearer \" + key },\n body: JSON.stringify({ model: \"deepseek-v4-flash\", messages: [{ role: \"system\", content: SYSTEM_PROMPT }, { role: \"user\", content: idea }], max_tokens: 6000, temperature: 0.4 }),\n });\n const data = await res.json();\n const text = data.choices?.[0]?.message?.content || \"\";\n const jsonMatch = text.match(/\\{[\\s\\S]*\\}/);\n const result = jsonMatch ? JSON.parse(jsonMatch[0]) : { error: \"Parse failed\", raw: text };\n\n if (result.options) {\n for (const opt of result.options) {\n if (opt.patternSlugs?.length) opt.patterns = await db.buildPattern.findMany({ where: { slug: { in: opt.patternSlugs }, isPublished: true }, select: { title: true, slug: true, difficulty: true } });\n else opt.patterns = [];\n if (opt.mcpServers?.length) opt.mcp = await db.mCPServer.findMany({ where: { slug: { in: opt.mcpServers } }, select: { name: true, slug: true, category: true } });\n else opt.mcp = [];\n if (opt.promptTypes?.length) opt.prompts = await db.promptBlueprint.findMany({ where: { OR: opt.promptTypes.map((t: string) => ({ category: { contains: t, mode: \"insensitive\" as const } })) }, select: { title: true, slug: true, category: true }, take: 5 });\n else opt.prompts = [];\n }\n }\n\n if (isLimited && result.options) {\n // Strip detailed fields for free users\n result.options = result.options.map((opt: any) => ({\n name: opt.name, description: opt.description, complexity: opt.complexity,\n mvpDays: opt.mvpDays, pros: opt.pros, cons: opt.cons, monetization: opt.monetization,\n costDev: opt.costDev, costAi: opt.costAi, costServer: opt.costServer,\n summary: opt.summary, toolRecommendation: opt.toolRecommendation,\n }));\n }\n return NextResponse.json(result);\n } catch (err: any) {\n return NextResponse.json({ error: err.message }, { status: 500 });\n }\n}\n"}, "2965a8b19dd5487494e1c05a213d90118feac27b": {"t": 1785754653.4249659, "n": 4087, "h": "f2543559932d7f66", "c": "import { NextRequest, NextResponse } from \"next/server\";\nimport { getDb } from \"@/lib/db/index\";\n\nconst SYSTEM_PROMPT = `Ты — Senior Product Manager и Staff Engineer. Создай детальный архитектурный план. Он должен быть настолько конкретным, чтобы новичок понял суть, а AI-агент смог по нему писать код без галлюцинаций.\n\nЖЁСТКИЕ ПРАВИЛА:\n1. Сущности БД — бизнес-специфичные имена с типами полей. НЕ использовать User/Project/Settings. Пример: CrawlTarget (id, url, lastCrawled, status), AuditReport (id, siteId, score, errors[], createdAt).\n2. Не противоречить в стеке. Выбрал Next.js — весь документ про Next.js.\n3. Ошибки — нишевые. Не \"пишите тесты\", а \"Блокировка IP парсера через Cloudflare\".\n4. План — 5-8 КОНКРЕТНЫХ шагов с файлами/таблицами/пакетами. Не \"Основной функционал\", а \"Создать коллекцию Article в Strapi: поля title, content, category. Связь one-to-many с Category. Права: editor read/write\".\n5. Каждый шаг: (а) что создаётся, (б) ключевые пакеты, (в) что работает после шага.\n\nВерни ТОЛЬКО JSON:\n{\n \"productType\": \"Тип\",\n \"expertRecommendation\": \"Какой вариант выбрать и почему. 2-3 предложения.\",\n \"recommendedStack\": \"Стек (Next.js + Prisma + PostgreSQL + Tailwind)\",\n \"options\": [{\n \"name\": \"Название\",\n \"description\": \"Описание подхода\",\n \"complexity\": 5,\n \"mvpDays\": \"7-10 дней\",\n \"pros\": [\"Плюс 1\", \"Плюс 2\"],\n \"cons\": [\"Минус 1\"],\n \"monetization\": \"Модель\",\n \"costDev\": \"30-40 часов\",\n \"costAi\": \"15/мес\",\n \"costServer\": \"5/мес\",\n \"entities\": [\"EntityName (id, field1, field2) - описание\", \"...\"],\n \"plan\": [\n \"Шаг 1: Конкретно что создаётся, какие пакеты, что заработает\",\n \"Шаг 2: ...\"\n ],\n \"mcpServers\": [\"slug1\"],\n \"patternSlugs\": [\"slug1\"],\n \"promptTypes\": [\"тип1\"],\n \"mistakes\": [\"Нишевая ошибка 1\", \"Нишевая ошибка 2\"],\n \"summary\": \"Резюме, 2-3 предложения\",\n \"toolRecommendation\": \"Каким инструментом делать и почему\",\n \"aiModelRecommendation\": \"Какую модель использовать и почему\"\n }]\n}\n3 варианта. От простого к сложному. План — 5-8 шагов.`;\n\nexport async function POST(req: NextRequest) {\n const { idea } = await req.json();\n if (!idea || idea.length < 10) return NextResponse.json({ error: \"Too short\" }, { status: 400 });\n\n const db = await getDb();\n const settings = await db.siteSettings.findUnique({ where: { id: \"main\" } });\n const key = settings?.deepseekApiKey || process.env.DEEPSEEK_API_KEY || \"\";\n if (!key) return NextResponse.json({ error: \"No key\" }, { status: 500 });\n\n try {\n const res = await fetch(\"https://api.deepseek.com/v1/chat/completions\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: \"Bearer \" + key },\n body: JSON.stringify({ model: \"deepseek-v4-flash\", messages: [{ role: \"system\", content: SYSTEM_PROMPT }, { role: \"user\", content: idea }], max_tokens: 6000, temperature: 0.4 }),\n });\n const data = await res.json();\n const text = data.choices?.[0]?.message?.content || \"\";\n const jsonMatch = text.match(/\\{[\\s\\S]*\\}/);\n const result = jsonMatch ? JSON.parse(jsonMatch[0]) : { error: \"Parse failed\", raw: text };\n\n if (result.options) {\n for (const opt of result.options) {\n if (opt.patternSlugs?.length) opt.patterns = await db.buildPattern.findMany({ where: { slug: { in: opt.patternSlugs }, isPublished: true }, select: { title: true, slug: true, difficulty: true } });\n else opt.patterns = [];\n if (opt.mcpServers?.length) opt.mcp = await db.mCPServer.findMany({ where: { slug: { in: opt.mcpServers } }, select: { name: true, slug: true, category: true } });\n else opt.mcp = [];\n if (opt.promptTypes?.length) opt.prompts = await db.promptBlueprint.findMany({ where: { OR: opt.promptTypes.map((t: string) => ({ category: { contains: t, mode: \"insensitive\" as const } })) }, select: { title: true, slug: true, category: true }, take: 5 });\n else opt.prompts = [];\n }\n }\n\n return NextResponse.json(result);\n } catch (err: any) {\n return NextResponse.json({ error: err.message }, { status: 500 });\n }\n}\n"}, "02e4ce90cf2d6daca5c1996b8befa1aa459e5e43": {"t": 1785754654.81164, "n": 4087, "h": "4cb40dadc74399a4", "c": "import { NextRequest, NextResponse } from \"next/server\";\nimport { getDb } from \"@/lib/db/index\";\n\nconst SYSTEM_PROMPT = `Ты — Senior Product Manager и Staff Engineer. Создай детальный архитектурный план. Он должен быть настолько конкретным, чтобы новичок понял суть, а AI-агент смог по нему писать код без галлюцинаций.\n\nЖЁСТКИЕ ПРАВИЛА:\n1. Сущности БД — бизнес-специфичные имена с типами полей. НЕ использовать User/Project/Settings. Пример: CrawlTarget (id, url, lastCrawled, status), AuditReport (id, siteId, score, errors[], createdAt).\n2. Не противоречить в стеке. Выбрал Next.js — весь документ про Next.js.\n3. Ошибки — нишевые. Не \"пишите тесты\", а \"Блокировка IP парсера через Cloudflare\".\n4. План — 5-8 КОНКРЕТНЫХ шагов с файлами/таблицами/пакетами. Не \"Основной функционал\", а \"Создать коллекцию Article в Strapi: поля title, content, category. Связь one-to-many с Category. Права: editor read/write\".\n5. Каждый шаг: (а) что создаётся, (б) ключевые пакеты, (в) что работает после шага.\n\nВерни ТОЛЬКО JSON:\n{\n \"productType\": \"Тип\",\n \"expertRecommendation\": \"Какой вариант выбрать и почему. 2-3 предложения.\",\n \"recommendedStack\": \"Стек (Next.js + Prisma + PostgreSQL + Tailwind)\",\n \"options\": [{\n \"name\": \"Название\",\n \"description\": \"Описание подхода\",\n \"complexity\": 5,\n \"mvpDays\": \"7-10 дней\",\n \"pros\": [\"Плюс 1\", \"Плюс 2\"],\n \"cons\": [\"Минус 1\"],\n \"monetization\": \"Модель\",\n \"costDev\": \"30-40 часов\",\n \"costAi\": \"15/мес\",\n \"costServer\": \"5/мес\",\n \"entities\": [\"EntityName (id, field1, field2) - описание\", \"...\"],\n \"plan\": [\n \"Шаг 1: Конкретно что создаётся, какие пакеты, что заработает\",\n \"Шаг 2: ...\"\n ],\n \"mcpServers\": [\"slug1\"],\n \"patternSlugs\": [\"slug1\"],\n \"promptTypes\": [\"тип1\"],\n \"mistakes\": [\"Нишевая ошибка 1\", \"Нишевая ошибка 2\"],\n \"summary\": \"Резюме, 2-3 предложения\",\n \"toolRecommendation\": \"Каким инструментом делать и почему\",\n \"aiModelRecommendation\": \"Какую модель использовать и почему\"\n }]\n}\n3 варианта. От простого к сложному. План — 5-8 шагов.`;\n\nexport async function POST(req: NextRequest) {\n const { idea } = await req.json();\n if (!idea || idea.length < 10) return NextResponse.json({ error: \"Too short\" }, { status: 400 });\n\n const db = await getDb();\n const settings = await db.siteSettings.findUnique({ where: { id: \"main\" } });\n const key = settings?.deepseekApiKey || process.env.DEEPSEEK_API_KEY || \"\";\n if (!key) return NextResponse.json({ error: \"No key\" }, { status: 500 });\n\n try {\n const res = await fetch(\"https://api.deepseek.com/v1/chat/completions\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: \"Bearer \" + key },\n body: JSON.stringify({ model: \"deepseek-v4-flash\", messages: [{ role: \"system\", content: SYSTEM_PROMPT }, { role: \"user\", content: idea }], max_tokens: 4000, temperature: 0.4 }),\n });\n const data = await res.json();\n const text = data.choices?.[0]?.message?.content || \"\";\n const jsonMatch = text.match(/\\{[\\s\\S]*\\}/);\n const result = jsonMatch ? JSON.parse(jsonMatch[0]) : { error: \"Parse failed\", raw: text };\n\n if (result.options) {\n for (const opt of result.options) {\n if (opt.patternSlugs?.length) opt.patterns = await db.buildPattern.findMany({ where: { slug: { in: opt.patternSlugs }, isPublished: true }, select: { title: true, slug: true, difficulty: true } });\n else opt.patterns = [];\n if (opt.mcpServers?.length) opt.mcp = await db.mCPServer.findMany({ where: { slug: { in: opt.mcpServers } }, select: { name: true, slug: true, category: true } });\n else opt.mcp = [];\n if (opt.promptTypes?.length) opt.prompts = await db.promptBlueprint.findMany({ where: { OR: opt.promptTypes.map((t: string) => ({ category: { contains: t, mode: \"insensitive\" as const } })) }, select: { title: true, slug: true, category: true }, take: 5 });\n else opt.prompts = [];\n }\n }\n\n return NextResponse.json(result);\n } catch (err: any) {\n return NextResponse.json({ error: err.message }, { status: 500 });\n }\n}\n"}, "4a528d3da2946ab713d42d9a410218d4f58ee8ec": {"t": 1785754656.101596, "n": 3486, "h": "c87cb8dd9d220fd2", "c": "import { NextRequest, NextResponse } from \"next/server\";\nimport { getDb } from \"@/lib/db/index\";\n\nexport async function POST(req: NextRequest) {\n const { idea } = await req.json();\n if (!idea || idea.length < 10) return NextResponse.json({ error: \"Слишком короткое описание\" }, { status: 400 });\n\n const db = await getDb();\n const settings = await db.siteSettings.findUnique({ where: { id: \"main\" } });\n const key = settings?.deepseekApiKey || process.env.DEEPSEEK_API_KEY || \"\";\n if (!key) return NextResponse.json({ error: \"API key not configured\" }, { status: 500 });\n\n const systemPrompt = `Ты — AI-Архитектор. Проанализируй бизнес-идею и верни ТОЛЬКО JSON (без markdown):\n\n{\n \"productType\": \"Тип продукта (SaaS/Бот/Лендинг/CRM/Маркетплейс/API)\",\n \"expertRecommendation\": \"Краткая экспертная рекомендация: какой вариант выбрать и почему. 2-3 предложения.\",\n \"options\": [\n {\n \"name\": \"Название варианта (например: Быстрый MVP)\",\n \"description\": \"Краткое описание подхода, 1-2 предложения\",\n \"complexity\": число 1-10,\n \"mvpDays\": \"Оценка (5-7 дней)\",\n \"pros\": [\"Плюс 1\", \"Плюс 2\", \"Плюс 3\"],\n \"cons\": [\"Минус 1\", \"Минус 2\"],\n \"monetization\": \"Модель\",\n \"costDev\": \"20-30 часов\",\n \"costAi\": \"15/мес\",\n \"costServer\": \"5/мес\",\n \"entities\": [\"Сущность1 - описание\", \"Сущность2\"],\n \"plan\": [\"Этап 1: ...\", \"Этап 2: ...\", \"Этап 3: ...\"],\n \"mcpServers\": [\"slug1\", \"slug2\"],\n \"patternSlugs\": [\"slug1\", \"slug2\"],\n \"promptTypes\": [\"тип1\", \"тип2\"],\n \"mistakes\": [\"Ошибка 1\", \"Ошибка 2\"],\n \"summary\": \"Резюме варианта, 2-3 предложения\"\n }\n ]\n}\n\nСоздай РОВНО 3 варианта: от простого к сложному. Первый — быстрый MVP, второй — сбалансированный, третий — максимальный. Варианты должны реально отличаться архитектурой и стеком, а не формулировками.`;\n\n try {\n const res = await fetch(\"https://api.deepseek.com/v1/chat/completions\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: \"Bearer \" + key },\n body: JSON.stringify({ model: \"deepseek-v4-flash\", messages: [{ role: \"system\", content: systemPrompt }, { role: \"user\", content: idea }], max_tokens: 3000, temperature: 0.5 }),\n });\n const data = await res.json();\n const text = data.choices?.[0]?.message?.content || \"\";\n const jsonMatch = text.match(/\\{[\\s\\S]*\\}/);\n const result = jsonMatch ? JSON.parse(jsonMatch[0]) : { error: \"Parse failed\", raw: text };\n\n // Enrich each option: resolve slugs to actual DB entities\n if (result.options) {\n for (const opt of result.options) {\n if (opt.patternSlugs?.length) opt.patterns = await db.buildPattern.findMany({ where: { slug: { in: opt.patternSlugs }, isPublished: true }, select: { title: true, slug: true, difficulty: true } });\n else opt.patterns = [];\n if (opt.mcpServers?.length) opt.mcp = await db.mCPServer.findMany({ where: { slug: { in: opt.mcpServers } }, select: { name: true, slug: true, category: true } });\n else opt.mcp = [];\n if (opt.promptTypes?.length) opt.prompts = await db.promptBlueprint.findMany({ where: { OR: opt.promptTypes.map((t: string) => ({ category: { contains: t, mode: \"insensitive\" as const } })) }, select: { title: true, slug: true, category: true }, take: 5 });\n else opt.prompts = [];\n }\n }\n\n return NextResponse.json(result);\n } catch (err: any) {\n return NextResponse.json({ error: err.message }, { status: 500 });\n }\n}\n"}, "6da0191c05584a150c6568c1e535cff96ba4e03e": {"t": 1785754657.5105467, "n": 3452, "h": "2e1b90cc6aaf4764", "c": "import { NextRequest, NextResponse } from \"next/server\";\nimport { getDb } from \"@/lib/db/index\";\n\nexport async function POST(req: NextRequest) {\n const { idea } = await req.json();\n if (!idea || idea.length < 10) return NextResponse.json({ error: \"Слишком короткое описание\" }, { status: 400 });\n\n const db = await getDb();\n const settings = await db.siteSettings.findUnique({ where: { id: \"main\" } });\n const key = settings?.deepseekApiKey || process.env.DEEPSEEK_API_KEY || \"\";\n if (!key) return NextResponse.json({ error: \"API key not configured\" }, { status: 500 });\n\n const systemPrompt = `Ты — AI-Архитектор. Проанализируй бизнес-идею и верни ТОЛЬКО JSON (без markdown):\n\n{\n \"productType\": \"Тип продукта (SaaS/Бот/Лендинг/CRM/Маркетплейс/API/Другое)\",\n \"complexity\": число 1-10,\n \"mvpDays\": \"Оценка MVP (например: 5-7 дней)\",\n \"monetization\": \"Модель монетизации\",\n \"entities\": [\"Сущность1 - описание\", \"Сущность2 - описание\", ...],\n \"integrations\": [\"Интеграция1\", \"Интеграция2\", ...],\n \"mcpServers\": [\"mcp-сервер1\", \"mcp-сервер2\", ...],\n \"promptTypes\": [\"тип промпта1\", \"тип промпта2\", ...],\n \"patternSlugs\": [\"slug-паттерна1\", ...],\n \"mistakes\": [\"Типичная ошибка 1\", \"Типичная ошибка 2\", ...],\n \"costDev\": \"Часы разработки (например: 20-30 часов)\",\n \"costAi\": \"AI-расходы (например: 15/мес)\",\n \"costServer\": \"Сервер (например: 5/мес)\",\n \"plan\": [\"Этап 1: ...\", \"Этап 2: ...\", \"Этап 3: ...\"],\n \"summary\": \"Краткое резюме проекта, 2-3 предложения\"\n}`;\n\n try {\n const res = await fetch(\"https://api.deepseek.com/v1/chat/completions\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: \"Bearer \" + key },\n body: JSON.stringify({ model: \"deepseek-v4-flash\", messages: [{ role: \"system\", content: systemPrompt }, { role: \"user\", content: idea }], max_tokens: 2000, temperature: 0.5 }),\n });\n const data = await res.json();\n if (!res.ok) return NextResponse.json({ error: \"DeepSeek API error: \" + (data.error?.message || res.status), raw: \"\", patterns: [], mcp: [], prompts: [] });\n const text = data.choices?.[0]?.message?.content || \"\";\n\n // Extract JSON from response\n const jsonMatch = text.match(/\\{[\\s\\S]*\\}/);\n const result = jsonMatch ? JSON.parse(jsonMatch[0]) : { error: \"Parse failed\", raw: text };\n\n // Enrich: resolve pattern slugs to actual patterns\n if (result.patternSlugs?.length > 0) {\n result.patterns = await db.buildPattern.findMany({\n where: { slug: { in: result.patternSlugs }, isPublished: true },\n select: { title: true, slug: true, difficulty: true, stack: true },\n });\n } else { result.patterns = []; }\n\n // Enrich: resolve MCP servers\n if (result.mcpServers?.length > 0) {\n result.mcp = await db.mCPServer.findMany({\n where: { slug: { in: result.mcpServers } },\n select: { name: true, slug: true, category: true },\n });\n } else { result.mcp = []; }\n\n // Enrich: resolve prompts\n if (result.promptTypes?.length > 0) {\n result.prompts = await db.promptBlueprint.findMany({\n where: { OR: result.promptTypes.map((t: string) => ({ category: { contains: t, mode: \"insensitive\" as const } })) },\n select: { title: true, slug: true, category: true }, take: 5,\n });\n } else { result.prompts = []; }\n\n return NextResponse.json(result);\n } catch (err: any) {\n return NextResponse.json({ error: err.message }, { status: 500 });\n }\n}\n"}}}} |