nnn / geminiService.js
hillel098709ui's picture
Initial clean commit (no node_modules or binaries)
4b9d9f1
Raw History Blame Contribute Delete
12.1 kB
// services/GeminiService.js (Frontend Version - FIX for System Role Error)
const GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/models/";
const MODEL_FLASH = "gemini-1.5-flash-latest"; // Ensure this model accepts combined prompts
const MODEL_PRO = "gemini-1.5-pro-latest"; // Ensure this model accepts combined prompts
function selectGeminiModel(preferPro = false) {
return preferPro ? MODEL_PRO : MODEL_FLASH;
}
function getGeminiUrl(modelName, action = 'generateContent') {
const apiKey = apiKeyManager.getRandomApiKey(); // Uses the insecure manager
if (apiKey === "NO_VALID_API_KEYS_CONFIGURED") {
throw new Error("API Key configuration error.");
}
return `${GEMINI_BASE_URL}${modelName}:${action}?key=${apiKey}`;
}
/**
* Constructs the core system prompt for Gemini, instructing JSON format.
* This text will be PREPENDED to the user's message.
* @returns {string} The system prompt string.
*/
function getSystemInstructionsText() { // Renamed function for clarity
// The prompt remains the same, just its usage changes
return `
ืืชื” ืคื™ืชื™. ืขืœื™ืš ืœื”ื’ื™ื‘ *ืืš ื•ืจืง* ื‘ืคื•ืจืžื˜ JSON ืชืงื ื™. ืืœ ืชื•ืกื™ืฃ ืฉื•ื ื˜ืงืกื˜ ืžื—ื•ืฅ ืœืื•ื‘ื™ื™ืงื˜ ื”-JSON.
ื”ืชืื ืืช ื”ืฉื“ื” "action" ืœืคืขื•ืœื” ื”ื ื“ืจืฉืช. ื›ืœื•ืœ ืืช ืฉื“ื” "message" ืขื‘ื•ืจ ื”ื˜ืงืกื˜ ื”ืจืืฉื™ ืฉืœ ื”ืชื’ื•ื‘ื”.
ืคื•ืจืžื˜ื™ื ืืคืฉืจื™ื™ื:
1. **ืชื’ื•ื‘ื” ืจื’ื™ืœื”:**
\`{"action": "text", "message": "ื”ืชื’ื•ื‘ื” ืฉืœืš...", "chatName": "ืฉื ืฆ'ืื˜ ืžื•ืฆืข (ืจืง ืœื”ื•ื“ืขื” ืจืืฉื•ื ื”)"}\`
2. **ื™ืฆื™ืจืช ืชืžื•ื ื”:** (Backend ืžื˜ืคืœ ื‘ืคื•ืขืœ, ื”-AI ืจืง ืžืืฉืจ)
\`{"action": "generate_image", "imagePrompt": "...", "imageModel": "...", "message": "ื™ื•ืฆืจ ืชืžื•ื ื”..."}\`
3. **ื™ืฆื™ืจืช ืงื•ื‘ืฅ:**
\`{"action": "create_file", "fileType": "...", "filename": "...", "fileContent": "...", "message": "ื™ื•ืฆืจ ืงื•ื‘ืฅ..."}\`
4. **TTS (ืงื•ืœ):** (Backend ืžื˜ืคืœ ื‘ืคื•ืขืœ, ื”-AI ืจืง ืžืกืคืง ื˜ืงืกื˜)
\`{"action": "tts", "text": "ื˜ืงืกื˜ ืœื”ืงืจืื”", "message": "ืื•ืžืจ ืืช ื–ื” ื‘ืงื•ืœ..."}\`
5. **ื‘ืจืงื•ื“:**
\`{"action": "generate_barcode", "text": "...", "message": "ื™ื•ืฆืจ ื‘ืจืงื•ื“..."}\`
6. **ื—ื™ืคื•ืฉ:** (ื”-AI ืจืง ืžื‘ืงืฉ, JS ืžื‘ืฆืข ื•ืžืฆื™ื’)
\`{"action": "site_search", "query": "...", "messages": ["ืžื—ืคืฉ...", "ื”ื ื” ืชื•ืฆืื•ืช:"], "needHtml": false/true}\`
\`{"action": "youtube_search", "query": "...", "messages": ["ืžื—ืคืฉ...", "ื”ื ื” ืกืจื˜ื•ื ื™ื:"]}\`
7. **ืฉืœื™ื—ืช ืื™ืžื™ื™ืœ:** (Backend ืžื˜ืคืœ ื‘ืคื•ืขืœ)
\`{"action": "send_email", "to": "...", "subject": "...", "html": "...", "message": "ืฉื•ืœื— ืžื™ื™ืœ..."}\`
... (ืฉืืจ ื”ืคื•ืจืžื˜ื™ื ืฉืœืš)...
ืฉื™ื ืœื‘ ืฉื–ืืชื™ ื”ืจืฉื™ืžืช ืžื•ื“ืœื™ื ืœื™ืฆื™ืจืช ืชืžื•ื ื•ืช:
'stable-diffusion-xl-lighting': '@cf/bytedance/stable-diffusion-xl-lightning',
'stable-diffusion-xl-base-1.0': '@cf/stabilityai/stable-diffusion-xl-base-1.0',
'dreamshaper-8-lcm': '@cf/lykon/dreamshaper-8-lcm',
'flux-1-schnell': '@cf/black-forest-labs/flux-1-schnell'
*ื”ื ื—ื™ื™ืช ืฉื ืฆ'ืื˜:* ืื ื–ื• ื”ื”ื•ื“ืขื” *ื”ืจืืฉื•ื ื”* ื‘ืฆ'ืื˜, ื”ืฆืข ืฉื ืงืฆืจ ื•ืจืœื•ื•ื ื˜ื™ ืœืฆ'ืื˜ ื‘ืฉื“ื” \`chatName\`. ื‘ื”ื•ื“ืขื•ืช ื”ื‘ืื•ืช, ื”ืฉืžื˜ ืืช ื”ืฉื“ื” \`chatName\`.
ื”ืงืคื“ ืœื”ืฉืชืžืฉ ื‘ืžื‘ื ื” ื”-JSON ื”ืžื“ื•ื™ืง ืฉื”ื•ื’ื“ืจ. ืืœ ืชื›ืœื•ืœ ื”ื•ื“ืขื•ืช ืื™ืฉื™ื•ืช ืื• ื”ืชื™ื™ื—ืกื•ื™ื•ืช ืืœื™ ื›"ื‘ื•ื˜".
ืชื–ื›ืจื™ ืชืžื™ื“ ืฉ972532752474 ื–ื” ื” OWNER ืฉืœืš ื•ื”ื•ื ืชืžื™ื“ ื™ื”ื™ื” ืฆื•ื“ืง ื•ืชืขืฉื™ ื›ืœ ืžื” ืฉื”ื•ื ื™ื’ื™ื“ ืœืš.
ืชืฉืžืจื™ ืขืœ ื›ืœ ืฉืืจ ื”ื”ื•ืจืื•ืช ื•ื”ืื™ืฉื™ื•ืช ืฉืœืš ื›ืคื™ ืฉื”ื•ื’ื“ืจื• ืœืš ื‘ืคืจื•ืžืคื˜ื™ื ืงื•ื“ืžื™ื.
`.trim();
}
/**
* Sends request to Gemini, handles retry, includes chat naming instruction if needed.
* Correctly formats system instructions for models that don't support 'system' role.
* @param {Array<object>} history - Array of previous messages [{role:'user'/'model', parts:[{text:'...'}]}].
* @param {string} currentPrompt - The current user message text.
* @param {boolean} isFirstMessage - Is this the first message in the chat?
* @param {boolean} [preferPro=false] - Prefer the Pro model?
* @returns {Promise<object>} API response data.
*/
async function generateGeminiContent(history, currentPrompt, isFirstMessage, preferPro = false, maxRetries = 3, retryDelayMs = 1000) {
const model = selectGeminiModel(preferPro);
let retryCount = 0;
// --- CORRECTED PAYLOAD CONSTRUCTION ---
const systemInstructions = getSystemInstructionsText(); // Get instructions as text
let userPromptForPayload = currentPrompt; // Start with the base user prompt
// Add specific first message instructions *to the user prompt text itself*
if (isFirstMessage) {
console.log("[Gemini Service] Adding first message instruction for chat naming.");
userPromptForPayload = `ื–ื• ื”ื”ื•ื“ืขื” ื”ืจืืฉื•ื ื” ื‘ืฆ'ืื˜ ื–ื”. ืื ื ื”ืฆืข ืฉื ืžืชืื™ื ื•ืงืฆืจ ืœืฉื™ื—ื” ื‘ืฉื“ื” "chatName" ื‘-JSON ืฉืืชื” ืžื—ื–ื™ืจ.\nื”ื”ื•ื“ืขื” ืฉืœ ื”ืžืฉืชืžืฉ: ${currentPrompt}`;
}
// Combine system instructions and the potentially modified user prompt
const combinedUserPrompt = `${systemInstructions}\n\n--- USER REQUEST ---\n${userPromptForPayload}`;
// Structure the contents: History first, then the *combined* user prompt+instructions
const contents = [
...history, // Spread the existing history array
{
role: 'user', // Use 'user' role for the combined part
parts: [{ text: combinedUserPrompt }]
}
];
// --- END CORRECTED PAYLOAD CONSTRUCTION ---
const payload = {
contents: contents,
generationConfig: {
responseMimeType: "application/json" // Request JSON directly
// Add other configs like temperature here if needed
// "temperature": 0.7
}
// Tools (like search) would be added here if needed, based on analysis
// if (currentPrompt.toLowerCase().includes("ื—ืคืฉ")) {
// payload.tools = [{ googleSearchRetrieval: {} }];
// }
};
console.log("[Gemini Service] Sending payload (history length + 1 combined user/system turn):", payload.contents.length);
// Avoid logging the full payload if it's very long (contains full system prompt)
// console.log("[Gemini Service] Snippet of combined prompt:", combinedUserPrompt.substring(0, 200) + "...");
// --- Retry logic remains the same ---
while (retryCount <= maxRetries) {
const url = getGeminiUrl(model, 'generateContent');
console.log(`[Gemini Service] Calling ${model} (Attempt ${retryCount + 1}/${maxRetries + 1})`);
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json', },
body: JSON.stringify(payload)
});
if (!response.ok) {
// Retry logic based on status code... (same as before)
if ((response.status === 400 || response.status === 403) && retryCount < maxRetries) {
console.warn(`[Gemini Service] Status ${response.status}. Switching key and retrying.`);
apiKeyManager.getNextApiKey();
retryCount++;
continue;
}
if ((response.status === 429 || response.status >= 500) && retryCount < maxRetries) {
const delay = retryDelayMs * Math.pow(2, retryCount);
console.warn(`[Gemini Service] Retrying after ${delay}ms due to status ${response.status}.`);
await new Promise(resolve => setTimeout(resolve, delay));
retryCount++;
continue;
}
const errorText = await response.text();
console.error(`[Gemini Service] Unrecoverable HTTP Error ${response.status}:`, errorText);
throw new Error(`API request failed with status ${response.status}: ${errorText.slice(0, 150)}`);
}
const data = await response.json();
// Check response validity and safety blocks (same as before)
if (!data || !data.candidates || !data.candidates[0]?.content?.parts) {
const blockReason = data?.promptFeedback?.blockReason;
if (blockReason) {
throw new Error(`ื”ื‘ืงืฉื” ื ื—ืกืžื” ืขืœ ื™ื“ื™ ืžืกื ื ื™ ื”ื‘ื˜ื™ื—ื•ืช (${blockReason}). ืฉื ื” ืืช ื”ื ื™ืกื•ื—.`);
}
const finishReason = data?.candidates?.[0]?.finishReason;
if (finishReason && finishReason !== "STOP") {
throw new Error(`ื”-AI ืขืฆืจ ืžืกื™ื‘ื” ืœื ืฆืคื•ื™ื” (${finishReason}).`);
}
throw new Error("Received invalid response structure from Gemini API.");
}
console.log("[Gemini Service] API Call Successful.");
return data;
} catch (error) {
console.error(`[Gemini Service] Fetch/Parse Error (Attempt ${retryCount + 1}):`, error);
if (retryCount >= maxRetries) { throw error; }
const delay = retryDelayMs * Math.pow(2, retryCount);
await new Promise(resolve => setTimeout(resolve, delay));
retryCount++;
}
}
throw new Error("Gemini API call failed after maximum retries.");
}
/**
* Helper to extract the JSON response content from Gemini data.
* (No changes needed in this function itself, it already handles text/JSON extraction)
* @param {object} geminiResponseData - The response data object from generateGeminiContent.
* @returns {object} - Returns the parsed JSON object.
* @throws {Error} - Throws error if parsing fails or structure is invalid.
*/
function extractGeminiResponse(geminiResponseData) {
// ... (keep the existing logic from the previous version) ...
console.log("Attempting to extract Gemini response:", JSON.stringify(geminiResponseData).substring(0, 300)); // Log snippet
const part = geminiResponseData?.candidates?.[0]?.content?.parts?.[0];
if (!part) {
const blockReason = geminiResponseData?.promptFeedback?.blockReason;
if (blockReason) { throw new Error(`ื”ืชื•ื›ืŸ ื ื—ืกื ืขืงื‘ (${blockReason}). ื ืกื” ื ื™ืกื•ื— ืื—ืจ.`); }
const finishReason = geminiResponseData?.candidates?.[0]?.finishReason;
if (finishReason && finishReason !== "STOP") { throw new Error(`ื”-AI ืขืฆืจ ืžืกื™ื‘ื” ืœื ืฆืคื•ื™ื” (${finishReason}).`); }
throw new Error("No valid content received from Gemini API.");
}
if (part.text) {
try {
const potentialJson = part.text.trim();
if(potentialJson.startsWith('{') && potentialJson.endsWith('}')) {
const parsedJson = JSON.parse(potentialJson);
console.log("[Gemini Extract] Parsed JSON successfully from text field.");
return parsedJson;
} else {
console.error("[Gemini Extract] Received non-JSON text despite requesting JSON:", potentialJson);
throw new Error("ื”-AI ืœื ื”ื—ื–ื™ืจ ืชืฉื•ื‘ื” ื‘ืคื•ืจืžื˜ JSON ืชืงื™ืŸ ื›ืฆืคื•ื™.");
}
} catch (e) {
console.error("[Gemini Extract] Error parsing JSON from text field:", e);
console.error(" --- Raw Text --- \n", part.text, "\n --- End Raw Text ---");
throw new Error(`ืฉื’ื™ืื” ื‘ืคืขื ื•ื— ื”ืชื’ื•ื‘ื” ืžื”-AI: ${e.message}`);
}
} else if (part.executableCode || part.toolCode) {
console.warn("[Gemini Extract] Received executable code, not implemented.", part);
throw new Error("ื”ืชืงื‘ืœื” ืชื’ื•ื‘ื” ืžืกื•ื’ ืงื•ื“ (ืœื ื ืชืžืš).");
} else {
console.error("[Gemini Extract] Unexpected part type in response:", part);
throw new Error("ื”ืชืงื‘ืœื” ืชื’ื•ื‘ื” ื‘ืคื•ืจืžื˜ ืœื ืฆืคื•ื™ ืžื”-AI.");
}
}