This commit is contained in:
commit
46af290db3
100 files changed
+16663
No files matched your search
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,428 @@
|
||||
from config.settings import settings
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from models.ai import (
|
||||
AutoUpdateBibleRequest,
|
||||
AutoUpdateBibleResponse,
|
||||
CleanupDuplicatesResponse,
|
||||
ExtractedEntities,
|
||||
ExtractEntitiesRequest,
|
||||
ExtractEntitiesResponse,
|
||||
GenerateImageRequest,
|
||||
GenerateImageResponse,
|
||||
GenerateRequest,
|
||||
GenerateResponse,
|
||||
PreviewContextRequest,
|
||||
PreviewContextResponse,
|
||||
)
|
||||
from models.bible import Character, Event, Location, StoryBible
|
||||
from models.user import User
|
||||
from services.bible_service import bible_service
|
||||
from services.context_builder import context_builder
|
||||
from services.deduplication import deduplication_service
|
||||
from services.entity_extractor import entity_extractor
|
||||
from services.image_generator import image_generator
|
||||
from services.llm_router import llm_router
|
||||
from services.model_config import model_config
|
||||
|
||||
from routers.auth import get_current_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _get_user_project(project_id: str, current_user: User) -> None:
|
||||
"""Проверить что проект принадлежит пользователю"""
|
||||
from services.project_service import project_service
|
||||
|
||||
project = project_service.get_project(project_id)
|
||||
if not project:
|
||||
raise HTTPException(status_code=404, detail="Project not found")
|
||||
if project.user_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
|
||||
@router.get("/ai/providers/status")
|
||||
async def get_providers_status():
|
||||
"""Проверить, какие провайдеры настроены и готовы к использованию"""
|
||||
return {
|
||||
"providers": settings.validate(),
|
||||
"openrouter_configured": bool(
|
||||
settings.OPENROUTER_API_KEY
|
||||
and settings.OPENROUTER_API_KEY != "your_api_key_here"
|
||||
),
|
||||
"openrouter_url": settings.OPENROUTER_BASE_URL,
|
||||
"ollama_url": settings.OLLAMA_BASE_URL,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/ai/models")
|
||||
async def get_available_models(provider: str = "ollama"):
|
||||
"""Получить список доступных моделей для указанного провайдера"""
|
||||
try:
|
||||
models = model_config.get_available_models(provider)
|
||||
default_model = model_config.get_default_model(provider)
|
||||
return {
|
||||
"provider": provider,
|
||||
"default_model": default_model,
|
||||
"models": models,
|
||||
}
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/ai/generate", response_model=GenerateResponse)
|
||||
async def generate_text(request: GenerateRequest):
|
||||
try:
|
||||
# Обогащаем промпт контекстом из Story Bible
|
||||
enriched_prompt = context_builder.enrich_prompt(
|
||||
original_prompt=request.prompt,
|
||||
selected_text=request.selected_text or "",
|
||||
bible=request.story_bible,
|
||||
)
|
||||
|
||||
# Генерируем текст с обогащенным промптом
|
||||
generated_text = await llm_router.generate(
|
||||
prompt=enriched_prompt,
|
||||
provider=request.provider,
|
||||
model=request.model,
|
||||
api_key=request.api_key,
|
||||
preset=request.preset,
|
||||
options=request.options,
|
||||
)
|
||||
return GenerateResponse(
|
||||
generated_text=generated_text,
|
||||
provider=request.provider,
|
||||
model=request.model or "default",
|
||||
preset=request.preset,
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/ai/preview-context", response_model=PreviewContextResponse)
|
||||
async def preview_context(request: PreviewContextRequest):
|
||||
"""Предпросмотр контекста, который будет добавлен к промпту"""
|
||||
mentioned = context_builder.extract_mentioned_entities(
|
||||
text=request.text,
|
||||
bible=request.story_bible,
|
||||
)
|
||||
|
||||
context_str = context_builder.build_context_string(mentioned)
|
||||
|
||||
return PreviewContextResponse(
|
||||
context_string=context_str,
|
||||
mentioned_characters=[c.name for c in mentioned["characters"]],
|
||||
mentioned_locations=[l.name for l in mentioned["locations"]],
|
||||
mentioned_events=[e.title for e in mentioned["events"]],
|
||||
)
|
||||
|
||||
|
||||
@router.post("/ai/extract-entities", response_model=ExtractEntitiesResponse)
|
||||
async def extract_entities(request: ExtractEntitiesRequest):
|
||||
"""Извлечение сущностей из текста с помощью AI"""
|
||||
try:
|
||||
# Используем OpenRouter с моделью, оптимальной для извлечения
|
||||
provider = "openrouter"
|
||||
model = "qwen/qwen-2.5-7b-instruct"
|
||||
|
||||
if (
|
||||
not settings.OPENROUTER_API_KEY
|
||||
or settings.OPENROUTER_API_KEY == "your_api_key_here"
|
||||
):
|
||||
raise ValueError(
|
||||
"OpenRouter API key not configured. "
|
||||
"Entity extraction requires a cloud model. "
|
||||
"Please set OPENROUTER_API_KEY in backend/.env"
|
||||
)
|
||||
|
||||
entities = await entity_extractor.extract_entities(
|
||||
text=request.text,
|
||||
existing_bible=request.existing_bible,
|
||||
provider=provider,
|
||||
model=model,
|
||||
)
|
||||
|
||||
return ExtractEntitiesResponse(
|
||||
entities=ExtractedEntities(**entities),
|
||||
model_used=model,
|
||||
provider=provider,
|
||||
)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/ai/auto-update-bible", response_model=AutoUpdateBibleResponse)
|
||||
async def auto_update_bible(
|
||||
request: AutoUpdateBibleRequest, current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Автоматическое обновление Story Bible при сохранении сцены"""
|
||||
if not request.auto_update_enabled:
|
||||
return AutoUpdateBibleResponse(updated=False, message="Auto-update disabled")
|
||||
|
||||
_get_user_project(request.project_id, current_user)
|
||||
|
||||
try:
|
||||
# Получаем текущую Story Bible проекта
|
||||
current_bible = bible_service.get_bible(request.project_id, current_user.id)
|
||||
|
||||
# Извлекаем сущности из текста сцены
|
||||
entities = await entity_extractor.extract_entities(
|
||||
text=request.scene_text,
|
||||
existing_bible=current_bible,
|
||||
provider="openrouter",
|
||||
model="qwen/qwen-2.5-7b-instruct",
|
||||
)
|
||||
|
||||
# Добавляем новые сущности в Bible
|
||||
new_chars_count = 0
|
||||
new_locs_count = 0
|
||||
new_events_count = 0
|
||||
|
||||
# Добавляем новых персонажей С ДЕДУПЛИКАЦИЕЙ
|
||||
for char_data in entities.get("new_characters", []):
|
||||
try:
|
||||
char_name = char_data.get("name", "")
|
||||
if not char_name:
|
||||
continue
|
||||
|
||||
# Ищем существующего персонажа с похожим именем
|
||||
existing_char = deduplication_service.find_existing_character(
|
||||
char_name, current_bible
|
||||
)
|
||||
|
||||
if existing_char:
|
||||
# Объединяем данные с существующим персонажем
|
||||
deduplication_service.merge_character_data(existing_char, char_data)
|
||||
print(f"Merged character: {char_name} -> {existing_char.name}")
|
||||
else:
|
||||
# Создаем нового персонажа
|
||||
character = Character(**char_data)
|
||||
current_bible.characters.append(character)
|
||||
new_chars_count += 1
|
||||
print(f"Added new character: {char_name}")
|
||||
except Exception as e:
|
||||
print(f"Failed to process character: {e}")
|
||||
|
||||
# Добавляем новые локации С ДЕДУПЛИКАЦИЕЙ
|
||||
for loc_data in entities.get("new_locations", []):
|
||||
try:
|
||||
loc_name = loc_data.get("name", "")
|
||||
if not loc_name:
|
||||
continue
|
||||
|
||||
existing_loc = deduplication_service.find_existing_location(
|
||||
loc_name, current_bible
|
||||
)
|
||||
|
||||
if existing_loc:
|
||||
# Обновляем описание, если новое длиннее
|
||||
new_desc = loc_data.get("description", "")
|
||||
if new_desc and len(new_desc) > len(existing_loc.description):
|
||||
existing_loc.description = new_desc
|
||||
|
||||
new_atm = loc_data.get("atmosphere", "")
|
||||
if new_atm and not existing_loc.atmosphere:
|
||||
existing_loc.atmosphere = new_atm
|
||||
|
||||
print(f"Merged location: {loc_name} -> {existing_loc.name}")
|
||||
else:
|
||||
location = Location(**loc_data)
|
||||
current_bible.locations.append(location)
|
||||
new_locs_count += 1
|
||||
print(f"Added new location: {loc_name}")
|
||||
except Exception as e:
|
||||
print(f"Failed to process location: {e}")
|
||||
|
||||
# Добавляем новые события С ДЕДУПЛИКАЦИЕЙ
|
||||
for event_data in entities.get("new_events", []):
|
||||
try:
|
||||
event_title = event_data.get("title", "")
|
||||
if not event_title:
|
||||
continue
|
||||
|
||||
existing_event = deduplication_service.find_existing_event(
|
||||
event_title, current_bible
|
||||
)
|
||||
|
||||
if existing_event:
|
||||
# Обновляем описание, если новое длиннее
|
||||
new_desc = event_data.get("description", "")
|
||||
if new_desc and len(new_desc) > len(existing_event.description):
|
||||
existing_event.description = new_desc
|
||||
print(f"Merged event: {event_title} -> {existing_event.title}")
|
||||
else:
|
||||
event = Event(**event_data)
|
||||
current_bible.events.append(event)
|
||||
new_events_count += 1
|
||||
print(f"Added new event: {event_title}")
|
||||
except Exception as e:
|
||||
print(f"Failed to process event: {e}")
|
||||
|
||||
# Обновляем черты существующих персонажей
|
||||
for trait_update in entities.get("updated_traits", []):
|
||||
char_name = trait_update.get("character_name")
|
||||
new_traits = trait_update.get("new_traits", [])
|
||||
|
||||
for char in current_bible.characters:
|
||||
if char.name.lower() == (char_name or "").lower():
|
||||
# Добавляем новые черты, которых еще нет
|
||||
for trait in new_traits:
|
||||
if trait not in char.traits:
|
||||
char.traits.append(trait)
|
||||
|
||||
# Сохраняем обновленную Bible
|
||||
if new_chars_count > 0 or new_locs_count > 0 or new_events_count > 0:
|
||||
bible_service.update_bible(
|
||||
request.project_id, current_bible, current_user.id
|
||||
)
|
||||
message = (
|
||||
f"Story Bible обновлена: +{new_chars_count} персонажей, "
|
||||
f"+{new_locs_count} локаций, +{new_events_count} событий"
|
||||
)
|
||||
else:
|
||||
message = "Новых сущностей не обнаружено"
|
||||
|
||||
return AutoUpdateBibleResponse(
|
||||
updated=True,
|
||||
new_characters=new_chars_count,
|
||||
new_locations=new_locs_count,
|
||||
new_events=new_events_count,
|
||||
message=message,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Auto-update bible error: {e}")
|
||||
return AutoUpdateBibleResponse(
|
||||
updated=False,
|
||||
message=f"Ошибка при обновлении: {str(e)}",
|
||||
)
|
||||
|
||||
|
||||
@router.post("/ai/extract-entities/test")
|
||||
async def test_extraction():
|
||||
"""Тестовый эндпоинт с примером текста"""
|
||||
test_text = """
|
||||
Алекс вошел в таверну "Серебряный дракон". Внутри пахло жареным мясом и элем.
|
||||
За стойкой стоял хозяин — крепкий мужчина по имени Гаррет с рыжей бородой.
|
||||
В углу сидела таинственная женщина в красном плаще — Алекс узнал Лиру.
|
||||
"""
|
||||
|
||||
sample_bible = StoryBible(
|
||||
characters=[
|
||||
Character(name="Алекс", description="Главный герой", role="protagonist"),
|
||||
Character(
|
||||
name="Лира", description="Таинственная проводница", role="supporting"
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
try:
|
||||
entities = await entity_extractor.extract_entities(
|
||||
text=test_text,
|
||||
existing_bible=sample_bible,
|
||||
provider="openrouter",
|
||||
model="qwen/qwen-2.5-7b-instruct",
|
||||
)
|
||||
|
||||
return {
|
||||
"test_text": test_text,
|
||||
"extracted": entities,
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post(
|
||||
"/projects/{project_id}/bible/cleanup-duplicates",
|
||||
response_model=CleanupDuplicatesResponse,
|
||||
)
|
||||
async def cleanup_duplicates(
|
||||
project_id: str, current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Очистка дубликатов в Story Bible"""
|
||||
_get_user_project(project_id, current_user)
|
||||
try:
|
||||
bible = bible_service.get_bible(project_id, current_user.id)
|
||||
|
||||
# Удаляем дубликаты персонажей
|
||||
unique_chars = []
|
||||
removed_chars = 0
|
||||
for char in bible.characters:
|
||||
is_duplicate = False
|
||||
for existing in unique_chars:
|
||||
if deduplication_service.is_similar_name(char.name, existing.name):
|
||||
# Объединяем данные
|
||||
deduplication_service.merge_character_data(
|
||||
existing, char.model_dump()
|
||||
)
|
||||
is_duplicate = True
|
||||
removed_chars += 1
|
||||
break
|
||||
if not is_duplicate:
|
||||
unique_chars.append(char)
|
||||
bible.characters = unique_chars
|
||||
|
||||
# Удаляем дубликаты локаций
|
||||
unique_locs = []
|
||||
removed_locs = 0
|
||||
for loc in bible.locations:
|
||||
is_duplicate = False
|
||||
for existing in unique_locs:
|
||||
if deduplication_service.is_similar_name(loc.name, existing.name):
|
||||
is_duplicate = True
|
||||
removed_locs += 1
|
||||
break
|
||||
if not is_duplicate:
|
||||
unique_locs.append(loc)
|
||||
bible.locations = unique_locs
|
||||
|
||||
# Удаляем дубликаты событий
|
||||
unique_events = []
|
||||
removed_events = 0
|
||||
for event in bible.events:
|
||||
is_duplicate = False
|
||||
for existing in unique_events:
|
||||
if deduplication_service.is_similar_name(event.title, existing.title):
|
||||
is_duplicate = True
|
||||
removed_events += 1
|
||||
break
|
||||
if not is_duplicate:
|
||||
unique_events.append(event)
|
||||
bible.events = unique_events
|
||||
|
||||
# Сохраняем очищенную Bible
|
||||
bible_service.update_bible(project_id, bible, current_user.id)
|
||||
|
||||
return CleanupDuplicatesResponse(
|
||||
removed_characters=removed_chars,
|
||||
removed_locations=removed_locs,
|
||||
removed_events=removed_events,
|
||||
message=f"Удалено дубликатов: {removed_chars} персонажей, {removed_locs} локаций, {removed_events} событий",
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/ai/generate-image", response_model=GenerateImageResponse)
|
||||
async def generate_image(
|
||||
request: GenerateImageRequest, current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Генерация изображения по промпту"""
|
||||
try:
|
||||
result = await image_generator.generate_image(
|
||||
prompt=request.prompt,
|
||||
project_id=request.project_id,
|
||||
user_id=current_user.id,
|
||||
document_id=request.document_id,
|
||||
document_type=request.document_type,
|
||||
model=request.model,
|
||||
size=request.size,
|
||||
)
|
||||
return result
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
@@ -0,0 +1,50 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
from jose import JWTError, jwt
|
||||
from models.user import Token, User, UserCreate, UserLogin
|
||||
from services.auth_service import ALGORITHM, SECRET_KEY, auth_service
|
||||
|
||||
router = APIRouter()
|
||||
security = HTTPBearer()
|
||||
|
||||
|
||||
@router.post("/auth/register", response_model=Token)
|
||||
async def register(user_create: UserCreate):
|
||||
try:
|
||||
user = auth_service.create_user(user_create)
|
||||
return auth_service.get_token(user.username, user_create.password)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/auth/login", response_model=Token)
|
||||
async def login(user_login: UserLogin):
|
||||
try:
|
||||
return auth_service.get_token(user_login.username, user_login.password)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=401, detail="Invalid username or password")
|
||||
|
||||
|
||||
def get_current_user(
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
||||
) -> User:
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
credentials.credentials, SECRET_KEY, algorithms=[ALGORITHM]
|
||||
)
|
||||
username: str = payload.get("sub")
|
||||
user_id: str = payload.get("user_id")
|
||||
if username is None or user_id is None:
|
||||
raise HTTPException(status_code=401, detail="Invalid token")
|
||||
|
||||
user = auth_service.get_user_by_username(username)
|
||||
if not user or user.id != user_id:
|
||||
raise HTTPException(status_code=401, detail="User not found")
|
||||
return user
|
||||
except JWTError:
|
||||
raise HTTPException(status_code=401, detail="Invalid token")
|
||||
|
||||
|
||||
@router.get("/auth/me", response_model=User)
|
||||
async def get_me(current_user: User = Depends(get_current_user)):
|
||||
return current_user
|
||||
@@ -0,0 +1,83 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from models.bible import Character, Event, Location, StoryBible, StoryThread
|
||||
from models.user import User
|
||||
from services.bible_service import bible_service
|
||||
|
||||
from routers.auth import get_current_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _get_user_project(project_id: str, current_user: User) -> None:
|
||||
"""Проверить что проект принадлежит пользователю"""
|
||||
from services.project_service import project_service
|
||||
|
||||
project = project_service.get_project(project_id)
|
||||
if not project:
|
||||
raise HTTPException(status_code=404, detail="Project not found")
|
||||
if project.user_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
|
||||
@router.get("/projects/{project_id}/bible", response_model=StoryBible)
|
||||
async def get_story_bible(
|
||||
project_id: str, current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Получить Story Bible проекта"""
|
||||
_get_user_project(project_id, current_user)
|
||||
return bible_service.get_bible(project_id, current_user.id)
|
||||
|
||||
|
||||
@router.put("/projects/{project_id}/bible", response_model=StoryBible)
|
||||
async def update_story_bible(
|
||||
project_id: str,
|
||||
bible: StoryBible,
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Обновить всю Story Bible проекта"""
|
||||
_get_user_project(project_id, current_user)
|
||||
return bible_service.update_bible(project_id, bible, current_user.id)
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/bible/characters", response_model=Character)
|
||||
async def add_character(
|
||||
project_id: str,
|
||||
character: Character,
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Добавить нового персонажа в проект"""
|
||||
_get_user_project(project_id, current_user)
|
||||
return bible_service.add_character(project_id, character, current_user.id)
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/bible/locations", response_model=Location)
|
||||
async def add_location(
|
||||
project_id: str,
|
||||
location: Location,
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Добавить новую локацию в проект"""
|
||||
_get_user_project(project_id, current_user)
|
||||
return bible_service.add_location(project_id, location, current_user.id)
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/bible/events", response_model=Event)
|
||||
async def add_event(
|
||||
project_id: str,
|
||||
event: Event,
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Добавить новое событие в проект"""
|
||||
_get_user_project(project_id, current_user)
|
||||
return bible_service.add_event(project_id, event, current_user.id)
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/bible/threads", response_model=StoryThread)
|
||||
async def add_thread(
|
||||
project_id: str,
|
||||
thread: StoryThread,
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Добавить новую сюжетную нить в проект"""
|
||||
_get_user_project(project_id, current_user)
|
||||
return bible_service.add_thread(project_id, thread, current_user.id)
|
||||
@@ -0,0 +1,151 @@
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from models.document import DocumentNode
|
||||
from models.user import User
|
||||
from pydantic import BaseModel
|
||||
from services.document_service import document_service
|
||||
|
||||
from routers.auth import get_current_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class CreateDocumentRequest(BaseModel):
|
||||
title: str
|
||||
type: str # "book", "chapter", "scene"
|
||||
parent_id: Optional[str] = None
|
||||
content: str = ""
|
||||
|
||||
|
||||
class UpdateDocumentRequest(BaseModel):
|
||||
title: Optional[str] = None
|
||||
content: Optional[str] = None
|
||||
parent_id: Optional[str] = None # Для переноса
|
||||
order: Optional[int] = None
|
||||
|
||||
|
||||
def _get_user_project(project_id: str, current_user: User) -> None:
|
||||
"""Проверить что проект принадлежит пользователю"""
|
||||
from services.project_service import project_service
|
||||
|
||||
project = project_service.get_project(project_id)
|
||||
if not project:
|
||||
raise HTTPException(status_code=404, detail="Project not found")
|
||||
if project.user_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
|
||||
@router.get(
|
||||
"/projects/{project_id}/documents/tree", response_model=List[Dict[str, Any]]
|
||||
)
|
||||
async def get_documents_tree(
|
||||
project_id: str, current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Получить документы в виде дерева"""
|
||||
_get_user_project(project_id, current_user)
|
||||
return document_service.get_documents_tree(project_id, current_user.id)
|
||||
|
||||
|
||||
@router.get("/projects/{project_id}/documents", response_model=List[DocumentNode])
|
||||
async def get_all_documents(
|
||||
project_id: str, current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Получить все документы (плоский список)"""
|
||||
_get_user_project(project_id, current_user)
|
||||
return document_service.get_all_documents(project_id, current_user.id)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/projects/{project_id}/documents/{document_id}", response_model=DocumentNode
|
||||
)
|
||||
async def get_document(
|
||||
project_id: str,
|
||||
document_id: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Получить документ по ID"""
|
||||
_get_user_project(project_id, current_user)
|
||||
document = document_service.get_document(project_id, document_id, current_user.id)
|
||||
if not document:
|
||||
raise HTTPException(status_code=404, detail="Document not found")
|
||||
return document
|
||||
|
||||
|
||||
@router.post(
|
||||
"/projects/{project_id}/documents", response_model=DocumentNode, status_code=201
|
||||
)
|
||||
async def create_document(
|
||||
project_id: str,
|
||||
request: CreateDocumentRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Создать новый документ"""
|
||||
_get_user_project(project_id, current_user)
|
||||
try:
|
||||
return document_service.create_document(
|
||||
project_id=project_id,
|
||||
title=request.title,
|
||||
doc_type=request.type,
|
||||
user_id=current_user.id,
|
||||
parent_id=request.parent_id,
|
||||
content=request.content,
|
||||
)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.put(
|
||||
"/projects/{project_id}/documents/{document_id}", response_model=DocumentNode
|
||||
)
|
||||
async def update_document(
|
||||
project_id: str,
|
||||
document_id: str,
|
||||
request: UpdateDocumentRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Обновить документ (включая перенос)"""
|
||||
_get_user_project(project_id, current_user)
|
||||
try:
|
||||
document = document_service.update_document(
|
||||
project_id=project_id,
|
||||
document_id=document_id,
|
||||
user_id=current_user.id,
|
||||
title=request.title,
|
||||
content=request.content,
|
||||
parent_id=request.parent_id,
|
||||
order=request.order,
|
||||
)
|
||||
if not document:
|
||||
raise HTTPException(status_code=404, detail="Document not found")
|
||||
return document
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.delete("/projects/{project_id}/documents/{document_id}")
|
||||
async def delete_document(
|
||||
project_id: str,
|
||||
document_id: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Удалить документ с каскадом"""
|
||||
_get_user_project(project_id, current_user)
|
||||
success = document_service.delete_document(project_id, document_id, current_user.id)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="Document not found")
|
||||
return {"status": "deleted", "document_id": document_id}
|
||||
|
||||
|
||||
@router.get(
|
||||
"/projects/{project_id}/documents/{parent_id}/children",
|
||||
response_model=List[DocumentNode],
|
||||
)
|
||||
async def get_children(
|
||||
project_id: str,
|
||||
parent_id: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Получить прямых детей узла"""
|
||||
_get_user_project(project_id, current_user)
|
||||
return document_service.get_children(project_id, parent_id, current_user.id)
|
||||
@@ -0,0 +1,64 @@
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi.responses import Response
|
||||
from models.user import User
|
||||
from pydantic import BaseModel
|
||||
from services.export_service import export_service
|
||||
|
||||
from routers.auth import get_current_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class ExportRequest(BaseModel):
|
||||
project_id: str
|
||||
document_id: Optional[str] = (
|
||||
None # ID книги или главы. Если None — экспортируем всю книгу
|
||||
)
|
||||
format: str # 'epub', 'fb2', 'pdf', 'docx'
|
||||
|
||||
|
||||
def _get_user_project(project_id: str, current_user: User) -> None:
|
||||
"""Проверить что проект принадлежит пользователю"""
|
||||
from services.project_service import project_service
|
||||
|
||||
project = project_service.get_project(project_id)
|
||||
if not project:
|
||||
raise HTTPException(status_code=404, detail="Project not found")
|
||||
if project.user_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
|
||||
@router.post("/export")
|
||||
async def export_book(
|
||||
request: ExportRequest, current_user: User = Depends(get_current_user)
|
||||
):
|
||||
_get_user_project(request.project_id, current_user)
|
||||
try:
|
||||
file_content = export_service.export_document(
|
||||
project_id=request.project_id,
|
||||
user_id=current_user.id,
|
||||
document_id=request.document_id,
|
||||
format=request.format,
|
||||
)
|
||||
|
||||
# Определяем MIME-тип
|
||||
mime_types = {
|
||||
"epub": "application/epub+zip",
|
||||
"fb2": "application/fb2",
|
||||
"pdf": "application/pdf",
|
||||
"docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
}
|
||||
|
||||
filename = f"export.{request.format}"
|
||||
|
||||
return Response(
|
||||
content=file_content,
|
||||
media_type=mime_types.get(request.format, "application/octet-stream"),
|
||||
headers={"Content-Disposition": f"attachment; filename={filename}"},
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
@@ -0,0 +1,8 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
async def health_check():
|
||||
return {"status": "ok", "message": "Writer IDE API is running"}
|
||||
@@ -0,0 +1,40 @@
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi.responses import FileResponse
|
||||
from models.user import User
|
||||
from services.project_service import project_service
|
||||
|
||||
from routers.auth import get_current_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/images/{project_id}/{filename}")
|
||||
async def get_image(
|
||||
project_id: str, filename: str, current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Отдаёт изображение из папки проекта"""
|
||||
try:
|
||||
project_dir = project_service.get_project_dir(project_id, current_user.id)
|
||||
image_path = project_dir / "images" / filename
|
||||
|
||||
if not image_path.exists():
|
||||
raise HTTPException(status_code=404, detail="Image not found")
|
||||
|
||||
# Определяем MIME тип по расширению
|
||||
suffix = Path(filename).suffix.lower()
|
||||
mime_types = {
|
||||
".png": "image/png",
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".webp": "image/webp",
|
||||
".gif": "image/gif",
|
||||
}
|
||||
|
||||
return FileResponse(
|
||||
path=image_path,
|
||||
media_type=mime_types.get(suffix, "application/octet-stream"),
|
||||
)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
@@ -0,0 +1,85 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from models.project import Project
|
||||
from models.user import User
|
||||
from pydantic import BaseModel
|
||||
from services.project_service import project_service
|
||||
|
||||
from routers.auth import get_current_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class CreateProjectRequest(BaseModel):
|
||||
title: str
|
||||
description: str = ""
|
||||
|
||||
|
||||
class UpdateProjectRequest(BaseModel):
|
||||
title: str | None = None
|
||||
description: str | None = None
|
||||
|
||||
|
||||
@router.get("/projects", response_model=list[Project])
|
||||
async def get_all_projects(current_user: User = Depends(get_current_user)):
|
||||
"""Получить все проекты текущего пользователя"""
|
||||
return project_service.get_all_projects(user_id=current_user.id)
|
||||
|
||||
|
||||
@router.get("/projects/{project_id}", response_model=Project)
|
||||
async def get_project(project_id: str, current_user: User = Depends(get_current_user)):
|
||||
"""Получить проект по ID"""
|
||||
project = project_service.get_project(project_id)
|
||||
if not project:
|
||||
raise HTTPException(status_code=404, detail="Project not found")
|
||||
if project.user_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
return project
|
||||
|
||||
|
||||
@router.post("/projects", response_model=Project, status_code=201)
|
||||
async def create_project(
|
||||
request: CreateProjectRequest, current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Создать новый проект"""
|
||||
return project_service.create_project(
|
||||
title=request.title,
|
||||
user_id=current_user.id,
|
||||
description=request.description,
|
||||
)
|
||||
|
||||
|
||||
@router.put("/projects/{project_id}", response_model=Project)
|
||||
async def update_project(
|
||||
project_id: str,
|
||||
request: UpdateProjectRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Обновить проект"""
|
||||
project = project_service.get_project(project_id)
|
||||
if not project:
|
||||
raise HTTPException(status_code=404, detail="Project not found")
|
||||
if project.user_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
updated = project_service.update_project(
|
||||
project_id=project_id,
|
||||
title=request.title,
|
||||
description=request.description,
|
||||
)
|
||||
if not updated:
|
||||
raise HTTPException(status_code=404, detail="Project not found")
|
||||
return updated
|
||||
|
||||
|
||||
@router.delete("/projects/{project_id}")
|
||||
async def delete_project(
|
||||
project_id: str, current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Удалить проект"""
|
||||
try:
|
||||
success = project_service.delete_project(project_id, current_user.id)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="Project not found")
|
||||
return {"status": "deleted", "project_id": project_id}
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=403, detail=str(e))
|
||||
Reference in new issue
Block a user