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:
429
.claude/specs/memory-intelligence/CHILD-PROFILE-MODEL.md
Normal file
429
.claude/specs/memory-intelligence/CHILD-PROFILE-MODEL.md
Normal file
@@ -0,0 +1,429 @@
|
||||
# 孩子档案数据模型
|
||||
|
||||
## 概述
|
||||
|
||||
孩子档案是记忆智能系统的核心,存储孩子的基础信息、兴趣偏好和阅读行为数据。
|
||||
|
||||
---
|
||||
|
||||
## 一、数据库模型
|
||||
|
||||
### 1.1 主表: child_profiles
|
||||
|
||||
```sql
|
||||
CREATE TABLE child_profiles (
|
||||
-- 主键
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
-- 外键: 所属用户
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
|
||||
-- 基础信息
|
||||
name VARCHAR(50) NOT NULL,
|
||||
avatar_url VARCHAR(500),
|
||||
birth_date DATE,
|
||||
gender VARCHAR(10) CHECK (gender IN ('male', 'female', 'other')),
|
||||
|
||||
-- 显式偏好 (家长填写)
|
||||
interests JSONB DEFAULT '[]',
|
||||
-- 示例: ["恐龙", "太空", "公主", "动物"]
|
||||
|
||||
growth_themes JSONB DEFAULT '[]',
|
||||
-- 示例: ["勇气", "分享"]
|
||||
|
||||
-- 隐式偏好 (系统学习)
|
||||
reading_preferences JSONB DEFAULT '{}',
|
||||
-- 示例: {
|
||||
-- "preferred_length": "medium", -- short/medium/long
|
||||
-- "preferred_style": "adventure", -- adventure/fairy/educational
|
||||
-- "tag_weights": {"恐龙": 5, "公主": 2, "太空": 3}
|
||||
-- }
|
||||
|
||||
-- 统计数据
|
||||
stories_count INTEGER DEFAULT 0,
|
||||
total_reading_time INTEGER DEFAULT 0, -- 秒
|
||||
|
||||
-- 时间戳
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
|
||||
-- 约束
|
||||
CONSTRAINT unique_child_per_user UNIQUE (user_id, name)
|
||||
);
|
||||
|
||||
-- 索引
|
||||
CREATE INDEX idx_child_profiles_user_id ON child_profiles(user_id);
|
||||
```
|
||||
|
||||
### 1.2 兴趣标签枚举
|
||||
|
||||
预定义的兴趣标签,前端展示用:
|
||||
|
||||
```python
|
||||
INTEREST_TAGS = {
|
||||
"animals": {
|
||||
"zh": "动物",
|
||||
"icon": "🐾",
|
||||
"subtags": ["恐龙", "猫咪", "狗狗", "兔子", "海洋动物"]
|
||||
},
|
||||
"fantasy": {
|
||||
"zh": "奇幻",
|
||||
"icon": "✨",
|
||||
"subtags": ["公主", "王子", "魔法", "精灵", "龙"]
|
||||
},
|
||||
"adventure": {
|
||||
"zh": "冒险",
|
||||
"icon": "🗺️",
|
||||
"subtags": ["太空", "海盗", "探险", "寻宝"]
|
||||
},
|
||||
"vehicles": {
|
||||
"zh": "交通工具",
|
||||
"icon": "🚗",
|
||||
"subtags": ["汽车", "火车", "飞机", "火箭"]
|
||||
},
|
||||
"nature": {
|
||||
"zh": "自然",
|
||||
"icon": "🌳",
|
||||
"subtags": ["森林", "海洋", "山川", "四季"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 1.3 成长主题枚举
|
||||
|
||||
```python
|
||||
GROWTH_THEMES = [
|
||||
{"key": "courage", "zh": "勇气", "description": "克服恐惧,勇敢面对"},
|
||||
{"key": "sharing", "zh": "分享", "description": "学会与他人分享"},
|
||||
{"key": "friendship", "zh": "友谊", "description": "交朋友,珍惜友情"},
|
||||
{"key": "honesty", "zh": "诚实", "description": "说真话,不撒谎"},
|
||||
{"key": "independence", "zh": "独立", "description": "自己的事情自己做"},
|
||||
{"key": "kindness", "zh": "善良", "description": "帮助他人,关爱弱小"},
|
||||
{"key": "patience", "zh": "耐心", "description": "学会等待,不急躁"},
|
||||
{"key": "curiosity", "zh": "好奇", "description": "探索未知,爱问为什么"}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、SQLAlchemy 模型
|
||||
|
||||
```python
|
||||
# backend/app/db/models.py
|
||||
|
||||
from sqlalchemy import Column, String, Date, Integer, ForeignKey, JSON
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import relationship
|
||||
import uuid
|
||||
|
||||
class ChildProfile(Base):
|
||||
__tablename__ = "child_profiles"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
|
||||
|
||||
# 基础信息
|
||||
name = Column(String(50), nullable=False)
|
||||
avatar_url = Column(String(500))
|
||||
birth_date = Column(Date)
|
||||
gender = Column(String(10))
|
||||
|
||||
# 偏好
|
||||
interests = Column(JSON, default=list)
|
||||
growth_themes = Column(JSON, default=list)
|
||||
reading_preferences = Column(JSON, default=dict)
|
||||
|
||||
# 统计
|
||||
stories_count = Column(Integer, default=0)
|
||||
total_reading_time = Column(Integer, default=0)
|
||||
|
||||
# 时间戳
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
|
||||
# 关系
|
||||
user = relationship("User", back_populates="child_profiles")
|
||||
story_universes = relationship("StoryUniverse", back_populates="child_profile", cascade="all, delete-orphan")
|
||||
|
||||
@property
|
||||
def age(self) -> int | None:
|
||||
"""计算年龄"""
|
||||
if not self.birth_date:
|
||||
return None
|
||||
today = date.today()
|
||||
return today.year - self.birth_date.year - (
|
||||
(today.month, today.day) < (self.birth_date.month, self.birth_date.day)
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、Pydantic Schema
|
||||
|
||||
```python
|
||||
# backend/app/schemas/child_profile.py
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from datetime import date
|
||||
from uuid import UUID
|
||||
|
||||
class ChildProfileCreate(BaseModel):
|
||||
name: str = Field(..., min_length=1, max_length=50)
|
||||
birth_date: date | None = None
|
||||
gender: str | None = Field(None, pattern="^(male|female|other)$")
|
||||
interests: list[str] = Field(default_factory=list)
|
||||
growth_themes: list[str] = Field(default_factory=list)
|
||||
|
||||
class ChildProfileUpdate(BaseModel):
|
||||
name: str | None = Field(None, min_length=1, max_length=50)
|
||||
birth_date: date | None = None
|
||||
gender: str | None = Field(None, pattern="^(male|female|other)$")
|
||||
interests: list[str] | None = None
|
||||
growth_themes: list[str] | None = None
|
||||
avatar_url: str | None = None
|
||||
|
||||
class ChildProfileResponse(BaseModel):
|
||||
id: UUID
|
||||
name: str
|
||||
avatar_url: str | None
|
||||
birth_date: date | None
|
||||
gender: str | None
|
||||
age: int | None
|
||||
interests: list[str]
|
||||
growth_themes: list[str]
|
||||
stories_count: int
|
||||
total_reading_time: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class ChildProfileListResponse(BaseModel):
|
||||
profiles: list[ChildProfileResponse]
|
||||
total: int
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、API 实现
|
||||
|
||||
```python
|
||||
# backend/app/api/profiles.py
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from uuid import UUID
|
||||
|
||||
router = APIRouter(prefix="/api/profiles", tags=["profiles"])
|
||||
|
||||
@router.get("", response_model=ChildProfileListResponse)
|
||||
async def list_profiles(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""获取当前用户的所有孩子档案"""
|
||||
profiles = await db.execute(
|
||||
select(ChildProfile)
|
||||
.where(ChildProfile.user_id == current_user.id)
|
||||
.order_by(ChildProfile.created_at)
|
||||
)
|
||||
profiles = profiles.scalars().all()
|
||||
return ChildProfileListResponse(profiles=profiles, total=len(profiles))
|
||||
|
||||
@router.post("", response_model=ChildProfileResponse, status_code=201)
|
||||
async def create_profile(
|
||||
data: ChildProfileCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""创建孩子档案"""
|
||||
# 检查是否超过限制 (每用户最多5个孩子档案)
|
||||
count = await db.scalar(
|
||||
select(func.count(ChildProfile.id))
|
||||
.where(ChildProfile.user_id == current_user.id)
|
||||
)
|
||||
if count >= 5:
|
||||
raise HTTPException(400, "最多只能创建5个孩子档案")
|
||||
|
||||
profile = ChildProfile(user_id=current_user.id, **data.model_dump())
|
||||
db.add(profile)
|
||||
await db.commit()
|
||||
await db.refresh(profile)
|
||||
return profile
|
||||
|
||||
@router.get("/{profile_id}", response_model=ChildProfileResponse)
|
||||
async def get_profile(
|
||||
profile_id: UUID,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""获取单个孩子档案"""
|
||||
profile = await db.get(ChildProfile, profile_id)
|
||||
if not profile or profile.user_id != current_user.id:
|
||||
raise HTTPException(404, "档案不存在")
|
||||
return profile
|
||||
|
||||
@router.put("/{profile_id}", response_model=ChildProfileResponse)
|
||||
async def update_profile(
|
||||
profile_id: UUID,
|
||||
data: ChildProfileUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""更新孩子档案"""
|
||||
profile = await db.get(ChildProfile, profile_id)
|
||||
if not profile or profile.user_id != current_user.id:
|
||||
raise HTTPException(404, "档案不存在")
|
||||
|
||||
for key, value in data.model_dump(exclude_unset=True).items():
|
||||
setattr(profile, key, value)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(profile)
|
||||
return profile
|
||||
|
||||
@router.delete("/{profile_id}", status_code=204)
|
||||
async def delete_profile(
|
||||
profile_id: UUID,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""删除孩子档案"""
|
||||
profile = await db.get(ChildProfile, profile_id)
|
||||
if not profile or profile.user_id != current_user.id:
|
||||
raise HTTPException(404, "档案不存在")
|
||||
|
||||
await db.delete(profile)
|
||||
await db.commit()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、隐式偏好学习
|
||||
|
||||
### 5.1 行为事件
|
||||
|
||||
```python
|
||||
class ReadingEvent(BaseModel):
|
||||
"""阅读行为事件"""
|
||||
profile_id: UUID
|
||||
story_id: UUID
|
||||
event_type: Literal["started", "completed", "skipped", "replayed"]
|
||||
reading_time: int # 秒
|
||||
timestamp: datetime
|
||||
```
|
||||
|
||||
### 5.2 偏好更新算法
|
||||
|
||||
```python
|
||||
async def update_reading_preferences(
|
||||
db: AsyncSession,
|
||||
profile_id: UUID,
|
||||
story: Story,
|
||||
event: ReadingEvent
|
||||
):
|
||||
"""根据阅读行为更新隐式偏好"""
|
||||
profile = await db.get(ChildProfile, profile_id)
|
||||
prefs = profile.reading_preferences or {}
|
||||
tag_weights = prefs.get("tag_weights", {})
|
||||
|
||||
# 权重调整
|
||||
weight_delta = {
|
||||
"completed": 1.0, # 完整阅读,正向
|
||||
"replayed": 1.5, # 重复播放,强正向
|
||||
"skipped": -0.5, # 跳过,负向
|
||||
"started": 0.1 # 开始阅读,弱正向
|
||||
}
|
||||
|
||||
delta = weight_delta.get(event.event_type, 0)
|
||||
|
||||
for tag in story.tags:
|
||||
current = tag_weights.get(tag, 0)
|
||||
tag_weights[tag] = max(0, current + delta) # 不低于0
|
||||
|
||||
# 更新阅读长度偏好
|
||||
if event.event_type == "completed":
|
||||
word_count = len(story.content)
|
||||
if word_count < 300:
|
||||
length_pref = "short"
|
||||
elif word_count < 600:
|
||||
length_pref = "medium"
|
||||
else:
|
||||
length_pref = "long"
|
||||
|
||||
# 简单的移动平均
|
||||
prefs["preferred_length"] = length_pref
|
||||
|
||||
prefs["tag_weights"] = tag_weights
|
||||
profile.reading_preferences = prefs
|
||||
await db.commit()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、数据迁移
|
||||
|
||||
```python
|
||||
# backend/alembic/versions/xxx_add_child_profiles.py
|
||||
|
||||
def upgrade():
|
||||
op.create_table(
|
||||
'child_profiles',
|
||||
sa.Column('id', sa.UUID(), nullable=False),
|
||||
sa.Column('user_id', sa.UUID(), nullable=False),
|
||||
sa.Column('name', sa.String(50), nullable=False),
|
||||
sa.Column('avatar_url', sa.String(500)),
|
||||
sa.Column('birth_date', sa.Date()),
|
||||
sa.Column('gender', sa.String(10)),
|
||||
sa.Column('interests', sa.JSON(), server_default='[]'),
|
||||
sa.Column('growth_themes', sa.JSON(), server_default='[]'),
|
||||
sa.Column('reading_preferences', sa.JSON(), server_default='{}'),
|
||||
sa.Column('stories_count', sa.Integer(), server_default='0'),
|
||||
sa.Column('total_reading_time', sa.Integer(), server_default='0'),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index('idx_child_profiles_user_id', 'child_profiles', ['user_id'])
|
||||
|
||||
def downgrade():
|
||||
op.drop_index('idx_child_profiles_user_id')
|
||||
op.drop_table('child_profiles')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、隐私与安全
|
||||
|
||||
### 7.1 数据加密
|
||||
|
||||
敏感字段(姓名、出生日期)在存储时加密:
|
||||
|
||||
```python
|
||||
from cryptography.fernet import Fernet
|
||||
|
||||
class EncryptedChildProfile:
|
||||
"""加密存储的孩子档案"""
|
||||
|
||||
@staticmethod
|
||||
def encrypt_name(name: str, key: bytes) -> str:
|
||||
f = Fernet(key)
|
||||
return f.encrypt(name.encode()).decode()
|
||||
|
||||
@staticmethod
|
||||
def decrypt_name(encrypted: str, key: bytes) -> str:
|
||||
f = Fernet(key)
|
||||
return f.decrypt(encrypted.encode()).decode()
|
||||
```
|
||||
|
||||
### 7.2 访问控制
|
||||
|
||||
- 孩子档案只能被创建者访问
|
||||
- 删除用户时级联删除所有孩子档案
|
||||
- API 层强制校验 `user_id` 归属
|
||||
|
||||
### 7.3 数据保留
|
||||
|
||||
- 用户可随时删除孩子档案
|
||||
- 删除后 30 天内可恢复(软删除)
|
||||
- 30 天后永久删除
|
||||
455
.claude/specs/memory-intelligence/MEMORY-INTELLIGENCE-PRD.md
Normal file
455
.claude/specs/memory-intelligence/MEMORY-INTELLIGENCE-PRD.md
Normal file
@@ -0,0 +1,455 @@
|
||||
# 记忆智能系统 PRD
|
||||
|
||||
## 概述
|
||||
|
||||
**功能名称**: 记忆智能 (Memory Intelligence)
|
||||
**版本**: v1.0
|
||||
**优先级**: Phase 2.5 (体验增强后、社区化前)
|
||||
**目标用户**: 家长 + 3-8 岁儿童
|
||||
|
||||
### 核心价值
|
||||
|
||||
让 DreamWeaver 从"故事生成工具"进化为"懂孩子的故事伙伴":
|
||||
- **记住孩子**: 偏好、成长阶段、兴趣变化
|
||||
- **延续故事**: 角色、世界观跨故事延续
|
||||
- **主动关怀**: 适时推送个性化故事建议
|
||||
|
||||
---
|
||||
|
||||
## 一、功能模块
|
||||
|
||||
### 1.1 孩子档案系统 (Child Profile)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| 基础信息 | 显式 | 姓名、年龄、性别 |
|
||||
| 兴趣标签 | 显式+隐式 | 恐龙、公主、太空、动物等 |
|
||||
| 成长主题 | 显式 | 当前关注:勇气/分享/独立等 |
|
||||
| 阅读偏好 | 隐式 | 故事长度、风格、复杂度 |
|
||||
| 互动历史 | 隐式 | 喜欢的故事、跳过的故事 |
|
||||
|
||||
**数据来源**:
|
||||
- 显式: 家长主动填写
|
||||
- 隐式: 系统从使用行为中学习
|
||||
|
||||
### 1.2 故事宇宙记忆 (Story Universe)
|
||||
|
||||
跨故事保持连续性的元素:
|
||||
|
||||
| 元素 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| 主角设定 | 孩子的故事化身 | "小明是个爱冒险的男孩" |
|
||||
| 常驻角色 | 反复出现的配角 | 魔法猫咪"星星"、智慧老树 |
|
||||
| 世界观 | 故事发生的宇宙 | 梦幻森林、星际学院 |
|
||||
| 成就系统 | 主角的成长轨迹 | "学会了勇敢"、"交到新朋友" |
|
||||
|
||||
**记忆结构字段**:
|
||||
- `protagonist` / `recurring_characters` / `world_settings` / `achievements`(JSON 结构)
|
||||
- “延续上一个故事”默认选最近更新的宇宙(按 `updated_at` 倒序)
|
||||
|
||||
### 1.3 主动推送系统 (Proactive Push)
|
||||
|
||||
| 触发类型 | 条件 | 推送内容 |
|
||||
|----------|------|----------|
|
||||
| 时间触发 | 睡前时段 (19:00-21:00) | "今晚想听什么故事?" |
|
||||
| 事件触发 | 节日/生日 | 主题故事推荐 |
|
||||
| 行为触发 | 3天未使用 | 召回提醒 |
|
||||
| 成长触发 | 年龄变化 | 难度升级建议 |
|
||||
|
||||
**优先级与抑制**:
|
||||
- 优先级:事件 > 成长 > 行为 > 时间
|
||||
- 抑制:当天已推送不再触发;静默时段(21:00-09:00)延迟;用户关闭推送则不触发
|
||||
|
||||
---
|
||||
|
||||
## 二、用户故事
|
||||
|
||||
### US-1: 创建孩子档案
|
||||
```
|
||||
作为家长
|
||||
我想要创建孩子的专属档案
|
||||
以便系统生成更适合孩子的故事
|
||||
```
|
||||
|
||||
**验收标准**:
|
||||
- [ ] 可填写孩子基础信息(姓名、年龄、性别)
|
||||
- [ ] 可选择兴趣标签(多选)
|
||||
- [ ] 可设置当前成长主题
|
||||
- [ ] 支持多个孩子档案切换
|
||||
|
||||
### US-2: 故事角色延续
|
||||
```
|
||||
作为家长
|
||||
我想要故事中的角色能在新故事中再次出现
|
||||
以便孩子感受到故事的连续性
|
||||
```
|
||||
|
||||
**验收标准**:
|
||||
- [ ] 生成故事时可选择"延续上一个故事"
|
||||
- [ ] 系统自动带入主角设定和常驻角色
|
||||
- [ ] 新故事引用之前的"成就"
|
||||
|
||||
### US-3: 睡前故事提醒
|
||||
```
|
||||
作为家长
|
||||
我想要在睡前时段收到故事推荐
|
||||
以便养成固定的亲子阅读习惯
|
||||
```
|
||||
|
||||
**验收标准**:
|
||||
- [ ] 可设置提醒时间
|
||||
- [ ] 推送包含个性化故事建议
|
||||
- [ ] 可一键进入故事生成
|
||||
|
||||
---
|
||||
|
||||
## 三、数据模型
|
||||
|
||||
### 3.1 孩子档案表 (child_profiles)
|
||||
|
||||
```sql
|
||||
CREATE TABLE child_profiles (
|
||||
id UUID PRIMARY KEY,
|
||||
user_id UUID REFERENCES users(id),
|
||||
name VARCHAR(50) NOT NULL,
|
||||
birth_date DATE,
|
||||
gender VARCHAR(10),
|
||||
interests JSONB DEFAULT '[]',
|
||||
growth_themes JSONB DEFAULT '[]',
|
||||
reading_preferences JSONB DEFAULT '{}',
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
### 3.2 故事宇宙表 (story_universes)
|
||||
|
||||
```sql
|
||||
CREATE TABLE story_universes (
|
||||
id UUID PRIMARY KEY,
|
||||
child_profile_id UUID REFERENCES child_profiles(id),
|
||||
name VARCHAR(100) NOT NULL,
|
||||
protagonist JSONB NOT NULL,
|
||||
recurring_characters JSONB DEFAULT '[]',
|
||||
world_settings JSONB DEFAULT '{}',
|
||||
achievements JSONB DEFAULT '[]',
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
### 3.3 推送配置表 (push_configs)
|
||||
|
||||
```sql
|
||||
CREATE TABLE push_configs (
|
||||
id UUID PRIMARY KEY,
|
||||
user_id UUID REFERENCES users(id),
|
||||
child_profile_id UUID REFERENCES child_profiles(id),
|
||||
push_time TIME,
|
||||
push_days INTEGER[], -- 0-6 表示周日到周六
|
||||
enabled BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
### 3.4 推送事件表 (push_events)
|
||||
|
||||
```sql
|
||||
CREATE TABLE push_events (
|
||||
id UUID PRIMARY KEY,
|
||||
user_id UUID NOT NULL,
|
||||
child_profile_id UUID NOT NULL,
|
||||
trigger_type VARCHAR(20) NOT NULL, -- time/event/behavior/growth
|
||||
sent_at TIMESTAMP NOT NULL,
|
||||
status VARCHAR(20) NOT NULL, -- sent/failed/suppressed
|
||||
reason TEXT
|
||||
);
|
||||
```
|
||||
|
||||
### 3.5 记忆条目表 (memory_items)
|
||||
|
||||
用于存储“可解释、可控”的记忆条目(兴趣偏好、成长主题、常驻角色、关键事件等),并支持时序衰减。
|
||||
|
||||
```sql
|
||||
CREATE TABLE memory_items (
|
||||
id UUID PRIMARY KEY,
|
||||
child_profile_id UUID NOT NULL,
|
||||
universe_id UUID,
|
||||
type VARCHAR(50) NOT NULL, -- interest/growth/character/event等
|
||||
value JSONB NOT NULL, -- 结构化内容
|
||||
base_weight FLOAT DEFAULT 1.0, -- 初始权重
|
||||
last_used_at TIMESTAMP, -- 最近使用时间
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
ttl_days INTEGER -- 可选:过期天数
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、API 设计
|
||||
|
||||
### 4.1 孩子档案 API
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/api/profiles` | 获取当前用户的所有孩子档案 |
|
||||
| POST | `/api/profiles` | 创建孩子档案 |
|
||||
| GET | `/api/profiles/{id}` | 获取单个档案详情 |
|
||||
| PUT | `/api/profiles/{id}` | 更新档案 |
|
||||
| DELETE | `/api/profiles/{id}` | 删除档案 |
|
||||
|
||||
### 4.2 故事宇宙 API
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/api/profiles/{id}/universes` | 获取孩子的故事宇宙列表 |
|
||||
| POST | `/api/profiles/{id}/universes` | 创建新宇宙 |
|
||||
| GET | `/api/universes/{id}` | 获取宇宙详情 |
|
||||
| PUT | `/api/universes/{id}` | 更新宇宙设定 |
|
||||
| POST | `/api/universes/{id}/achievements` | 添加成就 |
|
||||
|
||||
### 4.3 推送配置 API
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/api/push-configs` | 获取推送配置 |
|
||||
| PUT | `/api/push-configs` | 更新推送配置 |
|
||||
|
||||
---
|
||||
|
||||
## 五、Prompt 工程
|
||||
|
||||
### 5.1 带记忆的故事生成 Prompt
|
||||
|
||||
```
|
||||
你是一个专业的儿童故事作家。请为以下孩子创作一个故事:
|
||||
|
||||
【孩子档案】
|
||||
- 姓名: {child_name}
|
||||
- 年龄: {age}岁
|
||||
- 兴趣: {interests}
|
||||
- 当前成长主题: {growth_theme}
|
||||
|
||||
【故事宇宙】
|
||||
- 主角设定: {protagonist}
|
||||
- 常驻角色: {recurring_characters}
|
||||
- 世界观: {world_settings}
|
||||
- 已获成就: {achievements}
|
||||
|
||||
【本次创作要求】
|
||||
- 关键词: {keywords}
|
||||
- 延续之前的故事世界观
|
||||
- 让主角在故事中有新的成长
|
||||
|
||||
请创作一个适合{age}岁儿童的故事,约{word_count}字。
|
||||
```
|
||||
|
||||
### 5.2 成就提取 Prompt
|
||||
|
||||
```
|
||||
请分析以下故事,提取主角获得的成长/成就:
|
||||
|
||||
【故事内容】
|
||||
{story_content}
|
||||
|
||||
请以JSON格式返回:
|
||||
{
|
||||
"achievements": [
|
||||
{"type": "勇气", "description": "克服了对黑暗的恐惧"},
|
||||
{"type": "友谊", "description": "帮助了迷路的小兔子"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、前端设计
|
||||
|
||||
### 6.1 孩子档案页面
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ 我的宝贝 [+添加] │
|
||||
├─────────────────────────────────────┤
|
||||
│ ┌─────┐ ┌─────┐ ┌─────┐ │
|
||||
│ │ 👦 │ │ 👧 │ │ + │ │
|
||||
│ │小明 │ │小红 │ │添加 │ │
|
||||
│ │5岁 │ │3岁 │ │ │ │
|
||||
│ └─────┘ └─────┘ └─────┘ │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 6.2 档案详情页
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ ← 小明的档案 [编辑] │
|
||||
├─────────────────────────────────────┤
|
||||
│ 基础信息 │
|
||||
│ 姓名: 小明 年龄: 5岁 性别: 男 │
|
||||
├─────────────────────────────────────┤
|
||||
│ 兴趣爱好 │
|
||||
│ [恐龙] [太空] [机器人] │
|
||||
├─────────────────────────────────────┤
|
||||
│ 成长主题 │
|
||||
│ ○ 勇气 ● 分享 ○ 独立 ○ 友谊 │
|
||||
├─────────────────────────────────────┤
|
||||
│ 故事宇宙 │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ 🌟 星际冒险 │ │
|
||||
│ │ 主角: 小明船长 │ │
|
||||
│ │ 伙伴: 机器人小七、外星猫咪 │ │
|
||||
│ │ 成就: 3个 │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 6.3 故事生成时选择档案
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ 为谁创作故事? │
|
||||
├─────────────────────────────────────┤
|
||||
│ ● 小明 (5岁) │
|
||||
│ ○ 小红 (3岁) │
|
||||
│ ○ 不使用档案 │
|
||||
├─────────────────────────────────────┤
|
||||
│ 选择故事宇宙 │
|
||||
│ ● 星际冒险 (延续上次) │
|
||||
│ ○ 创建新宇宙 │
|
||||
├─────────────────────────────────────┤
|
||||
│ [下一步: 输入关键词] │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、技术实现要点
|
||||
|
||||
### 7.1 隐式偏好学习
|
||||
|
||||
```python
|
||||
# 基于用户行为更新偏好
|
||||
async def update_implicit_preferences(
|
||||
child_id: UUID,
|
||||
story: Story,
|
||||
interaction: Interaction # 完整阅读/跳过/重复播放
|
||||
):
|
||||
profile = await get_child_profile(child_id)
|
||||
|
||||
if interaction == "completed":
|
||||
# 增加相关标签权重
|
||||
for tag in story.tags:
|
||||
profile.reading_preferences[tag] = \
|
||||
profile.reading_preferences.get(tag, 0) + 1
|
||||
elif interaction == "skipped":
|
||||
# 降低相关标签权重
|
||||
for tag in story.tags:
|
||||
profile.reading_preferences[tag] = \
|
||||
profile.reading_preferences.get(tag, 0) - 0.5
|
||||
```
|
||||
|
||||
### 7.2 成就自动提取
|
||||
|
||||
故事生成完成后,异步调用 LLM 提取成就(以 `type + description` 去重):
|
||||
|
||||
```python
|
||||
@celery.task
|
||||
async def extract_achievements(story_id: UUID, universe_id: UUID):
|
||||
story = await get_story(story_id)
|
||||
universe = await get_universe(universe_id)
|
||||
|
||||
achievements = await llm.extract_achievements(story.content)
|
||||
|
||||
universe.achievements.extend(achievements)
|
||||
await save_universe(universe)
|
||||
```
|
||||
|
||||
### 7.3 推送调度
|
||||
|
||||
使用 Celery Beat 定时检查推送:
|
||||
|
||||
```python
|
||||
@celery.task
|
||||
def check_push_notifications():
|
||||
current_time = datetime.now().time()
|
||||
current_day = datetime.now().weekday()
|
||||
|
||||
configs = PushConfig.query.filter(
|
||||
PushConfig.enabled == True,
|
||||
PushConfig.push_time <= current_time,
|
||||
current_day.in_(PushConfig.push_days)
|
||||
).all()
|
||||
|
||||
for config in configs:
|
||||
send_push_notification.delay(config.user_id, config.child_profile_id)
|
||||
```
|
||||
|
||||
**执行约束**:
|
||||
- 同一孩子每天最多 1 次推送
|
||||
- 推送前查询 `push_events` 去重,成功/抑制均需记录
|
||||
|
||||
### 7.4 时序衰减与记忆评分
|
||||
|
||||
**目标**:让“越新的记忆影响越大”,避免旧偏好长期干扰。
|
||||
|
||||
**默认实现(推荐)**:查询时动态计算分数,不直接修改数据库。
|
||||
- 记忆分数:`score = base_weight × decay(Δt)`
|
||||
- 衰减示例(分段):0-7 天 1.0,8-30 天 0.7,31-90 天 0.4,90 天后 0.2
|
||||
- 读取时按 `score` 排序,选 Top N 进入 Prompt
|
||||
|
||||
**可选实现**:定期批处理降权
|
||||
- 每日/每周批量更新 `base_weight`
|
||||
- 适合数据量大、读多写少的场景
|
||||
|
||||
**RAG 场景的衰减用法**:
|
||||
- 语义相似度分数 × 时间衰减
|
||||
- 可加时间窗口过滤(如仅取最近 90 天)
|
||||
|
||||
**删除策略(默认不删)**:
|
||||
- 默认只降权,不主动删除
|
||||
- 可选:对低权重且 180 天未使用的条目执行 TTL 清理
|
||||
|
||||
---
|
||||
|
||||
## 八、里程碑
|
||||
|
||||
### M1: 孩子档案基础
|
||||
- [ ] 数据库模型
|
||||
- [ ] CRUD API
|
||||
- [ ] 前端档案管理页面
|
||||
- [ ] 故事生成时选择档案
|
||||
|
||||
### M2: 故事宇宙
|
||||
- [ ] 宇宙数据模型
|
||||
- [ ] Prompt 集成
|
||||
- [ ] 成就自动提取
|
||||
- [ ] 前端宇宙管理
|
||||
|
||||
### M3: 主动推送
|
||||
- [ ] 推送配置 API
|
||||
- [ ] Celery Beat 调度
|
||||
- [ ] 推送通知集成 (Web Push / 微信)
|
||||
|
||||
### M4: 隐式学习
|
||||
- [ ] 行为埋点
|
||||
- [ ] 偏好学习算法
|
||||
- [ ] 推荐优化
|
||||
|
||||
---
|
||||
|
||||
## 九、风险与应对
|
||||
|
||||
| 风险 | 影响 | 应对 |
|
||||
|------|------|------|
|
||||
| 隐私合规 | 高 | 儿童数据加密存储,家长授权机制 |
|
||||
| 推送骚扰 | 中 | 默认关闭,用户主动开启 |
|
||||
| 记忆膨胀 | 低 | 定期清理旧数据,限制宇宙数量 |
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- [孩子档案数据模型](./CHILD-PROFILE-MODEL.md)
|
||||
- [故事宇宙记忆结构](./STORY-UNIVERSE-MODEL.md)
|
||||
- [主动推送触发规则](./PUSH-TRIGGER-RULES.md)
|
||||
@@ -0,0 +1,177 @@
|
||||
# 记忆与个性化技术方案建议(PRD 讨论稿)
|
||||
|
||||
> 目标:给 DreamWeaver 的“记忆与个性化”提供可落地的技术路径与产品取舍依据,用于 PRD 细化。
|
||||
|
||||
---
|
||||
|
||||
## 1. 总体结论(推荐方案)
|
||||
|
||||
**v1 推荐:混合方案(结构化 DB + 轻量语义检索)**
|
||||
|
||||
- **DB** 作为权威事实与可解释记忆(孩子档案、宇宙设定、成就、偏好权重)。
|
||||
- **RAG** 用于非结构化内容(故事摘要、互动摘要、近期期望),辅助个性化提示词。
|
||||
|
||||
**原因**
|
||||
- 纯 DB 可控但缺乏语义弹性;纯 RAG 难以稳定控制与审计。
|
||||
- 混合方案能在“可解释 + 个性化”之间取到最佳平衡。
|
||||
|
||||
---
|
||||
|
||||
## 2. DB vs RAG:技术与产品对比
|
||||
|
||||
### 2.1 DB(结构化记忆)
|
||||
|
||||
**适用内容**
|
||||
- 孩子档案(基础信息)
|
||||
- 兴趣标签与成长主题
|
||||
- 故事宇宙设定(主角、世界观、常驻角色)
|
||||
- 成就(可审核、可追溯)
|
||||
|
||||
**优点**
|
||||
- 高可解释性
|
||||
- 变更可追踪、可回滚
|
||||
- 便于用户管理(家长可编辑)
|
||||
|
||||
**缺点**
|
||||
- 灵活性不足
|
||||
- 难以覆盖“隐性偏好”(比如叙事风格喜好)
|
||||
|
||||
### 2.2 RAG(语义记忆)
|
||||
|
||||
**适用内容**
|
||||
- 故事摘要
|
||||
- 互动摘要(“最近更喜欢冒险故事”)
|
||||
- 非结构化日志
|
||||
|
||||
**优点**
|
||||
- 具备语义召回能力
|
||||
- 适合挖掘“隐含偏好”
|
||||
|
||||
**缺点**
|
||||
- 可解释性弱
|
||||
- 成本与性能压力大
|
||||
- 隐私风险更高
|
||||
|
||||
---
|
||||
|
||||
## 3. 时序性与记忆衰减(建议必须有)
|
||||
|
||||
**核心观点**:孩子兴趣会随时间变化,必须引入时间衰减。
|
||||
|
||||
**做法建议**
|
||||
- 所有记忆项带 `created_at` / `last_used_at`
|
||||
- 引入权重衰减模型:
|
||||
- 近 7 天:高权重
|
||||
- 30 天:中权重
|
||||
- 90 天:低权重
|
||||
- 超过 90 天:降权或淘汰
|
||||
|
||||
**价值**
|
||||
- 避免旧偏好过度影响新故事
|
||||
- 体现成长与兴趣演变
|
||||
|
||||
---
|
||||
|
||||
## 4. 分层记忆(建议引入)
|
||||
|
||||
建议采用三层结构:
|
||||
|
||||
### 4.1 短期记忆(Session)
|
||||
- 当前生成上下文(关键词、选定档案/宇宙)
|
||||
- 生命周期:仅本次请求有效
|
||||
|
||||
### 4.2 中期记忆(近期偏好)
|
||||
- 最近 5-10 次故事生成/阅读偏好
|
||||
- 生命周期:30-60 天
|
||||
|
||||
### 4.3 长期记忆(稳定事实)
|
||||
- 档案、宇宙、核心兴趣
|
||||
- 生命周期:长期可编辑
|
||||
|
||||
**价值**
|
||||
- 既保留稳定设定,又能捕捉近期变化
|
||||
|
||||
---
|
||||
|
||||
## 5. Agent 动态判断是否写入记忆
|
||||
|
||||
**建议:规则优先 + 模型辅助**
|
||||
|
||||
流程示例:
|
||||
1. 命中规则(如完整阅读/重复播放)→ 进入候选
|
||||
2. LLM 抽取结构化信息 + 置信度
|
||||
3. 置信度不足 → 不写入
|
||||
|
||||
**优点**
|
||||
- 避免模型“乱记忆”
|
||||
- 降低噪声,提高记忆质量
|
||||
|
||||
---
|
||||
|
||||
## 6. 推荐的记忆数据结构
|
||||
|
||||
### 6.1 结构化表(DB)
|
||||
|
||||
- `child_profiles`:基础信息、兴趣、成长主题
|
||||
- `story_universes`:主角、角色、世界观、成就
|
||||
- `reading_events`:阅读/跳过/重播行为日志
|
||||
- `memory_items`:抽象记忆表(type, value, confidence, ttl)
|
||||
|
||||
### 6.2 语义检索(RAG)
|
||||
|
||||
- 存储内容:故事摘要、成就摘要、行为总结
|
||||
- 向量库:**pgvector**(成本低、易部署)
|
||||
- 检索过滤:`child_id` / `universe_id` / 时间窗口
|
||||
|
||||
---
|
||||
|
||||
## 7. 关键产品问题(需明确)
|
||||
|
||||
1) **记忆是否可编辑**
|
||||
- 家长是否能查看、修改、删除系统记忆?
|
||||
|
||||
2) **跨孩子隔离**
|
||||
- 同账号多孩子的记忆是否完全隔离(推荐隔离)
|
||||
|
||||
3) **隐私与合规**
|
||||
- 哪些数据进入记忆?是否脱敏?是否加密?
|
||||
|
||||
4) **性能与成本**
|
||||
- RAG 查询是否影响生成时延?
|
||||
- 是否需要缓存与批量检索?
|
||||
|
||||
5) **效果评估**
|
||||
- 记忆是否提高故事满意度?
|
||||
- 需要 A/B 或指标体系吗?
|
||||
|
||||
---
|
||||
|
||||
## 8. 推荐实施路线
|
||||
|
||||
### v1(1-2 个月)
|
||||
- DB 记忆为主,RAG 只做轻量补充
|
||||
- 引入时序衰减
|
||||
- 记忆来源:用户显式输入 + 行为日志
|
||||
|
||||
### v2(2-3 个月)
|
||||
- 引入 Agent 记忆抽取与置信度
|
||||
- 记忆管理界面(家长可编辑)
|
||||
- 更精细的个性化推荐
|
||||
|
||||
---
|
||||
|
||||
## 9. 需要确认的决定点
|
||||
|
||||
- 是否采用混合方案(DB + RAG)
|
||||
- RAG 的检索范围(故事摘要 / 行为摘要 / 成就)
|
||||
- 记忆分层与衰减规则
|
||||
- Agent 记忆写入规则与阈值
|
||||
- 家长可见/可控的记忆管理策略
|
||||
|
||||
---
|
||||
|
||||
如确认以上方向,我可以进一步输出:
|
||||
- PRD 里的“记忆系统”完整章节
|
||||
- 数据模型(含字段 + 时序衰减)
|
||||
- 交互与界面草案
|
||||
- 后端实现拆解(任务清单 + 里程碑)
|
||||
129
.claude/specs/memory-intelligence/PUSH-TRIGGER-RULES.md
Normal file
129
.claude/specs/memory-intelligence/PUSH-TRIGGER-RULES.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# 主动推送触发规则
|
||||
|
||||
## 概述
|
||||
|
||||
主动推送用于在合适的时间为家长提供个性化故事建议,提升使用频次与亲子阅读习惯。推送默认关闭,需家长开启并配置时间。
|
||||
|
||||
---
|
||||
|
||||
## 一、数据输入
|
||||
|
||||
- **孩子档案**: `child_profiles`(年龄、兴趣、成长主题)
|
||||
- **故事数据**: `stories`(最近生成/阅读时间、主题标签)
|
||||
- **推送配置**: `push_configs`(时间、周期、开关)
|
||||
- **节日与生日**: 预置日历 + `birth_date`
|
||||
- **行为事件**: 阅读/播放/跳过等行为埋点
|
||||
|
||||
---
|
||||
|
||||
## 二、触发类型与规则
|
||||
|
||||
### 2.1 时间触发(睡前)
|
||||
- 条件:当前时间落在用户设定 `push_time` 附近(建议 ±30 分钟)。
|
||||
- 频率:同一孩子每天最多 1 次。
|
||||
- 示例:19:00-21:00 之间推送“今晚想听什么故事?”
|
||||
|
||||
### 2.2 事件触发(节日/生日)
|
||||
- 条件:
|
||||
- 生日:`birth_date` 月日与当天一致。
|
||||
- 节日:命中节日清单(如儿童节、中秋节等)。
|
||||
- 频率:当天仅推送 1 次,优先级高于时间触发。
|
||||
|
||||
### 2.3 行为触发(召回)
|
||||
- 条件:最近 3 天无故事生成或阅读行为。
|
||||
- 频率:每 3 天最多 1 次,避免频繁打扰。
|
||||
|
||||
### 2.4 成长触发(年龄变化)
|
||||
- 条件:年龄跨越关键节点(如 4→5 岁)。
|
||||
- 频率:每次年龄变化仅触发一次。
|
||||
- 目的:推荐难度升级或新的成长主题。
|
||||
|
||||
---
|
||||
|
||||
## 三、优先级与抑制规则
|
||||
|
||||
**优先级顺序**(从高到低):
|
||||
1. 事件触发
|
||||
2. 成长触发
|
||||
3. 行为触发
|
||||
4. 时间触发
|
||||
|
||||
**抑制规则**:
|
||||
- 当天已推送则不再触发其他类型。
|
||||
- 若在静默时间(21:00-09:00)触发,则延迟至下一个允许窗口。
|
||||
- 用户关闭推送或未配置推送时间时,不触发。
|
||||
|
||||
---
|
||||
|
||||
## 四、个性化内容策略
|
||||
|
||||
- **兴趣标签**: 引用孩子的兴趣标签生成主题。
|
||||
- **成长主题**: 优先匹配当前成长主题。
|
||||
- **历史偏好**: 参考最近故事的标签与完成度。
|
||||
|
||||
**示例模板**:
|
||||
- “今晚给{child_name}讲一个关于{interest}的故事,好吗?”
|
||||
- “{child_name}最近在学习{growth_theme},我准备了一个新故事。”
|
||||
|
||||
---
|
||||
|
||||
## 五、调度实现建议
|
||||
|
||||
使用 Celery Beat 每 5-10 分钟执行一次规则检查:
|
||||
|
||||
```python
|
||||
@celery.task
|
||||
def check_push_notifications():
|
||||
now = datetime.now(local_tz)
|
||||
configs = get_enabled_configs(now)
|
||||
|
||||
for config in configs:
|
||||
if has_sent_today(config.child_profile_id):
|
||||
continue
|
||||
|
||||
trigger = select_trigger(config, now)
|
||||
if trigger:
|
||||
send_push_notification(config.user_id, config.child_profile_id, trigger)
|
||||
```
|
||||
|
||||
**关键点**:
|
||||
- 需要记录每日推送日志用于去重。
|
||||
- 优先级触发时应立即标记已发送。
|
||||
|
||||
---
|
||||
|
||||
## 六、日志与度量
|
||||
|
||||
建议增加 `push_events` 事件表用于统计与去重:
|
||||
|
||||
```sql
|
||||
CREATE TABLE push_events (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL,
|
||||
child_profile_id UUID NOT NULL,
|
||||
trigger_type VARCHAR(20) NOT NULL, -- time/event/behavior/growth
|
||||
sent_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
status VARCHAR(20) NOT NULL, -- sent/failed/suppressed
|
||||
reason TEXT
|
||||
);
|
||||
```
|
||||
|
||||
核心指标:
|
||||
- Push 发送成功率
|
||||
- 打开率(CTA 点击)
|
||||
- 触发分布占比
|
||||
|
||||
---
|
||||
|
||||
## 七、安全与合规
|
||||
|
||||
- **默认关闭**,需家长显式开启。
|
||||
- 支持一键关闭或设定免打扰时段。
|
||||
- 遵循儿童隐私合规要求,最小化推送内容敏感信息。
|
||||
|
||||
---
|
||||
|
||||
## 八、相关文档
|
||||
|
||||
- [记忆智能系统 PRD](./MEMORY-INTELLIGENCE-PRD.md)
|
||||
- [孩子档案数据模型](./CHILD-PROFILE-MODEL.md)
|
||||
231
.claude/specs/memory-intelligence/STORY-UNIVERSE-MODEL.md
Normal file
231
.claude/specs/memory-intelligence/STORY-UNIVERSE-MODEL.md
Normal file
@@ -0,0 +1,231 @@
|
||||
# 故事宇宙记忆结构
|
||||
|
||||
## 概述
|
||||
|
||||
故事宇宙用于在多次故事生成中保持角色、世界观与成长成就的连续性。每个孩子档案可以拥有多个宇宙,故事生成时可选择“延续上一个故事”,系统自动带入宇宙设定。
|
||||
|
||||
---
|
||||
|
||||
## 一、数据库模型
|
||||
|
||||
### 1.1 主表: story_universes
|
||||
|
||||
```sql
|
||||
CREATE TABLE story_universes (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
child_profile_id UUID NOT NULL REFERENCES child_profiles(id) ON DELETE CASCADE,
|
||||
|
||||
-- 宇宙基础
|
||||
name VARCHAR(100) NOT NULL,
|
||||
|
||||
-- 记忆结构
|
||||
protagonist JSONB NOT NULL, -- 主角设定
|
||||
recurring_characters JSONB DEFAULT '[]',
|
||||
world_settings JSONB DEFAULT '{}',
|
||||
achievements JSONB DEFAULT '[]',
|
||||
|
||||
-- 时间戳
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_story_universes_child_id ON story_universes(child_profile_id);
|
||||
CREATE INDEX idx_story_universes_updated_at ON story_universes(updated_at);
|
||||
```
|
||||
|
||||
### 1.2 JSON 结构示例
|
||||
|
||||
**protagonist**
|
||||
```json
|
||||
{
|
||||
"name": "小明",
|
||||
"role": "星际船长",
|
||||
"traits": ["勇敢", "好奇"],
|
||||
"goal": "寻找失落的星球",
|
||||
"backstory": "来自地球的探险家"
|
||||
}
|
||||
```
|
||||
|
||||
**recurring_characters**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "星星",
|
||||
"role": "魔法猫咪",
|
||||
"traits": ["聪明", "调皮"],
|
||||
"relation": "伙伴",
|
||||
"first_story_id": "story-uuid"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**world_settings**
|
||||
```json
|
||||
{
|
||||
"world_name": "梦幻森林",
|
||||
"era": "童话时代",
|
||||
"locations": ["彩虹河", "月光山"],
|
||||
"rules": ["动物会说话", "星星会指路"],
|
||||
"tone": "温暖治愈"
|
||||
}
|
||||
```
|
||||
|
||||
**achievements**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"type": "勇气",
|
||||
"description": "克服了对黑暗的恐惧",
|
||||
"story_id": "story-uuid",
|
||||
"achieved_at": "2025-01-10T12:00:00Z"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、SQLAlchemy 模型
|
||||
|
||||
```python
|
||||
# backend/app/db/models.py
|
||||
|
||||
from sqlalchemy import Column, String, ForeignKey, JSON
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import relationship
|
||||
import uuid
|
||||
|
||||
class StoryUniverse(Base):
|
||||
__tablename__ = "story_universes"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
child_profile_id = Column(UUID(as_uuid=True), ForeignKey("child_profiles.id", ondelete="CASCADE"), nullable=False)
|
||||
|
||||
name = Column(String(100), nullable=False)
|
||||
protagonist = Column(JSON, nullable=False)
|
||||
recurring_characters = Column(JSON, default=list)
|
||||
world_settings = Column(JSON, default=dict)
|
||||
achievements = Column(JSON, default=list)
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
|
||||
child_profile = relationship("ChildProfile", back_populates="story_universes")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、Pydantic Schema
|
||||
|
||||
```python
|
||||
# backend/app/schemas/story_universe.py
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
class StoryUniverseCreate(BaseModel):
|
||||
name: str = Field(..., min_length=1, max_length=100)
|
||||
protagonist: dict[str, Any]
|
||||
recurring_characters: list[dict[str, Any]] = Field(default_factory=list)
|
||||
world_settings: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
class StoryUniverseUpdate(BaseModel):
|
||||
name: str | None = Field(None, min_length=1, max_length=100)
|
||||
protagonist: dict[str, Any] | None = None
|
||||
recurring_characters: list[dict[str, Any]] | None = None
|
||||
world_settings: dict[str, Any] | None = None
|
||||
|
||||
class StoryUniverseResponse(BaseModel):
|
||||
id: UUID
|
||||
child_profile_id: UUID
|
||||
name: str
|
||||
protagonist: dict[str, Any]
|
||||
recurring_characters: list[dict[str, Any]]
|
||||
world_settings: dict[str, Any]
|
||||
achievements: list[dict[str, Any]]
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、API 约定
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/api/profiles/{id}/universes` | 获取孩子的故事宇宙列表 |
|
||||
| POST | `/api/profiles/{id}/universes` | 创建新宇宙 |
|
||||
| GET | `/api/universes/{id}` | 获取宇宙详情 |
|
||||
| PUT | `/api/universes/{id}` | 更新宇宙设定 |
|
||||
| POST | `/api/universes/{id}/achievements` | 添加成就 |
|
||||
|
||||
---
|
||||
|
||||
## 五、业务规则
|
||||
|
||||
- **延续故事**: “延续上一个故事”默认选最近更新的宇宙(按 `updated_at` 倒序)。
|
||||
- **成就追加**: 新成就追加到 `achievements`,以 `type + description` 去重。
|
||||
- **成长轨迹**: 成就保留顺序,优先展示最新项。
|
||||
|
||||
---
|
||||
|
||||
## 六、Prompt 集成
|
||||
|
||||
当选择宇宙时,生成 Prompt 需带入宇宙记忆:
|
||||
|
||||
```
|
||||
【故事宇宙】
|
||||
- 主角设定: {protagonist}
|
||||
- 常驻角色: {recurring_characters}
|
||||
- 世界观: {world_settings}
|
||||
- 已获成就: {achievements}
|
||||
```
|
||||
|
||||
未选择宇宙时,提示词忽略该块,避免混淆。
|
||||
|
||||
---
|
||||
|
||||
## 七、数据迁移示例
|
||||
|
||||
```python
|
||||
# backend/alembic/versions/xxx_add_story_universes.py
|
||||
|
||||
def upgrade():
|
||||
op.create_table(
|
||||
"story_universes",
|
||||
sa.Column("id", sa.UUID(), nullable=False),
|
||||
sa.Column("child_profile_id", sa.UUID(), nullable=False),
|
||||
sa.Column("name", sa.String(100), nullable=False),
|
||||
sa.Column("protagonist", sa.JSON(), nullable=False),
|
||||
sa.Column("recurring_characters", sa.JSON(), server_default='[]'),
|
||||
sa.Column("world_settings", sa.JSON(), server_default='{}'),
|
||||
sa.Column("achievements", sa.JSON(), server_default='[]'),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
sa.ForeignKeyConstraint(["child_profile_id"], ["child_profiles.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index("idx_story_universes_child_id", "story_universes", ["child_profile_id"])
|
||||
op.create_index("idx_story_universes_updated_at", "story_universes", ["updated_at"])
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_index("idx_story_universes_updated_at")
|
||||
op.drop_index("idx_story_universes_child_id")
|
||||
op.drop_table("story_universes")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、权限与安全
|
||||
|
||||
- 宇宙数据必须通过 `child_profile_id` 归属校验,确保仅拥有者可访问。
|
||||
- 删除用户或档案时,级联删除所有宇宙数据。
|
||||
|
||||
---
|
||||
|
||||
## 九、相关文档
|
||||
|
||||
- [孩子档案数据模型](./CHILD-PROFILE-MODEL.md)
|
||||
- [记忆智能系统 PRD](./MEMORY-INTELLIGENCE-PRD.md)
|
||||
Reference in New Issue
Block a user