added all new chanes
This commit is contained in:
@@ -0,0 +1,297 @@
|
||||
// URL پیشفرض n8n chat flow
|
||||
const DEFAULT_N8N_CHAT_URL = 'https://nn.feep.ir/webhook/ad03737e-f857-4b5c-87d0-601327b2081c/chat';
|
||||
const n8nWebhookUrl = import.meta.env.VITE_N8N_WEBHOOK_URL || DEFAULT_N8N_CHAT_URL;
|
||||
const n8nApiKey = import.meta.env.VITE_N8N_API_KEY || '';
|
||||
|
||||
export type ChatMessage = {
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
timestamp?: string;
|
||||
};
|
||||
|
||||
export const isN8nConfigured = Boolean(n8nWebhookUrl);
|
||||
|
||||
// ذخیره sessionId برای هر کاربر (برای حفظ context در Simple Memory)
|
||||
const sessionIdMap = new Map<string, string>();
|
||||
|
||||
// تولید یا دریافت sessionId برای یک کاربر
|
||||
const getOrCreateSessionId = (userId: string): string => {
|
||||
if (!sessionIdMap.has(userId)) {
|
||||
// تولید sessionId جدید (UUID-like)
|
||||
const sessionId = `${userId}-${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
|
||||
sessionIdMap.set(userId, sessionId);
|
||||
console.log('🔑 ایجاد sessionId جدید:', sessionId, 'برای کاربر:', userId);
|
||||
}
|
||||
return sessionIdMap.get(userId)!;
|
||||
};
|
||||
|
||||
// در development از proxy استفاده میکنیم، در production از URL مستقیم
|
||||
const getN8nUrl = () => {
|
||||
// اگر در development هستیم و URL شامل nn.feep.ir است، از proxy استفاده کن
|
||||
const useProxy = import.meta.env.DEV && n8nWebhookUrl.includes('nn.feep.ir');
|
||||
|
||||
if (useProxy) {
|
||||
console.log('🔀 استفاده از proxy برای دور زدن CORS');
|
||||
// استخراج مسیر webhook از URL کامل
|
||||
try {
|
||||
const url = new URL(n8nWebhookUrl);
|
||||
const urlPath = url.pathname;
|
||||
const proxyUrl = `/api/n8n${urlPath}`;
|
||||
console.log('📍 مسیر proxy:', proxyUrl, 'از URL اصلی:', n8nWebhookUrl);
|
||||
return proxyUrl;
|
||||
} catch (error) {
|
||||
console.error('❌ خطا در parse کردن URL:', error);
|
||||
// اگر URL معتبر نیست، از URL مستقیم استفاده کن
|
||||
return n8nWebhookUrl;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('🌐 استفاده از URL مستقیم:', n8nWebhookUrl);
|
||||
return n8nWebhookUrl;
|
||||
};
|
||||
|
||||
/**
|
||||
* ارسال پیام به n8n و دریافت پاسخ
|
||||
* @param message - پیام کاربر
|
||||
* @param userId - شناسه کاربر
|
||||
* @param conversationHistory - تاریخچه مکالمه (اختیاری)
|
||||
* @returns پاسخ از n8n
|
||||
*/
|
||||
export async function sendMessageToN8n(
|
||||
message: string,
|
||||
userId: string,
|
||||
_conversationHistory?: ChatMessage[] // n8n Chat با Simple Memory خودش session را مدیریت میکند، این پارامتر برای backward compatibility است
|
||||
): Promise<string> {
|
||||
if (!isN8nConfigured) {
|
||||
throw new Error(
|
||||
'n8n تنظیم نشده است. مقدار VITE_N8N_WEBHOOK_URL را در فایل .env تنظیم کنید.'
|
||||
);
|
||||
}
|
||||
|
||||
// فرمت n8n Chat Trigger - این workflow از "When chat message received" استفاده میکند
|
||||
// فرمت مورد نیاز: { sessionId, action: "sendMessage", chatInput }
|
||||
const sessionId = getOrCreateSessionId(userId);
|
||||
|
||||
const payload = {
|
||||
sessionId: sessionId,
|
||||
action: 'sendMessage',
|
||||
chatInput: message
|
||||
};
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
// اگر API Key تنظیم شده باشد، اضافه میکنیم
|
||||
if (n8nApiKey) {
|
||||
headers['Authorization'] = `Bearer ${n8nApiKey}`;
|
||||
// یا میتوانید از header دیگری استفاده کنید
|
||||
// headers['X-API-Key'] = n8nApiKey;
|
||||
}
|
||||
|
||||
try {
|
||||
const actualUrl = getN8nUrl();
|
||||
console.log('📤 ارسال به n8n:', { url: actualUrl, originalUrl: n8nWebhookUrl, payload });
|
||||
|
||||
const response = await fetch(actualUrl, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(payload),
|
||||
// اضافه کردن mode برای جلوگیری از CORS issues
|
||||
mode: 'cors',
|
||||
credentials: 'omit'
|
||||
});
|
||||
|
||||
console.log('📥 پاسخ از n8n:', { status: response.status, ok: response.ok });
|
||||
|
||||
if (!response.ok) {
|
||||
let errorText = '';
|
||||
try {
|
||||
errorText = await response.text();
|
||||
// اگر پاسخ HTML است (مثل خطای 500)، پیام بهتری نمایش بده
|
||||
if (errorText.trim().startsWith('<!DOCTYPE') || errorText.includes('<html')) {
|
||||
console.error('❌ خطا از n8n (HTML Error Page):', { status: response.status });
|
||||
throw new Error(
|
||||
`خطا در n8n workflow (${response.status}): خطای داخلی سرور. لطفا workflow را در n8n بررسی کنید:\n` +
|
||||
`1. آیا workflow فعال است؟\n` +
|
||||
`2. آیا Message a model node درست تنظیم شده؟\n` +
|
||||
`3. آیا API Key OpenAI درست است؟\n` +
|
||||
`4. آیا Respond to Webhook node وجود دارد؟`
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.message.includes('خطا در n8n workflow')) {
|
||||
throw e;
|
||||
}
|
||||
errorText = response.statusText;
|
||||
}
|
||||
console.error('❌ خطا از n8n:', { status: response.status, errorText });
|
||||
throw new Error(
|
||||
`خطا در ارتباط با n8n (${response.status}): ${errorText.substring(0, 200) || response.statusText}`
|
||||
);
|
||||
}
|
||||
|
||||
let data;
|
||||
try {
|
||||
// بررسی Content-Type
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
console.log('📋 Content-Type:', contentType);
|
||||
|
||||
const responseText = await response.text();
|
||||
console.log('📄 پاسخ خام از n8n (اولین 500 کاراکتر):', responseText.substring(0, 500));
|
||||
|
||||
if (!responseText || responseText.trim() === '') {
|
||||
throw new Error('پاسخ خالی از n8n دریافت شد.');
|
||||
}
|
||||
|
||||
// اگر پاسخ HTML است (خطای 500)، خطا بده
|
||||
if (responseText.trim().startsWith('<!DOCTYPE') || responseText.includes('<html')) {
|
||||
console.error('❌ پاسخ HTML دریافت شد (خطای سرور)');
|
||||
throw new Error(
|
||||
'n8n خطای سرور برمیگرداند. لطفا بررسی کنید:\n' +
|
||||
'1. آیا workflow فعال است؟\n' +
|
||||
'2. آیا همه node ها درست تنظیم شدهاند؟\n' +
|
||||
'3. آیا Respond to Webhook node وجود دارد و درست تنظیم شده؟'
|
||||
);
|
||||
}
|
||||
|
||||
// تلاش برای parse کردن JSON
|
||||
try {
|
||||
data = JSON.parse(responseText);
|
||||
} catch (jsonError) {
|
||||
// اگر JSON نیست، ممکنه string ساده باشه
|
||||
console.warn('⚠️ پاسخ JSON نیست، به عنوان string استفاده میشود');
|
||||
return responseText.trim();
|
||||
}
|
||||
} catch (parseError) {
|
||||
if (parseError instanceof Error && parseError.message.includes('n8n خطای سرور')) {
|
||||
throw parseError;
|
||||
}
|
||||
console.error('❌ خطا در parse کردن پاسخ:', parseError);
|
||||
throw new Error('فرمت پاسخ n8n نامعتبر است (JSON parse failed): ' + String(parseError));
|
||||
}
|
||||
|
||||
console.log('📦 داده parse شده از n8n:', data);
|
||||
|
||||
// n8n Chat Trigger + AI Agent معمولاً پاسخ را در فرمتهای مختلف برمیگرداند
|
||||
// این فرمتهای رایج را پشتیبانی میکنیم:
|
||||
|
||||
// 0. فرمت AI Agent output (اولویت اول برای n8n chat)
|
||||
if (data.output && typeof data.output === 'string' && data.output.trim()) {
|
||||
console.log('✅ پاسخ از AI Agent (output):', data.output);
|
||||
return data.output.trim();
|
||||
}
|
||||
|
||||
// 0.1. فرمت AI Agent با object output
|
||||
if (data.output && typeof data.output === 'object') {
|
||||
// بررسی فیلدهای مختلف در output
|
||||
if (data.output.output && typeof data.output.output === 'string' && data.output.output.trim()) {
|
||||
console.log('✅ پاسخ از AI Agent (output.output):', data.output.output);
|
||||
return data.output.output.trim();
|
||||
}
|
||||
if (data.output.text && typeof data.output.text === 'string' && data.output.text.trim()) {
|
||||
console.log('✅ پاسخ از AI Agent (output.text):', data.output.text);
|
||||
return data.output.text.trim();
|
||||
}
|
||||
if (data.output.response && typeof data.output.response === 'string' && data.output.response.trim()) {
|
||||
console.log('✅ پاسخ از AI Agent (output.response):', data.output.response);
|
||||
return data.output.response.trim();
|
||||
}
|
||||
}
|
||||
|
||||
// 1. اگر پاسخ از Message a model (GPT) با Simplify Output خاموش است
|
||||
if (data.choices && Array.isArray(data.choices) && data.choices.length > 0) {
|
||||
const content = data.choices[0]?.message?.content;
|
||||
if (content && content.trim()) {
|
||||
console.log('✅ پاسخ از GPT (choices):', content);
|
||||
return content.trim();
|
||||
}
|
||||
}
|
||||
|
||||
// 2. اگر Simplify Output فعال است - پاسخ مستقیم از GPT
|
||||
if (data.response && typeof data.response === 'string' && data.response.trim()) {
|
||||
console.log('✅ پاسخ از n8n (response):', data.response);
|
||||
return data.response.trim();
|
||||
}
|
||||
|
||||
// 3. اگر response خالی است، ببین آیا در جای دیگری هست
|
||||
if (data.response === '' || data.response === null) {
|
||||
// شاید پاسخ در جای دیگری باشد
|
||||
if (data.choices?.[0]?.message?.content) {
|
||||
const content = data.choices[0].message.content;
|
||||
if (content && content.trim()) {
|
||||
console.log('✅ پاسخ از GPT (در choices):', content);
|
||||
return content.trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. فرمتهای دیگر
|
||||
if (typeof data === 'string' && data.trim()) {
|
||||
return data.trim();
|
||||
}
|
||||
|
||||
if (data.message && typeof data.message === 'string' && data.message.trim()) {
|
||||
return data.message.trim();
|
||||
}
|
||||
|
||||
if (data.text && typeof data.text === 'string' && data.text.trim()) {
|
||||
return data.text.trim();
|
||||
}
|
||||
|
||||
if (data.data?.response && typeof data.data.response === 'string' && data.data.response.trim()) {
|
||||
return data.data.response.trim();
|
||||
}
|
||||
|
||||
if (data.data?.message && typeof data.data.message === 'string' && data.data.message.trim()) {
|
||||
return data.data.message.trim();
|
||||
}
|
||||
|
||||
// 5. اگر پاسخ یک object است و هیچ فیلد متنی نداره، خطا بده
|
||||
if (typeof data === 'object') {
|
||||
// بررسی عمیقتر برای پیدا کردن متن
|
||||
const findTextInObject = (obj: any): string | null => {
|
||||
if (typeof obj === 'string' && obj.trim()) {
|
||||
return obj.trim();
|
||||
}
|
||||
if (Array.isArray(obj)) {
|
||||
for (const item of obj) {
|
||||
const text = findTextInObject(item);
|
||||
if (text) return text;
|
||||
}
|
||||
}
|
||||
if (obj && typeof obj === 'object') {
|
||||
for (const key in obj) {
|
||||
if (key === 'content' || key === 'text' || key === 'message' || key === 'response') {
|
||||
const text = findTextInObject(obj[key]);
|
||||
if (text) return text;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const foundText = findTextInObject(data);
|
||||
if (foundText) {
|
||||
console.log('✅ متن پیدا شد در object:', foundText);
|
||||
return foundText;
|
||||
}
|
||||
|
||||
// اگر هیچ متنی پیدا نشد، خطا بده
|
||||
console.error('❌ هیچ متن معتبری در پاسخ پیدا نشد:', JSON.stringify(data, null, 2));
|
||||
throw new Error('پاسخ از n8n خالی است یا فرمت نامعتبر دارد. لطفا Respond to Webhook را بررسی کنید.');
|
||||
}
|
||||
|
||||
throw new Error('فرمت پاسخ n8n نامعتبر است. ساختار: ' + JSON.stringify(data));
|
||||
} catch (error) {
|
||||
console.error('❌ خطا در sendMessageToN8n:', error);
|
||||
if (error instanceof TypeError && error.message.includes('fetch')) {
|
||||
throw new Error('خطا در اتصال به n8n. لطفا بررسی کنید:\n1. URL صحیح است؟\n2. n8n در دسترس است؟\n3. مشکل CORS وجود دارد؟');
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
throw error;
|
||||
}
|
||||
throw new Error('خطا در ارتباط با n8n: ' + String(error));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
const openAiApiKey = import.meta.env.VITE_OPENAI_API_KEY || '';
|
||||
const openAiBaseUrl = import.meta.env.VITE_OPENAI_BASE_URL || 'https://api.openai.com/v1';
|
||||
const openAiModel = import.meta.env.VITE_OPENAI_MODEL || 'gpt-4o-mini';
|
||||
const openAiOrg = import.meta.env.VITE_OPENAI_ORG_ID || '';
|
||||
const openAiProject = import.meta.env.VITE_OPENAI_PROJECT_ID || '';
|
||||
|
||||
export type ChatCompletionRole = 'system' | 'user' | 'assistant';
|
||||
|
||||
export type ChatCompletionMessage = {
|
||||
role: ChatCompletionRole;
|
||||
content: string;
|
||||
};
|
||||
|
||||
export const isOpenAIConfigured = Boolean(openAiApiKey);
|
||||
|
||||
const defaultSystemPrompt =
|
||||
import.meta.env.VITE_OPENAI_SYSTEM_PROMPT ||
|
||||
'You are an AI assistant that responds in Persian (Farsi) with concise, helpful answers for users of a progressive web app.';
|
||||
|
||||
export const systemMessage: ChatCompletionMessage = {
|
||||
role: 'system',
|
||||
content: defaultSystemPrompt,
|
||||
};
|
||||
|
||||
export async function generateAIResponse(messages: ChatCompletionMessage[]): Promise<string> {
|
||||
if (!isOpenAIConfigured) {
|
||||
throw new Error('کلید OpenAI تعریف نشده است. مقدار VITE_OPENAI_API_KEY را تنظیم کنید.');
|
||||
}
|
||||
|
||||
const payload = {
|
||||
model: openAiModel,
|
||||
messages: messages.length ? messages : [systemMessage],
|
||||
temperature: 0.3,
|
||||
max_tokens: 800,
|
||||
};
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${openAiApiKey}`,
|
||||
};
|
||||
|
||||
if (openAiOrg) {
|
||||
headers['OpenAI-Organization'] = openAiOrg;
|
||||
}
|
||||
|
||||
if (openAiProject) {
|
||||
headers['OpenAI-Project'] = openAiProject;
|
||||
}
|
||||
|
||||
const response = await fetch(`${openAiBaseUrl}/chat/completions`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorBody = await response.text();
|
||||
throw new Error(`خطا در دریافت پاسخ از GPT: ${errorBody}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const text: string | undefined = data?.choices?.[0]?.message?.content?.trim();
|
||||
|
||||
if (!text) {
|
||||
throw new Error('پاسخی از هوش مصنوعی دریافت نشد.');
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user