chore: align infrastructure with server

This commit is contained in:
2026-05-02 17:20:14 +05:00
parent e8c924f63a
commit 73c323685f
36 changed files with 785 additions and 540 deletions
+10 -5
View File
@@ -31,11 +31,16 @@ export default defineEventHandler(async (event) => {
orderBy: { updatedAt: 'desc' }
})
const totalCount = await prisma.canvas.count()
const canvases = canvasList.map(c => ({
...c,
nodeCount: Array.isArray(c.nodes) ? (c.nodes as any[]).length : 0,
nodes: undefined
}))
return {
canvases: canvasList.map(c => ({
...c,
nodeCount: Array.isArray(c.nodes) ? (c.nodes as any[]).length : 0,
nodes: undefined
}))
canvases,
totalCount
}
})
+14 -5
View File
@@ -8,6 +8,8 @@ export default defineEventHandler(async (event) => {
const body = await readBody(event)
const { canvasId, context } = body
console.log(`[Execute] nodeId=${nodeId} canvasId=${canvasId} userId=${user.id}`)
if (!nodeId || !canvasId) {
throw createError({ statusCode: 400, message: 'Node ID and Canvas ID are required' })
}
@@ -17,6 +19,8 @@ export default defineEventHandler(async (event) => {
where: { id: canvasId }
})
console.log(`[Execute] Canvas found: ${!!canvas}, nodes count: ${(canvas?.nodes as any[])?.length}`)
if (!canvas) {
throw createError({ statusCode: 404, message: 'Canvas not found' })
}
@@ -30,11 +34,14 @@ export default defineEventHandler(async (event) => {
const nodes = (canvas.nodes as any[]) || []
const nodeIdx = nodes.findIndex(n => n.id === nodeId)
console.log(`[Execute] Node found at index: ${nodeIdx}`)
if (nodeIdx === -1) {
throw createError({ statusCode: 404, message: 'Node not found in canvas' })
throw createError({ statusCode: 404, message: `Node ${nodeId} not found in canvas ${canvasId}` })
}
const node = nodes[nodeIdx]
console.log(`[Execute] Node type: ${node.type}`)
try {
let result = ''
@@ -42,15 +49,17 @@ export default defineEventHandler(async (event) => {
if (node.type === 'ai') {
// Формируем промпт с учетом контекста от соседей
let fullPrompt = node.prompt || node.text || 'Analyze the provided context.'
if (context && context.length > 0) {
const contextText = context.map((c: any) => `[Node ${c.id}]: ${c.text}`).join('\n\n')
fullPrompt = `CONTEXT INFORMATION:\n${contextText}\n\nUSER REQUEST/INSTRUCTION:\n${fullPrompt}`
}
console.log(`[AI Execute] Generating response with context for node ${nodeId}...`)
result = await generateAIResponse(fullPrompt)
// Модель можно задать прямо в узле: node.model = 'qwen_coder' | 'llama' | 'gemma' | 'laguna' | 'nemotron'
const model = node.model || undefined
console.log(`[AI Execute] model=${model || 'default'} nodeId=${nodeId}`)
result = await generateAIResponse(fullPrompt, 'You are a helpful assistant.', model)
// Сохраняем результат в текст узла
nodes[nodeIdx].text = result
+72 -14
View File
@@ -1,31 +1,89 @@
export const generateAIResponse = async (prompt: string, systemPrompt = 'You are a helpful assistant.') => {
const config = useRuntimeConfig()
const apiKey = config.deepseekApiKey || process.env.DEEPSEEK_API_KEY
/**
* XFlow AI Utility — OpenRouter Integration (Stable Models)
*
* Поддерживаемые модели (наиболее стабильные и без жестких rate-limit):
*
* • poolside/laguna-m.1:free — быстрые инженерные задачи, Claude-based (default)
* • nvidia/nemotron-3-super-120b-a12b:free — MoE 120B, тяжёлая аналитика, сложные рассуждения
*/
const OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1/chat/completions'
/** Доступные модели с описанием для выбора в UI */
export const AI_MODELS = {
/** Сложные инженерные и архитектурные задачи, высокая стабильность */
laguna: 'poolside/laguna-m.1:free',
/** MoE 120B тяжёлая аналитика, архитектурный анализ, сложные рассуждения */
nemotron: 'nvidia/nemotron-3-super-120b-a12b:free',
} as const
export type AiModelKey = keyof typeof AI_MODELS
/** Дефолтная модель, если не указана */
const DEFAULT_MODEL: AiModelKey = 'laguna'
export const generateAIResponse = async (
prompt: string,
systemPrompt = 'You are a helpful assistant.',
modelKey?: AiModelKey | string,
): Promise<string> => {
const apiKey =
process.env.NUXT_OPEN_ROUTER_API_KEY ||
process.env.OPEN_ROUTER_API_KEY
if (!apiKey) {
throw new Error('DEEPSEEK_API_KEY is not configured')
throw new Error('OPEN_ROUTER_API_KEY is not configured')
}
// Резолвим модель: принимаем как ключ ('laguna') так и полное имя
const resolvedModel: string =
modelKey && modelKey in AI_MODELS
? AI_MODELS[modelKey as AiModelKey]
: modelKey && modelKey.includes('/')
? modelKey
: AI_MODELS[DEFAULT_MODEL]
console.log(`[AI] Using model: ${resolvedModel}`)
try {
const response = await $fetch('https://api.deepseek.com/v1/chat/completions', {
const response = await $fetch<any>(OPENROUTER_BASE_URL, {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
'Content-Type': 'application/json',
'HTTP-Referer': 'https://a-crm.controlsystem.kz',
'X-Title': 'XFlow AI Node',
},
body: {
model: 'deepseek-chat',
model: resolvedModel,
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: prompt }
{ role: 'user', content: prompt },
],
stream: false
}
}) as any
stream: false,
},
})
return response.choices[0].message.content
const content = response?.choices?.[0]?.message?.content
if (!content) {
throw new Error('Empty response from AI model')
}
return content
} catch (error: any) {
console.error('DeepSeek AI error:', error.data || error.message)
throw new Error(`AI generation failed: ${error.message}`)
console.error(`[AI] Error with model ${resolvedModel}:`, error?.data || error?.message)
throw new Error(`AI generation failed (${resolvedModel}): ${error?.message}`)
}
}
/** Хелпер: список моделей для фронтенда */
export const getAvailableModels = () =>
Object.entries(AI_MODELS).map(([key, modelId]) => ({
key,
modelId,
label: modelLabels[key as AiModelKey],
}))
const modelLabels: Record<AiModelKey, string> = {
laguna: '🏊 Laguna M.1 — стабильная инженерная (Claude)',
nemotron: '🔬 Nemotron Super 120B — тяжёлая аналитика (NVIDIA)',
}