Initial commit: clean project structure
- Backend: FastAPI + SQLAlchemy + Celery (Python 3.11+) - Frontend: Vue 3 + TypeScript + Pinia + Tailwind - Admin Frontend: separate Vue 3 app for management - Docker Compose: 9 services orchestration - Specs: design prototypes, memory system PRD, product roadmap Cleanup performed: - Removed temporary debug scripts from backend root - Removed deprecated admin_app.py (embedded UI) - Removed duplicate docs from admin-frontend - Updated .gitignore for Vite cache and egg-info
This commit is contained in:
268
backend/app/api/memories.py
Normal file
268
backend/app/api/memories.py
Normal file
@@ -0,0 +1,268 @@
|
||||
"""Memory management APIs."""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import require_user
|
||||
from app.db.database import get_db
|
||||
from app.db.models import ChildProfile, User
|
||||
from app.services import memory_service
|
||||
from app.services.memory_service import MemoryType
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class MemoryItemResponse(BaseModel):
|
||||
"""Memory item response."""
|
||||
|
||||
id: str
|
||||
type: str
|
||||
value: dict
|
||||
base_weight: float
|
||||
ttl_days: int | None
|
||||
created_at: str
|
||||
last_used_at: str | None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class MemoryListResponse(BaseModel):
|
||||
"""Memory list response."""
|
||||
|
||||
memories: list[MemoryItemResponse]
|
||||
total: int
|
||||
|
||||
|
||||
class CreateMemoryRequest(BaseModel):
|
||||
"""Create memory request."""
|
||||
|
||||
type: str = Field(..., description="记忆类型")
|
||||
value: dict = Field(..., description="记忆内容")
|
||||
universe_id: str | None = Field(default=None, description="关联的故事宇宙 ID")
|
||||
weight: float | None = Field(default=None, description="权重")
|
||||
ttl_days: int | None = Field(default=None, description="过期天数")
|
||||
|
||||
|
||||
class CreateCharacterMemoryRequest(BaseModel):
|
||||
"""Create character memory request."""
|
||||
|
||||
name: str = Field(..., description="角色名称")
|
||||
description: str | None = Field(default=None, description="角色描述")
|
||||
source_story_id: int | None = Field(default=None, description="来源故事 ID")
|
||||
affinity_score: float = Field(default=1.0, ge=0.0, le=1.0, description="喜爱程度")
|
||||
universe_id: str | None = Field(default=None, description="关联的故事宇宙 ID")
|
||||
|
||||
|
||||
class CreateScaryElementRequest(BaseModel):
|
||||
"""Create scary element memory request."""
|
||||
|
||||
keyword: str = Field(..., description="回避的关键词")
|
||||
category: str = Field(default="other", description="分类")
|
||||
source_story_id: int | None = Field(default=None, description="来源故事 ID")
|
||||
|
||||
|
||||
async def _verify_profile_ownership(
|
||||
profile_id: str, user: User, db: AsyncSession
|
||||
) -> ChildProfile:
|
||||
"""验证档案所有权。"""
|
||||
from sqlalchemy import select
|
||||
|
||||
result = await db.execute(
|
||||
select(ChildProfile).where(
|
||||
ChildProfile.id == profile_id,
|
||||
ChildProfile.user_id == user.id,
|
||||
)
|
||||
)
|
||||
profile = result.scalar_one_or_none()
|
||||
if not profile:
|
||||
raise HTTPException(status_code=404, detail="档案不存在")
|
||||
return profile
|
||||
|
||||
|
||||
@router.get("/profiles/{profile_id}/memories", response_model=MemoryListResponse)
|
||||
async def list_memories(
|
||||
profile_id: str,
|
||||
memory_type: str | None = None,
|
||||
universe_id: str | None = None,
|
||||
limit: int = 50,
|
||||
user: User = Depends(require_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""获取档案的记忆列表。"""
|
||||
await _verify_profile_ownership(profile_id, user, db)
|
||||
|
||||
memories = await memory_service.get_profile_memories(
|
||||
db=db,
|
||||
profile_id=profile_id,
|
||||
memory_type=memory_type,
|
||||
universe_id=universe_id,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
return MemoryListResponse(
|
||||
memories=[
|
||||
MemoryItemResponse(
|
||||
id=m.id,
|
||||
type=m.type,
|
||||
value=m.value,
|
||||
base_weight=m.base_weight,
|
||||
ttl_days=m.ttl_days,
|
||||
created_at=m.created_at.isoformat() if m.created_at else "",
|
||||
last_used_at=m.last_used_at.isoformat() if m.last_used_at else None,
|
||||
)
|
||||
for m in memories
|
||||
],
|
||||
total=len(memories),
|
||||
)
|
||||
|
||||
|
||||
@router.post("/profiles/{profile_id}/memories", response_model=MemoryItemResponse)
|
||||
async def create_memory(
|
||||
profile_id: str,
|
||||
payload: CreateMemoryRequest,
|
||||
user: User = Depends(require_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""创建新的记忆项。"""
|
||||
await _verify_profile_ownership(profile_id, user, db)
|
||||
|
||||
# 验证类型
|
||||
valid_types = [
|
||||
MemoryType.RECENT_STORY,
|
||||
MemoryType.FAVORITE_CHARACTER,
|
||||
MemoryType.SCARY_ELEMENT,
|
||||
MemoryType.VOCABULARY_GROWTH,
|
||||
MemoryType.EMOTIONAL_HIGHLIGHT,
|
||||
MemoryType.READING_PREFERENCE,
|
||||
MemoryType.MILESTONE,
|
||||
MemoryType.SKILL_MASTERED,
|
||||
]
|
||||
if payload.type not in valid_types:
|
||||
raise HTTPException(status_code=400, detail=f"无效的记忆类型: {payload.type}")
|
||||
|
||||
memory = await memory_service.create_memory(
|
||||
db=db,
|
||||
profile_id=profile_id,
|
||||
memory_type=payload.type,
|
||||
value=payload.value,
|
||||
universe_id=payload.universe_id,
|
||||
weight=payload.weight,
|
||||
ttl_days=payload.ttl_days,
|
||||
)
|
||||
|
||||
return MemoryItemResponse(
|
||||
id=memory.id,
|
||||
type=memory.type,
|
||||
value=memory.value,
|
||||
base_weight=memory.base_weight,
|
||||
ttl_days=memory.ttl_days,
|
||||
created_at=memory.created_at.isoformat() if memory.created_at else "",
|
||||
last_used_at=memory.last_used_at.isoformat() if memory.last_used_at else None,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/profiles/{profile_id}/memories/character", response_model=MemoryItemResponse)
|
||||
async def create_character_memory(
|
||||
profile_id: str,
|
||||
payload: CreateCharacterMemoryRequest,
|
||||
user: User = Depends(require_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""添加喜欢的角色。"""
|
||||
await _verify_profile_ownership(profile_id, user, db)
|
||||
|
||||
memory = await memory_service.create_character_memory(
|
||||
db=db,
|
||||
profile_id=profile_id,
|
||||
name=payload.name,
|
||||
description=payload.description,
|
||||
source_story_id=payload.source_story_id,
|
||||
affinity_score=payload.affinity_score,
|
||||
universe_id=payload.universe_id,
|
||||
)
|
||||
|
||||
return MemoryItemResponse(
|
||||
id=memory.id,
|
||||
type=memory.type,
|
||||
value=memory.value,
|
||||
base_weight=memory.base_weight,
|
||||
ttl_days=memory.ttl_days,
|
||||
created_at=memory.created_at.isoformat() if memory.created_at else "",
|
||||
last_used_at=memory.last_used_at.isoformat() if memory.last_used_at else None,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/profiles/{profile_id}/memories/scary", response_model=MemoryItemResponse)
|
||||
async def create_scary_element_memory(
|
||||
profile_id: str,
|
||||
payload: CreateScaryElementRequest,
|
||||
user: User = Depends(require_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""添加回避元素。"""
|
||||
await _verify_profile_ownership(profile_id, user, db)
|
||||
|
||||
memory = await memory_service.create_scary_element_memory(
|
||||
db=db,
|
||||
profile_id=profile_id,
|
||||
keyword=payload.keyword,
|
||||
category=payload.category,
|
||||
source_story_id=payload.source_story_id,
|
||||
)
|
||||
|
||||
return MemoryItemResponse(
|
||||
id=memory.id,
|
||||
type=memory.type,
|
||||
value=memory.value,
|
||||
base_weight=memory.base_weight,
|
||||
ttl_days=memory.ttl_days,
|
||||
created_at=memory.created_at.isoformat() if memory.created_at else "",
|
||||
last_used_at=memory.last_used_at.isoformat() if memory.last_used_at else None,
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/profiles/{profile_id}/memories/{memory_id}")
|
||||
async def delete_memory(
|
||||
profile_id: str,
|
||||
memory_id: str,
|
||||
user: User = Depends(require_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""删除记忆项。"""
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.db.models import MemoryItem
|
||||
|
||||
await _verify_profile_ownership(profile_id, user, db)
|
||||
|
||||
result = await db.execute(
|
||||
select(MemoryItem).where(
|
||||
MemoryItem.id == memory_id,
|
||||
MemoryItem.child_profile_id == profile_id,
|
||||
)
|
||||
)
|
||||
memory = result.scalar_one_or_none()
|
||||
|
||||
if not memory:
|
||||
raise HTTPException(status_code=404, detail="记忆不存在")
|
||||
|
||||
await db.delete(memory)
|
||||
await db.commit()
|
||||
|
||||
return {"message": "Deleted"}
|
||||
|
||||
|
||||
@router.get("/memory-types")
|
||||
async def list_memory_types():
|
||||
"""获取所有可用的记忆类型及其配置。"""
|
||||
types = []
|
||||
for type_name, config in MemoryType.CONFIG.items():
|
||||
types.append({
|
||||
"type": type_name,
|
||||
"default_weight": config[0],
|
||||
"default_ttl_days": config[1],
|
||||
"description": config[2],
|
||||
})
|
||||
return {"types": types}
|
||||
Reference in New Issue
Block a user