diff --git a/.env b/.env
index 6ef9630..a2b9dd7 100644
--- a/.env
+++ b/.env
@@ -8,4 +8,6 @@ MINIO_ENDPOINT=31.59.58.220:9000
MINIO_ACCESS_KEY=admin
MINIO_SECRET_KEY=SuperSecretPassword123!
MINIO_BUCKET=filam3d
-MINIO_SECURE=false
\ No newline at end of file
+MINIO_SECURE=false
+AI_PROXY_URL=http://82.22.174.14:8001
+AI_PROXY_SALT=AbVJUkwTPcUWJWhPzmjXb5p4SYyKmYC5m1uVW7Dhi7o
\ No newline at end of file
diff --git a/backend/app/config.py b/backend/app/config.py
index 6528fa5..b0e4643 100644
--- a/backend/app/config.py
+++ b/backend/app/config.py
@@ -4,6 +4,8 @@ from pydantic_settings import BaseSettings
class Settings(BaseSettings):
DATABASE_URL: str = "postgresql+asyncpg://print3d:P3D_PASSWORD@31.59.58.220:5432/print3d"
GOOGLE_API_KEY: str = ""
+ AI_PROXY_URL: str = "http://82.22.174.14:8001"
+ AI_PROXY_SALT: str = "change_me_in_production"
TELEGRAM_BOT_TOKEN: str = ""
TELEGRAM_CHAT_ID: str = ""
UPLOAD_DIR: str = "/app/uploads"
diff --git a/backend/app/routers/admin.py b/backend/app/routers/admin.py
index 44333f0..c490163 100644
--- a/backend/app/routers/admin.py
+++ b/backend/app/routers/admin.py
@@ -535,6 +535,10 @@ DEFAULT_SETTINGS = [
{"key": "painting_cost", "value": "500", "description": "Стоимость покраски (руб/шт)"},
{"key": "threading_cost", "value": "200", "description": "Стоимость нарезки резьбы (руб/шт)"},
{"key": "acetone_cost", "value": "400", "description": "Стоимость ацетоновой обработки (руб/шт)"},
+ {"key": "ai_use_proxy", "value": "true", "description": "Использовать AI-прокси (true/false)"},
+ {"key": "ai_proxy_url", "value": "", "description": "URL AI-прокси"},
+ {"key": "ai_proxy_salt", "value": "", "description": "Секретная соль для AI-прокси"},
+ {"key": "ai_direct_api_key", "value": "", "description": "Google API Key для прямого подключения"},
{"key": "company_name", "value": "Filam3D", "description": "Название компании"},
{"key": "company_phone", "value": "", "description": "Телефон компании"},
{"key": "company_email", "value": "", "description": "Email компании"},
diff --git a/backend/app/routers/calculate.py b/backend/app/routers/calculate.py
index 32fe11e..ce2a0fd 100644
--- a/backend/app/routers/calculate.py
+++ b/backend/app/routers/calculate.py
@@ -56,7 +56,7 @@ async def calculate(
logger.info("Params: material_id=%d, infill=%d%%, layer=%.2fmm, qty=%d, color='%s', multicolor=%s, post_processing='%s'",
material_id, infill_percent, layer_height_mm, quantity, color, multicolor, post_processing)
- if not 10 <= infill_percent <= 100:
+ if not 5 <= infill_percent <= 100:
logger.warning("Invalid infill_percent: %d", infill_percent)
raise HTTPException(400, "infill_percent должен быть от 10 до 100")
if not 0.08 <= layer_height_mm <= 0.4:
diff --git a/backend/app/services/ai_advisor.py b/backend/app/services/ai_advisor.py
index 3361fe1..c5f8469 100644
--- a/backend/app/services/ai_advisor.py
+++ b/backend/app/services/ai_advisor.py
@@ -1,11 +1,14 @@
+import hashlib
import json
import logging
import time
-from google import genai
-from google.genai import types
+import httpx
+from sqlalchemy import select
+from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
+from app.models.app_settings import AppSettings
logger = logging.getLogger("app.services.ai_advisor")
@@ -34,71 +37,133 @@ SYSTEM_PROMPT = """
"""
+async def _get_setting(db: AsyncSession, key: str, default: str = "") -> str:
+ result = await db.execute(select(AppSettings.value).where(AppSettings.key == key))
+ row = result.scalar_one_or_none()
+ return row if row is not None else default
+
+
+def _make_proxy_signature(salt: str) -> tuple[str, int]:
+ timestamp = int(time.time())
+ hash_input = f"{timestamp}{salt}".encode()
+ signature = hashlib.sha256(hash_input).hexdigest()
+ return signature, timestamp
+
+
+async def _call_via_proxy(proxy_url: str, proxy_salt: str, messages: list[dict]) -> str:
+ """Send request through AI proxy."""
+ signature, timestamp = _make_proxy_signature(proxy_salt)
+ headers = {
+ "x-signature": signature,
+ "x-timestamp": str(timestamp),
+ }
+ async with httpx.AsyncClient(timeout=60.0) as client:
+ resp = await client.post(
+ f"{proxy_url.rstrip('/')}/generate_text",
+ json={"messages": messages},
+ headers=headers,
+ )
+ if resp.status_code != 200:
+ logger.error("AI proxy error %d: %s", resp.status_code, resp.text[:500])
+ raise ValueError(f"AI proxy вернул ошибку: {resp.status_code}")
+
+ data = resp.json()
+ text = data.get("response", "")
+ if not text:
+ raise ValueError(f"AI proxy вернул пустой ответ (finish_reason={data.get('finish_reason')})")
+ return text
+
+
+async def _call_direct(api_key: str, system: str, user_message: str) -> str:
+ """Call Google GenAI directly via REST (no SDK dependency)."""
+ url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key={api_key}"
+ payload = {
+ "system_instruction": {"parts": [{"text": system}]},
+ "contents": [{"role": "user", "parts": [{"text": user_message}]}],
+ "generationConfig": {"temperature": 0.3, "maxOutputTokens": 1024},
+ }
+ async with httpx.AsyncClient(timeout=60.0) as client:
+ resp = await client.post(url, json=payload)
+ if resp.status_code != 200:
+ logger.error("Google API error %d: %s", resp.status_code, resp.text[:500])
+ raise ValueError(f"Google API ошибка: {resp.status_code}")
+
+ data = resp.json()
+ candidates = data.get("candidates", [])
+ if not candidates:
+ raise ValueError("Google API вернул пустой ответ")
+ parts = candidates[0].get("content", {}).get("parts", [])
+ return parts[0].get("text", "") if parts else ""
+
+
+def _parse_json_response(text: str) -> dict:
+ try:
+ return json.loads(text)
+ except json.JSONDecodeError:
+ start = text.find("{")
+ end = text.rfind("}") + 1
+ if start != -1 and end > start:
+ return json.loads(text[start:end])
+ raise ValueError("AI вернул невалидный JSON")
+
+
async def get_material_recommendation(
task_description: str,
materials_data: list[dict],
budget_preference: str = "optimal",
file_info: dict | None = None,
+ db: AsyncSession | None = None,
) -> dict:
- """Get material recommendation from Google Gemini API."""
+ """Get material recommendation — via proxy or direct, based on DB settings."""
logger.info("=== AI Advisor request ===")
logger.info("Task: %s", task_description)
- logger.info("Budget preference: %s", budget_preference)
- logger.info("File info: %s", file_info)
- logger.info("Materials count: %d", len(materials_data))
- if not settings.GOOGLE_API_KEY:
- logger.error("GOOGLE_API_KEY is not configured")
- raise ValueError("GOOGLE_API_KEY not configured")
+ # Read settings from DB, fallback to config
+ use_proxy = True
+ proxy_url = settings.AI_PROXY_URL
+ proxy_salt = settings.AI_PROXY_SALT
+ direct_api_key = settings.GOOGLE_API_KEY
+
+ if db:
+ use_proxy_str = await _get_setting(db, "ai_use_proxy", "true")
+ use_proxy = use_proxy_str.lower() in ("true", "1", "yes")
+ db_proxy_url = await _get_setting(db, "ai_proxy_url")
+ db_proxy_salt = await _get_setting(db, "ai_proxy_salt")
+ db_api_key = await _get_setting(db, "ai_direct_api_key")
+ if db_proxy_url:
+ proxy_url = db_proxy_url
+ if db_proxy_salt:
+ proxy_salt = db_proxy_salt
+ if db_api_key:
+ direct_api_key = db_api_key
materials_json = json.dumps(materials_data, ensure_ascii=False, indent=2)
system = SYSTEM_PROMPT.format(materials_json=materials_json)
- logger.debug("System prompt length: %d chars", len(system))
user_message = f"Описание задачи: {task_description}\nПредпочтение по бюджету: {budget_preference}"
if file_info:
user_message += f"\nИнформация о модели: {json.dumps(file_info, ensure_ascii=False)}"
- logger.debug("User message: %s", user_message)
- logger.info("Sending request to Gemini API (model: gemini-2.0-flash)...")
start_time = time.time()
- client = genai.Client(api_key=settings.GOOGLE_API_KEY)
- response = await client.aio.models.generate_content(
- model="gemini-3-flash-preview",
- contents=user_message,
- config=types.GenerateContentConfig(
- system_instruction=system,
- max_output_tokens=1024,
- temperature=0.3,
- ),
- )
+ if use_proxy:
+ logger.info("Mode: PROXY (%s)", proxy_url)
+ if not proxy_url:
+ raise ValueError("AI proxy URL не настроен")
+ messages = [{"role": "user", "content": system + "\n\n" + user_message}]
+ response_text = await _call_via_proxy(proxy_url, proxy_salt, messages)
+ else:
+ logger.info("Mode: DIRECT (Google API)")
+ if not direct_api_key:
+ raise ValueError("Google API Key не настроен")
+ response_text = await _call_direct(direct_api_key, system, user_message)
elapsed = time.time() - start_time
- logger.info("Gemini API responded in %.2f seconds", elapsed)
-
- response_text = response.text
+ logger.info("AI responded in %.2f seconds", elapsed)
logger.debug("Raw response (%d chars): %s", len(response_text), response_text[:500])
- try:
- result = json.loads(response_text)
- logger.info("Response parsed as JSON successfully")
- logger.info("Recommended material: id=%s, name=%s",
- result.get("recommended_material_id"), result.get("recommended_material_name"))
- logger.info("Alternatives: %d, Questions: %d",
- len(result.get("alternatives", [])), len(result.get("questions", [])))
- logger.info("=== AI Advisor complete ===")
- return result
- except json.JSONDecodeError:
- logger.warning("Direct JSON parse failed, trying to extract JSON from response...")
- start = response_text.find("{")
- end = response_text.rfind("}") + 1
- if start != -1 and end > start:
- extracted = response_text[start:end]
- logger.debug("Extracted JSON substring [%d:%d]: %s", start, end, extracted[:300])
- result = json.loads(extracted)
- logger.info("Extracted JSON parsed successfully")
- logger.info("=== AI Advisor complete ===")
- return result
- logger.error("Failed to extract JSON from AI response: %s", response_text[:200])
- raise ValueError("AI вернул невалидный JSON")
+ result = _parse_json_response(response_text)
+ logger.info("Recommended: id=%s, name=%s",
+ result.get("recommended_material_id"), result.get("recommended_material_name"))
+ logger.info("=== AI Advisor complete ===")
+ return result
diff --git a/backend/requirements.txt b/backend/requirements.txt
index 566aabc..33f7514 100644
--- a/backend/requirements.txt
+++ b/backend/requirements.txt
@@ -10,6 +10,5 @@ numpy==2.2.1
python-multipart==0.0.20
httpx==0.28.1
minio==7.2.12
-google-genai==1.14.0
pyjwt==2.10.1
bcrypt==4.2.1
diff --git a/frontend/src/components/HeroSection.vue b/frontend/src/components/HeroSection.vue
index fb3d3c8..8a7a6b9 100644
--- a/frontend/src/components/HeroSection.vue
+++ b/frontend/src/components/HeroSection.vue
@@ -5,7 +5,7 @@
3D-печать на заказ
с мгновенным расчётом
- Загрузите 3D-модель — получите точную стоимость за секунды. + Загрузите 3D-модель — получите приблизительную стоимость за секунды. 7 материалов, AI-подбор, от прототипа до серии в 500 штук.