chore: simplify project entrypoints

This commit is contained in:
2026-04-18 12:23:41 +08:00
parent 44405ff7ac
commit bb575a7fe9
76 changed files with 282 additions and 10399 deletions

View File

@@ -1,48 +0,0 @@
{
"permissions": {
"allow": [
"Skill(codex)",
"Bash(pip install:*)",
"Bash(alembic upgrade:*)",
"Bash(uvicorn:*)",
"Bash(npm run dev)",
"Bash(python:*)",
"Bash(ruff check:*)",
"Bash(tasklist:*)",
"Bash(findstr:*)",
"Bash(pushd:*)",
"Bash(popd)",
"Bash(curl:*)",
"WebSearch",
"WebFetch(domain:www.novelai.net)",
"WebFetch(domain:www.storywizard.ai)",
"WebFetch(domain:www.oscarstories.com)",
"WebFetch(domain:www.moshi.com)",
"WebFetch(domain:www.calm.com)",
"WebFetch(domain:www.epic.com)",
"WebFetch(domain:www.headspace.com)",
"WebFetch(domain:www.getepic.com)",
"WebFetch(domain:www.tonies.com)",
"Bash(del:*)",
"Bash(netstat:*)",
"Bash(taskkill:*)",
"Bash(codex-wrapper:*)",
"Bash(dir /b /s /a-d /o-d)",
"Bash(dir:*)",
"Bash(.venv/Scripts/python:*)",
"Bash(.venv/Scripts/ruff check:*)",
"Bash(npm run build:*)",
"Bash(test -f \"F:\\\\Code\\\\dreamweaver-python\\\\backend\\\\.env\")",
"Bash(pytest:*)",
"Bash(npm run type-check:*)",
"Bash(npx vue-tsc:*)",
"Bash(ls:*)",
"Bash(git init:*)",
"Bash(git add:*)",
"Bash(git commit:*)",
"Bash(git remote add:*)",
"Bash(git push:*)",
"Bash(git branch:*)"
]
}
}

View File

@@ -1,416 +0,0 @@
# 代码架构重构 PRD
> 版本: 1.0
> 日期: 2025-01-21
> 状态: 待实施
---
## 1. 背景与目标
### 1.1 现状问题
经过代码审计,发现以下架构债务:
| 问题 | 位置 | 影响 |
|------|------|------|
| 缺少 Schemas 层 | Backend API | 类型不安全,文档生成差 |
| Fat Controller | `stories.py` 562行 | 难测试,职责混乱 |
| 重复加载逻辑 | Frontend 5+ 组件 | 代码冗余,维护困难 |
| 无 Repository 层 | Backend | DB 查询散落,难复用 |
### 1.2 重构目标
- **后端**:引入 Pydantic Schemas 层,分离请求/响应模型
- **前端**:抽取 Vue Composables消除重复逻辑
- **原则**:渐进式重构,不破坏现有功能
---
## 2. 后端重构Schemas 层
### 2.1 目标结构
```
backend/app/
├── api/ # 路由层(瘦身)
│ ├── stories.py
│ └── ...
├── schemas/ # 新增Pydantic 模型
│ ├── __init__.py
│ ├── auth.py
│ ├── story.py
│ ├── profile.py
│ ├── universe.py
│ ├── memory.py
│ ├── push_config.py
│ └── common.py # 分页、错误响应等通用结构
├── models/ # SQLAlchemy ORM原 db/
└── services/ # 业务逻辑
```
### 2.2 Schema 设计规范
#### 命名约定
| 类型 | 命名模式 | 示例 |
|------|----------|------|
| 创建请求 | `{Entity}Create` | `StoryCreate` |
| 更新请求 | `{Entity}Update` | `ProfileUpdate` |
| 响应模型 | `{Entity}Response` | `StoryResponse` |
| 列表响应 | `{Entity}ListResponse` | `StoryListResponse` |
#### 示例Story Schema
```python
# schemas/story.py
from datetime import datetime
from typing import Literal
from pydantic import BaseModel, Field
class StoryCreate(BaseModel):
"""创建故事请求"""
type: Literal["keywords", "enhance", "storybook"]
data: str = Field(..., min_length=1, max_length=2000)
education_theme: str | None = None
child_profile_id: str | None = None
universe_id: str | None = None
class StoryResponse(BaseModel):
"""故事响应"""
id: int
title: str
story_text: str | None
pages: list[dict] | None
image_url: str | None
mode: str
created_at: datetime
class Config:
from_attributes = True # 支持 ORM 模型转换
class StoryListResponse(BaseModel):
"""故事列表响应"""
stories: list[StoryResponse]
total: int
page: int
page_size: int
```
### 2.3 迁移步骤
| 阶段 | 任务 | 文件 |
|------|------|------|
| P1 | 创建 `schemas/common.py` | 分页、错误响应 |
| P2 | 创建 `schemas/story.py` | 故事相关模型 |
| P3 | 创建 `schemas/profile.py` | 档案相关模型 |
| P4 | 创建 `schemas/universe.py` | 宇宙相关模型 |
| P5 | 创建 `schemas/auth.py` | 认证相关模型 |
| P6 | 创建 `schemas/memory.py` | 记忆相关模型 |
| P7 | 创建 `schemas/push_config.py` | 推送配置模型 |
| P8 | 重构 `api/stories.py` | 使用新 schemas |
| P9 | 重构其他 API 文件 | 逐个迁移 |
### 2.4 验收标准
- [ ] 所有 API endpoint 使用 `response_model` 参数
- [ ] 请求体使用 Pydantic 模型而非 `Body(...)`
- [ ] OpenAPI 文档自动生成完整类型
- [ ] 现有测试全部通过
- [ ] `ruff check` 无错误
---
## 3. 前端重构Composables
### 3.1 目标结构
```
frontend/src/
├── composables/ # 新增:可复用逻辑
│ ├── index.ts
│ ├── useAsyncData.ts # 异步数据加载
│ ├── useFormValidation.ts # 表单验证
│ └── useDateFormat.ts # 日期格式化
├── components/
├── stores/
└── views/
```
### 3.2 Composable 设计
#### 3.2.1 useAsyncData
**用途**:统一处理 API 数据加载的 loading/error 状态
```typescript
// composables/useAsyncData.ts
import { ref, type Ref } from 'vue'
interface AsyncDataOptions<T> {
immediate?: boolean // 立即执行,默认 true
initialData?: T // 初始数据
onError?: (e: Error) => void
}
interface AsyncDataReturn<T> {
data: Ref<T | null>
loading: Ref<boolean>
error: Ref<string>
execute: () => Promise<void>
reset: () => void
}
export function useAsyncData<T>(
fetcher: () => Promise<T>,
options: AsyncDataOptions<T> = {}
): AsyncDataReturn<T> {
const { immediate = true, initialData = null, onError } = options
const data = ref<T | null>(initialData) as Ref<T | null>
const loading = ref(false)
const error = ref('')
async function execute() {
loading.value = true
error.value = ''
try {
data.value = await fetcher()
} catch (e) {
const message = e instanceof Error ? e.message : '加载失败'
error.value = message
onError?.(e instanceof Error ? e : new Error(message))
} finally {
loading.value = false
}
}
function reset() {
data.value = initialData
loading.value = false
error.value = ''
}
if (immediate) {
execute()
}
return { data, loading, error, execute, reset }
}
```
**使用示例**
```typescript
// views/MyStories.vue重构前 40 行 → 重构后 10 行)
import { useAsyncData } from '@/composables'
import { api } from '@/api/client'
const { data: response, loading, error } = useAsyncData(
() => api.get<StoryListResponse>('/api/stories')
)
const stories = computed(() => response.value?.stories ?? [])
```
#### 3.2.2 useFormValidation
**用途**:统一表单验证逻辑
```typescript
// composables/useFormValidation.ts
import { ref, reactive } from 'vue'
type ValidationRule = (value: unknown) => string | true
interface FieldRules {
[field: string]: ValidationRule[]
}
export function useFormValidation<T extends Record<string, unknown>>(
initialData: T,
rules: FieldRules
) {
const form = reactive({ ...initialData })
const errors = reactive<Record<string, string>>({})
function validate(): boolean {
let valid = true
for (const [field, fieldRules] of Object.entries(rules)) {
const value = form[field]
for (const rule of fieldRules) {
const result = rule(value)
if (result !== true) {
errors[field] = result
valid = false
break
} else {
errors[field] = ''
}
}
}
return valid
}
function reset() {
Object.assign(form, initialData)
Object.keys(errors).forEach(k => errors[k] = '')
}
return { form, errors, validate, reset }
}
// 常用验证规则
export const required = (msg = '此字段必填') =>
(v: unknown) => (v !== null && v !== undefined && v !== '') || msg
export const minLength = (min: number, msg?: string) =>
(v: unknown) => (typeof v === 'string' && v.length >= min) || msg || `至少 ${min} 个字符`
export const maxLength = (max: number, msg?: string) =>
(v: unknown) => (typeof v === 'string' && v.length <= max) || msg || `最多 ${max} 个字符`
```
#### 3.2.3 useDateFormat
**用途**:统一日期格式化
```typescript
// composables/useDateFormat.ts
export function useDateFormat() {
function formatDate(dateStr: string | Date, format: 'date' | 'datetime' | 'relative' = 'date'): string {
const date = typeof dateStr === 'string' ? new Date(dateStr) : dateStr
if (format === 'relative') {
return formatRelative(date)
}
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
if (format === 'date') {
return `${year}-${month}-${day}`
}
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}`
}
function formatRelative(date: Date): string {
const now = new Date()
const diffMs = now.getTime() - date.getTime()
const diffMins = Math.floor(diffMs / 60000)
const diffHours = Math.floor(diffMs / 3600000)
const diffDays = Math.floor(diffMs / 86400000)
if (diffMins < 1) return '刚刚'
if (diffMins < 60) return `${diffMins} 分钟前`
if (diffHours < 24) return `${diffHours} 小时前`
if (diffDays < 7) return `${diffDays} 天前`
return formatDate(date, 'date')
}
return { formatDate, formatRelative }
}
```
### 3.3 迁移步骤
| 阶段 | 任务 | 影响文件 |
|------|------|----------|
| P1 | 创建 `composables/useAsyncData.ts` | 新文件 |
| P2 | 创建 `composables/useDateFormat.ts` | 新文件 |
| P3 | 创建 `composables/useFormValidation.ts` | 新文件 |
| P4 | 创建 `composables/index.ts` | 统一导出 |
| P5 | 重构 `views/MyStories.vue` | 使用 useAsyncData |
| P6 | 重构 `views/ChildProfiles.vue` | 使用 useAsyncData |
| P7 | 重构 `views/Universes.vue` | 使用 useAsyncData |
| P8 | 重构 `components/CreateStoryModal.vue` | 使用 useFormValidation |
| P9 | 同步重构 `admin-frontend/` | 复制 composables |
### 3.4 验收标准
- [ ] `composables/` 目录包含 3 个核心 composable
- [ ] 至少 3 个 view 组件使用 `useAsyncData`
- [ ] `CreateStoryModal` 使用 `useFormValidation`
- [ ] `npm run build` 无类型错误
- [ ] 现有功能正常运行
---
## 4. 风险与缓解
| 风险 | 影响 | 缓解措施 |
|------|------|----------|
| 重构引入 bug | 高 | 每个阶段运行测试 |
| 类型不兼容 | 中 | 使用 `from_attributes = True` |
| 前端构建失败 | 中 | 逐文件迁移,及时 commit |
---
## 5. 时间估算
| 模块 | 工作量 | 预计时间 |
|------|--------|----------|
| Backend Schemas | 10 个新文件 + 9 个 API 重构 | 3-4 小时 |
| Frontend Composables | 4 个新文件 + 6 个组件重构 | 2-3 小时 |
| 测试验证 | 全量回归 | 1 小时 |
| **总计** | | **6-8 小时** |
---
## 6. 后续迭代
本次重构完成后,可继续:
1. **Repository 层**:抽象 DB 查询逻辑
2. **Service 层瘦身**:拆分 `provider_router.py` (433行)
3. **前端共享包**`frontend``admin-frontend` 共用 UI 组件
4. **API 版本化**:引入 `/api/v1/` 前缀
---
## 附录:文件清单
### 新增文件
```
backend/app/schemas/
├── __init__.py
├── common.py
├── auth.py
├── story.py
├── profile.py
├── universe.py
├── memory.py
└── push_config.py
frontend/src/composables/
├── index.ts
├── useAsyncData.ts
├── useFormValidation.ts
└── useDateFormat.ts
```
### 修改文件
```
backend/app/api/
├── stories.py
├── profiles.py
├── universes.py
├── memories.py
├── push_configs.py
├── reading_events.py
└── auth.py
frontend/src/views/
├── MyStories.vue
├── ChildProfiles.vue
├── Universes.vue
└── ...
frontend/src/components/
└── CreateStoryModal.vue
```

View File

@@ -1,153 +0,0 @@
# DreamWeaver 品牌视觉方向Web 阶段)
## 概述
提供三套高保真视觉方向,用于 Web MVP。三者的 UX 结构一致,仅在色彩、视觉重量与插画风格上不同。
---
## 方案 ASoft Aurora温暖高级
**理由**
- 家长信任感强,同时保留童趣与想象力。
- 高级但不商业化。
**配色**
- 主色 600: #6C5CE7
- 主色 500: #7C69FF
- 主色 100: #EAE7FF
- 强调粉: #FF8FB1
- 强调蓝: #65C3FF
- 中性 900: #1F2430
- 中性 700: #4B5563
- 中性 500: #9AA3B2
- 中性 200: #E5E7EB
- 中性 100: #F5F7FB
- 白色: #FFFFFF
**渐变**
- Hero 背景linear-gradient(135deg, #EAE7FF 0%, #FDF6FF 40%, #EAF6FF 100%)
- CTA 光晕radial-gradient(circle at 30% 30%, #7C69FF 0%, #6C5CE7 50%, #4C3FCF 100%)
**字体**
- 标题Noto Sans SC / Inter
- 正文Noto Sans SC / Inter
- 数字强调Inter
**插画风格**
- 柔和、低对比度、轻画笔质感。
- 圆润形状、轻高光。
- 角色简单轮廓与友好表情。
**图标风格**
- 1.5px 线宽,圆角端点。
- 强调色点缀,避免过度饱和。
**组件建议**
- 按钮:主色实心 + 内阴影。
- 卡片:大圆角 + 柔和阴影。
- 输入:浅底色 + 主色焦点环。
---
## 方案 BStorybook Minimal极简编辑风
**理由**
- 强可读性,适合长文本阅读。
- 简洁、专业、强调内容。
**配色**
- 主色 600: #3B82F6
- 主色 500: #60A5FA
- 主色 100: #DBEAFE
- 强调金: #F5C542
- 强调薄荷: #6EE7B7
- 中性 900: #111827
- 中性 700: #374151
- 中性 500: #9CA3AF
- 中性 200: #E5E7EB
- 中性 100: #F9FAFB
- 白色: #FFFFFF
**渐变**
- Hero 背景linear-gradient(180deg, #F9FAFB 0%, #EEF2FF 100%)
- CTA 光晕radial-gradient(circle at 40% 30%, #60A5FA 0%, #3B82F6 60%, #1D4ED8 100%)
**字体**
- 标题Inter / Noto Sans SC
- 正文Inter / Noto Sans SC
- 阅读场景可提升行高和对比度。
**插画风格**
- 扁平化、线条干净、留白较多。
- 色彩克制、视觉清爽。
**图标风格**
- 2px 线宽,极简。
**组件建议**
- 按钮:纯色、无明显渐变。
- 卡片:细边框 + 极轻阴影。
- 输入:白底 + 清晰边框。
---
## 方案 CPlayful Glow活力明快
**理由**
- 视觉更鲜活,记忆点强。
- 更偏童趣,但仍保持专业感。
**配色**
- 主色 600: #7C3AED
- 主色 500: #8B5CF6
- 主色 100: #EDE9FE
- 强调珊瑚: #FB7185
- 强调青蓝: #22D3EE
- 中性 900: #1F2937
- 中性 700: #4B5563
- 中性 500: #9CA3AF
- 中性 200: #E5E7EB
- 中性 100: #F5F5F7
- 白色: #FFFFFF
**渐变**
- Hero 背景linear-gradient(135deg, #EDE9FE 0%, #FFE4F3 45%, #E0F7FF 100%)
- CTA 光晕radial-gradient(circle at 30% 30%, #8B5CF6 0%, #7C3AED 60%, #5B21B6 100%)
**字体**
- 标题Noto Sans SC / Inter
- 正文Noto Sans SC / Inter
- 强调色点到为止,避免花哨。
**插画风格**
- 更鲜艳、更活泼。
- 大色块 + 轻纹理背景。
**图标风格**
- 1.5px 线宽 + 小实心点装饰。
**组件建议**
- 按钮:渐变或实心 + Hover 发光。
- 卡片:更明显阴影 + 彩色边角。
- 输入:轻微色彩底。
---
## 共享视觉资产
**封面比例**
- 列表卡片21:9
- 详情头图16:9
**插画 vs 照片**
- 默认使用插画,避免真实儿童照片(隐私与合规)。
**空态插画**
- 统一 1 张主插画,做颜色变体复用。
---
## 推荐
建议 Web MVP 使用方案 ASoft Aurora兼顾温暖与信任。方案 B/C 可作为后续主题或 A/B 测试备选。

View File

@@ -1,639 +0,0 @@
# DreamWeaver 落地页重构规范文档
## 1. 项目概述
### 1.1 目标
将当前简单的 Home.vue 落地页重构为专业级 SaaS 产品落地页,提升品牌形象和用户转化率。
### 1.2 当前状态
- 文件位置: `frontend/src/views/Home.vue`
- 问题: 页面结构单一,仅有一个故事生成表单,缺少产品介绍、功能展示、用户信任背书等专业落地页必备元素
### 1.3 技术栈
- Vue 3 + Composition API + TypeScript
- Tailwind CSS
- vue-i18n 国际化
- @heroicons/vue/24/outline 图标库
- 现有 UI 组件: BaseButton, BaseCard, BaseInput, BaseSelect, BaseTextarea
---
## 2. 页面结构规范
页面从上到下包含 8 个主要区块Section每个区块独立且可复用。
### 2.1 Hero Section主视觉区
**布局**: 两栏布局,左 60% 右 40%,移动端堆叠
**左侧内容**:
```
- 主标题: 使用 gradient-text 样式
- 第一行: "为孩子编织" (普通渐变)
- 第二行: "专属的童话梦境" (加粗强调)
- 副标题: 灰色次要文字,说明产品价值
- CTA 按钮组:
- 主按钮: "开始创作" (btn-magic 样式,点击打开创作模态框)
- 次按钮: "了解更多" (outline 样式,滚动到 Features 区块)
```
**右侧内容**:
```
- 故事卡片预览 (模拟产品效果)
- 卡片使用 glass 样式 + 阴影
- 顶部: 模拟封面图区域 (渐变色块 + 星星图标)
- 标题: "小兔子的勇气冒险"
- 内容预览: 故事开头文字 (截断显示)
- 底部: 模拟的播放按钮和图片生成按钮图标
- 添加浮动动画 (animate-float)
```
**背景装饰**:
```
- 左上角: 浮动星星 SVG (absolute, opacity-20)
- 右下角: 浮动云朵 SVG (absolute, opacity-15)
- 使用 CSS animation 实现缓慢浮动效果
```
**i18n 键**:
- `home.heroTitle`, `home.heroTitleHighlight`
- `home.heroSubtitle`
- `home.heroCta`, `home.heroCtaSecondary`
- `home.heroPreviewTitle`, `home.heroPreviewText`
---
### 2.2 Trust Bar信任背书区
**布局**: 水平三等分,居中对齐
**内容**:
```
| 10,000+ 故事已创作 | 5,000+ 家庭信赖 | 98% 满意度 |
```
**样式**:
```
- 背景: 浅紫色渐变 (from-purple-50 to-pink-50)
- 数字: 大号加粗,渐变色
- 文字: 灰色小号
- 分隔: 使用竖线或间距分隔
```
**交互**:
```
- 数字使用计数动画 (从 0 递增到目标值)
- 使用 IntersectionObserver 触发动画
- 动画时长: 2 秒,使用 easeOutQuart 缓动
```
**实现要点**:
```typescript
// 计数动画函数
function animateCount(target: number, duration: number, callback: (value: number) => void) {
const start = performance.now()
const step = (timestamp: number) => {
const progress = Math.min((timestamp - start) / duration, 1)
const eased = 1 - Math.pow(1 - progress, 4) // easeOutQuart
callback(Math.floor(eased * target))
if (progress < 1) requestAnimationFrame(step)
}
requestAnimationFrame(step)
}
```
**i18n 键**:
- `home.trustStoriesCreated`, `home.trustFamilies`, `home.trustSatisfaction`
---
### 2.3 Features Section功能特性区
**布局**: 标题 + 副标题 + 6 卡片网格 (3x2移动端 1 列)
**标题区**:
```
- 主标题: "为什么选择梦语织机"
- 副标题: "我们用 AI 技术和教育理念,为每个孩子打造独一无二的成长故事"
```
**6 个功能卡片**:
| # | 图标 | 标题 | 描述 |
|---|------|------|------|
| 1 | SparklesIcon | AI 智能创作 | 输入几个关键词AI 即刻为您的孩子创作一个充满想象力的原创故事 |
| 2 | UserIcon | 个性化记忆 | 系统记住孩子的喜好和成长轨迹,故事越来越懂 TA |
| 3 | PhotoIcon | 精美 AI 插画 | 为每个故事自动生成独特的精美封面插画,让故事更加生动 |
| 4 | SpeakerWaveIcon | 温暖语音朗读 | 专业级 AI 配音,温暖的声音陪伴孩子进入甜美梦乡 |
| 5 | AcademicCapIcon | 教育主题融入 | 勇气、友谊、分享、诚实...在故事中自然传递正向价值观 |
| 6 | GlobeAltIcon | 故事宇宙 | 创建专属世界观,让喜爱的角色在不同故事中持续冒险 |
**卡片样式**:
```
- 使用 BaseCard 组件,添加 hover 效果
- 图标: 48x48紫色渐变背景圆形容器
- 标题: font-bold text-gray-800
- 描述: text-gray-600 text-sm
- hover: 上移 + 阴影增强
```
**滚动动画**:
```
- 使用 IntersectionObserver
- 卡片依次渐入 (stagger 100ms)
- 动画: opacity 0->1, translateY 20px->0
```
**i18n 键**:
- `home.featuresTitle`, `home.featuresSubtitle`
- `home.feature1Title` ~ `home.feature6Title`
- `home.feature1Desc` ~ `home.feature6Desc`
---
### 2.4 How It Works Section使用流程区
**布局**: 标题 + 4 步骤水平排列(移动端垂直)
**步骤内容**:
| 步骤 | 图标 | 标题 | 描述 |
|------|------|------|------|
| 1 | LightBulbIcon | 输入灵感 | 输入关键词、角色或简单想法 |
| 2 | CpuChipIcon | AI 创作 | AI 根据输入生成专属故事 |
| 3 | PaintBrushIcon | 丰富内容 | 自动生成精美插画和语音 |
| 4 | ShareIcon | 分享故事 | 保存收藏,随时为孩子讲述 |
**样式**:
```
- 步骤编号: 圆形渐变背景,白色数字
- 步骤之间: 虚线连接 (桌面端水平,移动端垂直)
- 图标: 在编号下方,较大尺寸
- 文字: 居中对齐
```
**连接线实现**:
```css
/* 桌面端水平连接线 */
.step-connector {
position: absolute;
top: 24px;
left: 100%;
width: 100%;
height: 2px;
background: linear-gradient(90deg, #c084fc, #f472b6);
opacity: 0.3;
}
/* 移动端隐藏水平线,显示垂直线 */
@media (max-width: 768px) {
.step-connector { display: none; }
.step-connector-vertical { display: block; }
}
```
**i18n 键**:
- `home.howItWorksTitle`, `home.howItWorksSubtitle`
- `home.step1Title` ~ `home.step4Title`
- `home.step1Desc` ~ `home.step4Desc`
---
### 2.5 Product Showcase Section产品展示区
**布局**: 两栏,左侧功能列表,右侧模拟界面
**左侧内容**:
```
- 小标题: "专为家长设计"
- 大标题: "简单易用,功能强大"
- 功能列表 (带勾选图标):
✓ 直观的创作界面,几秒即可生成故事
✓ 多孩子档案管理,每个孩子独立记忆
✓ 故事历史永久保存,随时回顾美好时光
✓ 支持中英双语,培养语言能力
```
**右侧内容**:
```
- 模拟的产品界面截图 (CSS 绘制)
- 包含:
- 模拟浏览器顶栏 (三个圆点)
- 模拟导航栏
- 模拟故事卡片列表
- 使用 glass 样式 + 阴影
- 轻微倾斜 (transform: perspective + rotateY)
```
**模拟界面 CSS**:
```css
.mock-browser {
background: white;
border-radius: 12px;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
transform: perspective(1000px) rotateY(-5deg);
}
.mock-browser-bar {
height: 32px;
background: #f1f5f9;
border-radius: 12px 12px 0 0;
display: flex;
align-items: center;
padding: 0 12px;
gap: 6px;
}
.mock-dot {
width: 10px;
height: 10px;
border-radius: 50%;
}
.mock-dot-red { background: #ef4444; }
.mock-dot-yellow { background: #eab308; }
.mock-dot-green { background: #22c55e; }
```
**i18n 键**:
- `home.showcaseTitle`, `home.showcaseSubtitle`
- `home.showcaseFeature1` ~ `home.showcaseFeature4`
---
### 2.6 Testimonials Section用户评价区
**布局**: 标题 + 3 评价卡片水平排列
**评价内容**:
| # | 评价 | 用户名 | 身份 |
|---|------|--------|------|
| 1 | "每晚睡前,女儿都要听一个新故事。梦语织机让我不再为编故事发愁,而且故事质量真的很高!" | 小雨妈妈 | 5岁女孩家长 |
| 2 | "最惊喜的是个性化功能,系统记住了儿子喜欢恐龙和太空,每个故事都能戳中他的兴趣点。" | 航航爸爸 | 6岁男孩家长 |
| 3 | "语音朗读功能太棒了!出差时也能远程给孩子讲故事,声音温暖自然,孩子很喜欢。" | 朵朵妈妈 | 4岁女孩家长 |
**卡片样式**:
```
- 背景: glass 样式
- 顶部: 引号图标 (大号,低透明度)
- 评价文字: 斜体,灰色
- 底部: 头像 + 用户名 + 身份
- 头像: 渐变色圆形 + 首字母
```
**头像生成**:
```typescript
// 根据名字生成渐变色头像
function getAvatarStyle(name: string) {
const colors = [
['#667eea', '#764ba2'],
['#f093fb', '#f5576c'],
['#4facfe', '#00f2fe'],
]
const index = name.charCodeAt(0) % colors.length
return {
background: `linear-gradient(135deg, ${colors[index][0]}, ${colors[index][1]})`,
}
}
```
**i18n 键**:
- `home.testimonialsTitle`, `home.testimonialsSubtitle`
- `home.testimonial1Text` ~ `home.testimonial3Text`
- `home.testimonial1Name` ~ `home.testimonial3Name`
- `home.testimonial1Role` ~ `home.testimonial3Role`
---
### 2.7 FAQ Section常见问题区
**布局**: 标题 + 手风琴问答列表
**问答内容**:
| # | 问题 | 答案 |
|---|------|------|
| 1 | 梦语织机适合多大的孩子? | 我们专为 3-8 岁儿童设计,故事内容、语言难度和教育主题都针对这个年龄段优化。 |
| 2 | 生成的故事安全吗? | 绝对安全。所有故事都经过内容过滤,确保适合儿童阅读,传递积极正向的价值观。 |
| 3 | 可以自定义故事角色吗? | 可以您可以在孩子档案中设置喜好或在创作时指定角色名称、特点AI 会将其融入故事。 |
| 4 | 故事会重复吗? | 不会。每个故事都是 AI 实时原创生成的,即使使用相同关键词,也会产生不同的故事。 |
| 5 | 支持哪些语言? | 目前支持中文和英文,您可以随时切换界面语言,故事也会相应调整。 |
**手风琴实现**:
```typescript
const expandedFaq = ref<number | null>(null)
function toggleFaq(index: number) {
expandedFaq.value = expandedFaq.value === index ? null : index
}
```
**样式**:
```
- 问题行: 可点击,右侧箭头图标
- 展开时: 箭头旋转 180°答案滑入显示
- 使用 Transition 组件实现平滑动画
```
**i18n 键**:
- `home.faqTitle`
- `home.faq1Question` ~ `home.faq5Question`
- `home.faq1Answer` ~ `home.faq5Answer`
---
### 2.8 Final CTA Section底部转化区
**布局**: 居中,渐变背景
**内容**:
```
- 大标题: "准备好为孩子创造魔法了吗?"
- 副标题: "立即开始,让 AI 为您的孩子编织独一无二的成长故事"
- CTA 按钮: "免费开始创作" (大号btn-magic)
- 小字: "无需信用卡,立即体验"
```
**背景**:
```css
.cta-section {
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
position: relative;
overflow: hidden;
}
/* 装饰性圆形 */
.cta-section::before {
content: '';
position: absolute;
width: 400px;
height: 400px;
background: rgba(255, 255, 255, 0.1);
border-radius: 50%;
top: -200px;
right: -100px;
}
```
**i18n 键**:
- `home.ctaTitle`, `home.ctaSubtitle`
- `home.ctaButton`, `home.ctaNote`
---
## 3. 创作模态框规范
### 3.1 触发方式
- 点击 Hero 区 "开始创作" 按钮
- 点击 Final CTA 区按钮
- 已登录用户直接打开模态框
- 未登录用户跳转登录流程
### 3.2 模态框结构
```
- 遮罩层: 半透明黑色背景
- 模态框: 居中,最大宽度 600px
- 关闭按钮: 右上角 X 图标
- 内容: 复用原有表单逻辑
```
### 3.3 表单内容(保留原有逻辑)
```
1. 输入类型切换: 关键词创作 / 故事润色
2. 孩子档案选择 (可选)
3. 故事宇宙选择 (可选,依赖档案)
4. 输入区域 (关键词或故事文本)
5. 教育主题选择 (可选)
6. 提交按钮
```
### 3.4 状态管理
```typescript
const showCreateModal = ref(false)
function openCreateModal() {
if (!userStore.user) {
// 跳转登录
return
}
showCreateModal.value = true
}
```
---
## 4. 动画规范
### 4.1 滚动渐入动画
**实现方式**: 使用 IntersectionObserver + CSS Transition
```typescript
// composables/useScrollAnimation.ts
export function useScrollAnimation() {
const observedElements = ref<Set<Element>>(new Set())
onMounted(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('animate-in')
observer.unobserve(entry.target)
}
})
},
{ threshold: 0.1 }
)
document.querySelectorAll('.scroll-animate').forEach((el) => {
observer.observe(el)
})
})
}
```
**CSS**:
```css
.scroll-animate {
opacity: 0;
transform: translateY(20px);
transition: opacity 0.6s ease, transform 0.6s ease;
}
.scroll-animate.animate-in {
opacity: 1;
transform: translateY(0);
}
/* 延迟类 */
.delay-100 { transition-delay: 100ms; }
.delay-200 { transition-delay: 200ms; }
.delay-300 { transition-delay: 300ms; }
```
### 4.2 浮动动画
```css
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
.animate-float {
animation: float 3s ease-in-out infinite;
}
.animate-float-slow {
animation: float 5s ease-in-out infinite;
}
```
### 4.3 数字计数动画
见 2.2 节 Trust Bar 实现要点。
---
## 5. 响应式规范
### 5.1 断点定义
```
- sm: 640px
- md: 768px
- lg: 1024px
- xl: 1280px
```
### 5.2 各区块响应式行为
| 区块 | 桌面端 | 平板端 | 移动端 |
|------|--------|--------|--------|
| Hero | 两栏 60/40 | 两栏 50/50 | 单栏堆叠 |
| Trust Bar | 水平三等分 | 水平三等分 | 垂直堆叠 |
| Features | 3x2 网格 | 2x3 网格 | 单列 |
| How It Works | 水平 4 步 | 水平 4 步 | 垂直 4 步 |
| Showcase | 两栏 | 两栏 | 单栏堆叠 |
| Testimonials | 水平 3 卡 | 水平 3 卡 | 单列滚动 |
| FAQ | 单列 | 单列 | 单列 |
| Final CTA | 居中 | 居中 | 居中 |
---
## 6. 暗色模式规范
### 6.1 颜色映射
| 元素 | 亮色模式 | 暗色模式 |
|------|----------|----------|
| 背景 | 渐变浅色 | 渐变深色 |
| 卡片背景 | rgba(255,255,255,0.7) | rgba(15,23,42,0.6) |
| 主文字 | gray-800 | gray-100 |
| 次文字 | gray-600 | gray-400 |
| 边框 | gray-200 | gray-700 |
### 6.2 实现方式
使用 Tailwind dark: 前缀,配合现有 .dark 类切换。
---
## 7. 验收标准
### 7.1 功能验收
- [ ] Hero 区正确显示CTA 按钮可点击
- [ ] Trust Bar 数字动画正常触发
- [ ] Features 6 个卡片正确显示
- [ ] How It Works 4 步骤正确显示,连接线可见
- [ ] Product Showcase 模拟界面正确渲染
- [ ] Testimonials 3 个评价卡片正确显示
- [ ] FAQ 手风琴展开/收起正常
- [ ] Final CTA 按钮可点击
- [ ] 创作模态框正常打开/关闭
- [ ] 故事生成功能正常(保留原有逻辑)
### 7.2 样式验收
- [ ] 所有文案使用 i18n中英文切换正常
- [ ] 响应式布局在 320px ~ 1920px 宽度下正常
- [ ] 暗色模式下所有元素可读
- [ ] 滚动动画流畅,无卡顿
- [ ] 所有图标正确显示
### 7.3 性能验收
- [ ] 首屏加载时间 < 3s
- [ ] Lighthouse Performance 分数 > 80
- [ ] 无控制台错误
---
## 8. 文件变更清单
| 文件 | 操作 | 说明 |
|------|------|------|
| `frontend/src/views/Home.vue` | 重写 | 完整重构落地页 |
| `frontend/src/locales/zh.json` | 已更新 | 新增落地页文案 |
| `frontend/src/locales/en.json` | 已更新 | 新增落地页文案 |
| `frontend/src/style.css` | 修改 | 新增动画和样式类 |
| `frontend/src/composables/useScrollAnimation.ts` | 新建 | 滚动动画 composable |
---
## 9. 依赖说明
### 9.1 现有依赖(无需新增)
- Vue 3
- vue-router
- vue-i18n
- Pinia
- Tailwind CSS
- @heroicons/vue
### 9.2 需要使用的图标
```typescript
import {
SparklesIcon,
UserIcon,
PhotoIcon,
SpeakerWaveIcon,
AcademicCapIcon,
GlobeAltIcon,
LightBulbIcon,
CpuChipIcon,
PaintBrushIcon,
ShareIcon,
CheckIcon,
ChevronDownIcon,
XMarkIcon,
ArrowRightIcon,
} from '@heroicons/vue/24/outline'
```
---
## 10. 实现顺序建议
1. **Phase 1**: 基础结构
- 创建页面骨架8 个 section
- 实现 Hero 区(不含动画)
- 实现创作模态框
2. **Phase 2**: 内容区块
- Trust Bar + 计数动画
- Features 卡片
- How It Works 步骤
3. **Phase 3**: 展示区块
- Product Showcase
- Testimonials
- FAQ 手风琴
4. **Phase 4**: 收尾
- Final CTA
- 滚动动画
- 响应式调整
- 暗色模式适配
---
*文档版本: 1.0*
*创建时间: 2025-12-30*
*作者: Claude Code*

View File

@@ -1,230 +0,0 @@
# DreamWeaver 高保真页面布局与组件规格Web
## 范围
本文将每个页面映射为高保真布局规范:结构、核心组件与状态,便于在 Figma 中快速搭建。
---
## 全局布局
- 画布1440 x 900
- 内容容器1200px 居中
- 栅格12 列24px 间距
- 基础间距8pt
---
## 全局组件
**顶部导航**
-Logo + 产品名
- 中:主导航
- 右:搜索、孩子切换器、头像菜单
**主 CTA**
- 主色实心按钮
**卡片**
- 21:9 封面
- 标题、标签、元信息、操作
**表单控件**
- 文本输入、选择器、日期、滑块、标签
**状态**
- 空态、加载、错误、离线
---
## 1) 登录 / 授权
**结构**
- 渐变背景
- 居中卡片420px 宽)
**组件**
- Logo 组合
- 标题 + 副标题
- OAuth 按钮GitHub、Google
- 隐私说明
**状态**
- Loading按钮 spinner
- Error行内错误
---
## 2) 首页:生成故事
**结构**
- 双栏布局(左表单、右预览)
- 顶部步骤条
**左侧表单**
- 孩子选择器(下拉 + 头像)
- 宇宙选择器(延续 / 新建)
- 关键词标签输入
- 成长主题选择
- 长度选择(分段按钮)
- 生成按钮
**右侧预览**
- 封面占位
- 标题占位
- 摘要预览
- 进度指示(文本 -> 封面 -> 语音)
**状态**
- 空预览
- 生成中(进度)
- 封面失败(重试)
---
## 3) 我的故事(列表)
**结构**
- 工具条 + 网格列表
**工具条**
- 搜索
- 筛选(孩子、标签)
- 排序(最新、最早)
- 视图切换(网格/列表)
**网格卡片**
- 桌面端 3 列
- Hover 操作:阅读、重生成封面、删除
**状态**
- 空列表 + CTA
- 骨架屏
---
## 4) 故事详情
**头图**
- 16:9 封面
- 标题 + 元信息(孩子、宇宙、标签)
- 操作按钮:重生成封面、生成语音、分享
**正文**
- 正文阅读区
- 成就面板
**音频**
- 底部吸附迷你播放器
**状态**
- 封面失败
- 语音未生成
- 语音加载中
---
## 5) 孩子档案
**列表视图**
- 头像卡片网格
- CTA添加档案
**详情视图**
- 头像头部 + 编辑按钮
- Tabs基础信息 / 兴趣与成长 / 故事宇宙 / 阅读记录
**编辑弹窗**
- 姓名、生日、性别
- 兴趣标签
- 成长主题
---
## 6) 故事宇宙
**列表视图**
- 宇宙卡片 + 摘要
- CTA新建宇宙
**详情视图**
- 摘要区
- 分区:主角、角色、世界观、成就
**创建/编辑**
- 结构化表单 + 示例提示
---
## 7) 推送设置
**结构**
- 卡片式设置
**组件**
- 主开关
- 时间选择 + 周期
- 触发开关
- 免打扰时段
- 文案预览
- 测试推送按钮
---
## 8) 账户设置
**组件**
- 个人信息
- OAuth 连接
- 数据导出/删除
- 语言(预留)
---
## 9) 管理后台Providers
**结构**
- 表格布局
**表格列**
- Provider 名称、类型、状态、延迟、最近检查
**操作**
- 编辑、禁用、重载
- JSON 配置编辑器(弹窗)
---
## 10) 404 / 错误 / 空态
**布局**
- 居中插画 + CTA
---
## 交互规范
- 按钮 Hover轻微放大 1.02
- 卡片 Hover抬升阴影
- Toast右上角自动消失
- 列表使用 Skeleton
---
## 响应式规则(移动端阶段)
- 顶部导航 -> 底部 Tab
- 双栏 -> 单栏
- 详情页操作 -> 底部吸附按钮
---
## Figma 搭建清单
- 新建 PageDesign System
- 新建 PageWeb Screens
- 建立颜色与字体样式
- 组件做 Variant
- 全部使用 Auto Layout
- 1440/1200/1024/768 建立栅格
- 状态页复制并标注

View File

@@ -1,299 +0,0 @@
# DreamWeaver Web 高保真原型规范 (v1)
## 范围与目标
- 目标:为 DreamWeaver 提供专业、Web 优先的高保真 UI/UX既温暖有想象力又让家长感到可信与高品质。
- 受众3-8 岁儿童的家长。
- 阶段重点Web 端(桌面/平板),同时制定响应式规则,方便后续移动端迁移。
- 假设:界面语言为简体中文;管理端或系统字段可能含英文。
---
## 设计方向
- 氛围:温柔、治愈、有想象力,但保持简洁与高级感(避免过度幼儿化)。
- 视觉风格:柔和渐变、圆润形状、插画风封面、轻量阴影、舒适中性色。
- UX 原则低阻力、流程清晰、反馈及时、错误可恢复、AI 失败时有明确兜底。
---
## 设计系统Web
### 栅格与布局
- 基准8pt 间距系统。
- 容器1200px 最大宽度,左右 24px 边距12 列栅格。
- 断点:
- 1440+(宽屏)
- 1200标准桌面
- 1024横屏平板
- 768竖屏平板
### 色板
- 主色 600: #6C5CE7
- 主色 500: #7C69FF
- 主色 100: #EAE7FF
- 强调粉: #FF8FB1
- 强调蓝: #65C3FF
- 成功: #34C759
- 警告: #F6A609
- 错误: #FF5A5F
- 中性 900: #1F2430
- 中性 700: #4B5563
- 中性 500: #9AA3B2
- 中性 200: #E5E7EB
- 中性 100: #F5F7FB
- 白色: #FFFFFF
### 字体
- 主体字体PingFang SC, Noto Sans SC, Inter, system-ui
- H132/40Semibold
- H224/32Semibold
- H320/28Semibold
- Body L16/24Regular
- Body M14/22Regular
- Caption12/18Regular
### 圆角与阴影
- 圆角12卡片、10输入框、8按钮、24胶囊标签
- 阴影 S0 4 16 rgba(31,36,48,0.08)
- 阴影 M0 10 30 rgba(31,36,48,0.12)
### 核心组件
- 顶部导航Logo、主 CTA、搜索、孩子切换器、头像菜单
- 侧边导航(可用于设置/管理):图标 + 文案
- 按钮Primary / Secondary / Ghost / Destructive
- 输入:文本、文本域、数字、日期、选择、滑块
- 标签:兴趣/成长主题(多选)
- 卡片故事、孩子、宇宙、Provider
- 弹窗:创建/编辑表单
- Toast成功/错误/提示
- Skeleton列表与故事内容
- 音频播放器:播放/暂停、进度、倍速、下载
- 空态:插画 + CTA
- 错误态:行内错误 + 重试
---
## 信息架构Web
顶级导航:
- 生成故事Home
- 我的故事
- 孩子档案
- 故事宇宙
- 推送设置
- 账户设置
- 管理后台(仅开启时显示)
---
## 页面规格(高保真)
### 1) 登录/授权
**布局**
- 渐变背景 + 居中卡片
- Logo、Slogan、OAuth 按钮
**元素**
- 标题:“欢迎来到 DreamWeaver”
- 副标题:“为孩子生成独一无二的故事”
- 按钮:“使用 GitHub 登录”、“使用 Google 登录”
- 隐私说明:“我们仅使用公开信息创建账户”
**状态**
- Loading按钮 spinner
- Error行内错误 + 重试
---
### 2) 生成故事Home
**布局**
- 左表单 / 右预览双栏
- 顶部步骤条
**主表单**
- 孩子选择器:头像 + 姓名 + 年龄,含“新建档案”入口
- 宇宙选择器:默认“延续上一次”,可切“新建宇宙”
- 关键词输入(标签 + 手输)
- 成长主题选择(可选)
- 故事长度(短/中/长)
- 主要 CTA“生成故事”
**预览面板**
- 标题占位
- 封面占位
- 摘要预览
- 错误态:封面失败提示 + 重试
**交互**
- Stepper档案 → 宇宙 → 关键词 → 生成
- 生成过程:阶段进度(文本/封面/语音)
---
### 3) 我的故事(列表)
**布局**
- 顶部工具条 + 网格列表
**卡片**
- 21:9 封面、标题、标签、所属孩子、更新时间
- Hover 操作:继续阅读、重新生成封面、删除
**筛选**
- 孩子
- 标签
- 时间范围
**空态**
- 插画 + “开始生成第一个故事”
---
### 4) 故事详情
**头图**
- 大封面
- 标题 + 元信息(孩子、宇宙、标签、日期)
- 主操作:生成封面 / 生成语音 / 分享
**内容区**
- 正文排版(舒适行高)
- 成就模块(卡片式)
**音频**
- 滚动时底部吸附播放器
- 倍速切换0.8/1.0/1.2
**状态**
- 封面失败:占位 + 重试
- 语音未生成:显示 CTA
---
### 5) 孩子档案
**列表**
- 头像卡片 + 基础信息
- CTA“添加档案”
**详情**
- 档案头部 + 编辑
- Tabs基础信息 / 兴趣与成长 / 故事宇宙 / 阅读记录
**编辑弹窗**
- 姓名、生日、性别
- 兴趣标签(多选)
- 成长主题(单选或多选)
---
### 6) 故事宇宙
**列表**
- 宇宙卡片:主角、常驻角色、成就数量
- CTA“新建宇宙”
**详情**
- 宇宙摘要
- 可编辑区:主角 / 角色 / 世界观
- 成就时间轴
**创建/编辑**
- 结构化表单 + 示例提示
---
### 7) 推送设置
**布局**
- 卡片式设置区
**设置项**
- 主开关:开启主动推送
- 时间选择 + 周期
- 触发类型(复选)
- 免打扰时段
**预览**
- 推送文案预览
- 测试推送按钮
---
### 8) 账户设置
- 个人信息
- OAuth 绑定
- 数据隐私(导出/删除)
- 语言切换(预留)
---
### 9) 管理后台Provider
**表格**
- Provider 列表:状态、延迟、最近检查
- 操作:编辑、禁用、重载
**详情**
- JSON 配置编辑器(等宽字体)
- 健康检查按钮
---
### 10) 404 / 错误 / 空态
- 友好插画 + 返回 CTA
---
## 交互与动效
- 按钮Hover 轻微放大1.02
- 卡片Hover 提升阴影
- Loading列表 Skeleton、生成进度
- Toast右上角3s 自动消失
---
## 可访问性
- 文本对比度 >= 4.5:1
- 输入框/按钮焦点态清晰
- 最小触控区域 44px
---
## 响应式策略(移动端阶段)
- 顶部导航改为底部 Tab
- 双栏变单栏
- 详情页操作改为底部吸附操作条
- 卡片 1 列展示,触控面积更大
---
## Figma 实现说明
- 全部使用 Auto Layout
- 统一命名Page/Section/Component/State
- 按钮、输入、卡片做 Variant
- 颜色/字体/间距作为样式管理
---
## 交付物
- 设计系统库(色板、文字、组件)
- 全流程高保真页面
- 原型链接Figma 中生成)

View File

@@ -1,59 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>账户设置</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<header class="nav">
<div class="container nav__inner">
<div class="nav__left">
<div class="nav__logo">
<span class="nav__logo-badge"></span>
DreamWeaver
</div>
<span class="badge">Web 原型</span>
</div>
<div class="nav__center">
<a class="nav__item active" href="home.html">生成故事</a>
<a class="nav__item" href="my-stories.html">我的故事</a>
<a class="nav__item" href="child-profiles.html">孩子档案</a>
<a class="nav__item" href="universes.html">故事宇宙</a>
<a class="nav__item" href="push-settings.html">推送设置</a>
<a class="nav__item" href="account-settings.html">账户设置</a>
<a class="nav__item" href="admin-providers.html">管理后台</a>
</div>
<div class="nav__right">
<input class="input" style="width: 200px;" placeholder="搜索故事" />
<div class="avatar"></div>
</div>
</div>
</header>
<div class="container" style="padding: 28px 0 60px;">
<div class="grid grid-2 section">
<div class="card">
<h3>个人信息</h3>
<div class="row section">
<input class="input" placeholder="昵称" value="Dream Parent" />
<input class="input" placeholder="邮箱" value="parent@example.com" />
</div>
<button class="btn btn--primary">保存</button>
</div>
<div class="card">
<h3>账号安全</h3>
<div class="callout">已绑定 GitHub、Google</div>
<button class="btn btn--secondary" style="margin-top: 12px;">管理绑定</button>
</div>
<div class="card">
<h3>数据隐私</h3>
<button class="btn btn--secondary">导出数据</button>
<button class="btn btn--danger" style="margin-top: 12px;">删除账户</button>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,74 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>管理后台 - Providers</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<header class="nav">
<div class="container nav__inner">
<div class="nav__left">
<div class="nav__logo">
<span class="nav__logo-badge"></span>
DreamWeaver
</div>
<span class="badge">Web 原型</span>
</div>
<div class="nav__center">
<a class="nav__item active" href="home.html">生成故事</a>
<a class="nav__item" href="my-stories.html">我的故事</a>
<a class="nav__item" href="child-profiles.html">孩子档案</a>
<a class="nav__item" href="universes.html">故事宇宙</a>
<a class="nav__item" href="push-settings.html">推送设置</a>
<a class="nav__item" href="account-settings.html">账户设置</a>
<a class="nav__item" href="admin-providers.html">管理后台</a>
</div>
<div class="nav__right">
<input class="input" style="width: 200px;" placeholder="搜索故事" />
<div class="avatar"></div>
</div>
</div>
</header>
<div class="container" style="padding: 28px 0 60px;">
<div class="toolbar section">
<h2>Providers 管理</h2>
<button class="btn btn--primary">新增 Provider</button>
</div>
<table class="table">
<thead>
<tr>
<th>名称</th>
<th>类型</th>
<th>状态</th>
<th>延迟</th>
<th>最近检查</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr>
<td>text_primary</td>
<td>Text</td>
<td><span class="badge">健康</span></td>
<td>420ms</td>
<td>2 分钟前</td>
<td><a href="#">编辑</a> · <a href="#">禁用</a> · <a href="#">重载</a></td>
</tr>
<tr>
<td>image_primary</td>
<td>Image</td>
<td><span class="badge">健康</span></td>
<td>860ms</td>
<td>5 分钟前</td>
<td><a href="#">编辑</a> · <a href="#">禁用</a> · <a href="#">重载</a></td>
</tr>
</tbody>
</table>
<div class="footer-note">点击编辑后弹出 JSON 配置编辑器。</div>
</div>
</div>
</body>
</html>

View File

@@ -1,88 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>孩子档案详情</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<header class="nav">
<div class="container nav__inner">
<div class="nav__left">
<div class="nav__logo">
<span class="nav__logo-badge"></span>
DreamWeaver
</div>
<span class="badge">Web 原型</span>
</div>
<div class="nav__center">
<a class="nav__item active" href="home.html">生成故事</a>
<a class="nav__item" href="my-stories.html">我的故事</a>
<a class="nav__item" href="child-profiles.html">孩子档案</a>
<a class="nav__item" href="universes.html">故事宇宙</a>
<a class="nav__item" href="push-settings.html">推送设置</a>
<a class="nav__item" href="account-settings.html">账户设置</a>
<a class="nav__item" href="admin-providers.html">管理后台</a>
</div>
<div class="nav__right">
<input class="input" style="width: 200px;" placeholder="搜索故事" />
<div class="avatar"></div>
</div>
</div>
</header>
<div class="container" style="padding: 28px 0 60px;">
<div class="card section" style="display:flex; align-items:center; justify-content: space-between;">
<div style="display:flex; gap:12px; align-items:center;">
<div class="avatar"></div>
<div>
<div class="card-title">小明 · 5岁</div>
<div class="card-meta">男 · 生日 2020/05/12</div>
</div>
</div>
<button class="btn btn--secondary">编辑档案</button>
</div>
<div class="tabs section">
<div class="tab active">基础信息</div>
<div class="tab">兴趣与成长</div>
<div class="tab">故事宇宙</div>
<div class="tab">阅读记录</div>
</div>
<div class="grid grid-2 section">
<div class="card">
<h3>兴趣标签</h3>
<div class="chips">
<span class="chip selected">太空</span>
<span class="chip selected">机器人</span>
<span class="chip">冒险</span>
</div>
</div>
<div class="card">
<h3>成长主题</h3>
<div class="chips">
<span class="chip selected">勇气</span>
<span class="chip">分享</span>
</div>
</div>
</div>
<div class="section">
<h3>故事宇宙</h3>
<div class="grid grid-2">
<div class="card">
<div class="card-title">星际冒险</div>
<div class="card-meta">主角:小明船长 · 成就 3 个</div>
</div>
<div class="card">
<div class="card-title">梦幻森林</div>
<div class="card-meta">主角:森林守护者 · 成就 1 个</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,58 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>孩子档案</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<header class="nav">
<div class="container nav__inner">
<div class="nav__left">
<div class="nav__logo">
<span class="nav__logo-badge"></span>
DreamWeaver
</div>
<span class="badge">Web 原型</span>
</div>
<div class="nav__center">
<a class="nav__item active" href="home.html">生成故事</a>
<a class="nav__item" href="my-stories.html">我的故事</a>
<a class="nav__item" href="child-profiles.html">孩子档案</a>
<a class="nav__item" href="universes.html">故事宇宙</a>
<a class="nav__item" href="push-settings.html">推送设置</a>
<a class="nav__item" href="account-settings.html">账户设置</a>
<a class="nav__item" href="admin-providers.html">管理后台</a>
</div>
<div class="nav__right">
<input class="input" style="width: 200px;" placeholder="搜索故事" />
<div class="avatar"></div>
</div>
</div>
</header>
<div class="container" style="padding: 28px 0 60px;">
<div class="toolbar section">
<h2>我的宝贝</h2>
<button class="btn btn--primary">添加档案</button>
</div>
<div class="grid grid-3 section">
<div class="card">
<div class="avatar"></div>
<div class="card-title">小明 · 5岁</div>
<div class="chips"><span class="chip">太空</span><span class="chip">机器人</span></div>
</div>
<div class="card">
<div class="avatar"></div>
<div class="card-title">小红 · 3岁</div>
<div class="chips"><span class="chip">公主</span><span class="chip">动物</span></div>
</div>
<div class="card card--flat">
<div class="callout">空态示例:添加一个孩子档案</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,111 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>生成故事</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<header class="nav">
<div class="container nav__inner">
<div class="nav__left">
<div class="nav__logo">
<span class="nav__logo-badge"></span>
DreamWeaver
</div>
<span class="badge">Web 原型</span>
</div>
<div class="nav__center">
<a class="nav__item active" href="home.html">生成故事</a>
<a class="nav__item" href="my-stories.html">我的故事</a>
<a class="nav__item" href="child-profiles.html">孩子档案</a>
<a class="nav__item" href="universes.html">故事宇宙</a>
<a class="nav__item" href="push-settings.html">推送设置</a>
<a class="nav__item" href="account-settings.html">账户设置</a>
<a class="nav__item" href="admin-providers.html">管理后台</a>
</div>
<div class="nav__right">
<input class="input" style="width: 200px;" placeholder="搜索故事" />
<div class="avatar"></div>
</div>
</div>
</header>
<div class="container" style="padding: 28px 0 60px;">
<div class="stepper section">
<span class="step active">档案</span>
<span class="step">宇宙</span>
<span class="step">关键词</span>
<span class="step">生成</span>
</div>
<div class="split section">
<div class="card">
<h3>为谁创作故事</h3>
<div class="row section">
<div>
<label>孩子档案</label>
<select>
<option>小明 · 5岁</option>
<option>小红 · 3岁</option>
</select>
</div>
<div>
<label>故事宇宙</label>
<select>
<option>延续上一次(星际冒险)</option>
<option>新建宇宙</option>
</select>
</div>
</div>
<div class="section">
<label>关键词</label>
<div class="chips section">
<span class="chip selected">太空</span>
<span class="chip selected">勇气</span>
<span class="chip">机器人</span>
<span class="chip">探索</span>
</div>
<input class="input" placeholder="输入更多关键词" />
</div>
<div class="row section">
<div>
<label>成长主题</label>
<select>
<option>勇气</option>
<option>分享</option>
<option>独立</option>
</select>
</div>
<div>
<label>故事长度</label>
<div class="chips">
<span class="chip selected"></span>
<span class="chip"></span>
<span class="chip"></span>
</div>
</div>
</div>
<div class="section">
<button class="btn btn--primary" style="width: 100%;">生成故事</button>
</div>
</div>
<div class="card">
<h3>生成预览</h3>
<div class="card-cover"></div>
<div class="card-title">故事标题占位</div>
<p class="card-meta">故事摘要将显示在这里,支持 2-3 行预览。</p>
<div class="section">
<div class="callout">生成中:文本 → 封面 → 语音</div>
</div>
<div class="section">
<div class="callout" style="border-color: var(--error); color: var(--error);">封面生成失败,稍后重试</div>
<button class="btn btn--secondary" style="margin-top: 8px;">重新生成封面</button>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,33 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DreamWeaver 原型入口</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<div class="container" style="padding: 48px 0;">
<div class="hero">
<h1>DreamWeaver HTML 原型入口</h1>
<p>请选择页面进行导入或预览HTML to Figma</p>
<div class="grid grid-3 section">
<div class="card"><a href="login.html">登录 / 授权</a></div>
<div class="card"><a href="home.html">生成故事Home</a></div>
<div class="card"><a href="my-stories.html">我的故事(列表)</a></div>
<div class="card"><a href="story-detail.html">故事详情</a></div>
<div class="card"><a href="child-profiles.html">孩子档案(列表)</a></div>
<div class="card"><a href="child-profile-detail.html">孩子档案(详情)</a></div>
<div class="card"><a href="universes.html">故事宇宙(列表)</a></div>
<div class="card"><a href="universe-detail.html">故事宇宙(详情)</a></div>
<div class="card"><a href="push-settings.html">推送设置</a></div>
<div class="card"><a href="account-settings.html">账户设置</a></div>
<div class="card"><a href="admin-providers.html">管理后台Providers</a></div>
<div class="card"><a href="not-found.html">404 / 错误</a></div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,28 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>登录 / 授权</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<div class="container" style="padding: 80px 0;">
<div class="hero" style="max-width: 420px; margin: 0 auto; text-align: center;">
<div class="nav__logo" style="justify-content: center;">
<span class="nav__logo-badge"></span>
DreamWeaver
</div>
<h2 style="margin-top: 16px;">欢迎来到 DreamWeaver</h2>
<p>为孩子生成独一无二的故事</p>
<div class="section" style="display: grid; gap: 12px;">
<button class="btn btn--primary">使用 GitHub 登录</button>
<button class="btn btn--secondary">使用 Google 登录</button>
</div>
<div class="footer-note">我们仅使用公开信息创建账户</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,84 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>我的故事</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<header class="nav">
<div class="container nav__inner">
<div class="nav__left">
<div class="nav__logo">
<span class="nav__logo-badge"></span>
DreamWeaver
</div>
<span class="badge">Web 原型</span>
</div>
<div class="nav__center">
<a class="nav__item active" href="home.html">生成故事</a>
<a class="nav__item" href="my-stories.html">我的故事</a>
<a class="nav__item" href="child-profiles.html">孩子档案</a>
<a class="nav__item" href="universes.html">故事宇宙</a>
<a class="nav__item" href="push-settings.html">推送设置</a>
<a class="nav__item" href="account-settings.html">账户设置</a>
<a class="nav__item" href="admin-providers.html">管理后台</a>
</div>
<div class="nav__right">
<input class="input" style="width: 200px;" placeholder="搜索故事" />
<div class="avatar"></div>
</div>
</div>
</header>
<div class="container" style="padding: 28px 0 60px;">
<div class="toolbar section">
<input class="input" style="width: 260px;" placeholder="搜索标题或关键词" />
<select style="width: 160px;">
<option>孩子:全部</option>
<option>小明</option>
<option>小红</option>
</select>
<select style="width: 160px;">
<option>排序:最新</option>
<option>最早</option>
</select>
<button class="btn btn--ghost">网格</button>
<button class="btn btn--ghost">列表</button>
</div>
<div class="grid grid-3 section">
<div class="card">
<div class="card-cover"></div>
<div class="card-title">星际冒险 · 第三章</div>
<div class="chips">
<span class="chip">太空</span><span class="chip">勇气</span>
</div>
<div class="card-meta">小明 · 更新于 2 天前</div>
<div class="section hero-actions">
<button class="btn btn--primary">继续阅读</button>
<button class="btn btn--secondary">重生成封面</button>
</div>
</div>
<div class="card">
<div class="card-cover"></div>
<div class="card-title">梦幻森林 · 朋友篇</div>
<div class="chips">
<span class="chip">友谊</span><span class="chip">动物</span>
</div>
<div class="card-meta">小红 · 更新于 5 天前</div>
<div class="section hero-actions">
<button class="btn btn--primary">继续阅读</button>
<button class="btn btn--danger">删除</button>
</div>
</div>
<div class="card card--flat">
<div class="callout">空态示例:开始生成第一个故事</div>
<button class="btn btn--primary" style="margin-top: 12px;">生成故事</button>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,20 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>404</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<div class="container" style="padding: 80px 0; text-align:center;">
<div class="hero">
<h1>404</h1>
<p>页面走丢了,回到生成故事开始吧。</p>
<button class="btn btn--primary">返回首页</button>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,78 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>推送设置</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<header class="nav">
<div class="container nav__inner">
<div class="nav__left">
<div class="nav__logo">
<span class="nav__logo-badge"></span>
DreamWeaver
</div>
<span class="badge">Web 原型</span>
</div>
<div class="nav__center">
<a class="nav__item active" href="home.html">生成故事</a>
<a class="nav__item" href="my-stories.html">我的故事</a>
<a class="nav__item" href="child-profiles.html">孩子档案</a>
<a class="nav__item" href="universes.html">故事宇宙</a>
<a class="nav__item" href="push-settings.html">推送设置</a>
<a class="nav__item" href="account-settings.html">账户设置</a>
<a class="nav__item" href="admin-providers.html">管理后台</a>
</div>
<div class="nav__right">
<input class="input" style="width: 200px;" placeholder="搜索故事" />
<div class="avatar"></div>
</div>
</div>
</header>
<div class="container" style="padding: 28px 0 60px;">
<div class="grid grid-2 section">
<div class="card">
<h3>主动推送</h3>
<div class="row section">
<div>
<label>主开关</label>
<select>
<option>开启</option>
<option>关闭</option>
</select>
</div>
<div>
<label>推送时间</label>
<input class="input" placeholder="20:00" />
</div>
</div>
<div class="section">
<label>触发类型</label>
<div class="chips">
<span class="chip selected">时间触发</span>
<span class="chip selected">事件触发</span>
<span class="chip">行为触发</span>
<span class="chip">成长触发</span>
</div>
</div>
<div class="section">
<label>免打扰</label>
<div class="row">
<input class="input" placeholder="21:00" />
<input class="input" placeholder="09:00" />
</div>
</div>
</div>
<div class="card">
<h3>推送预览</h3>
<div class="callout">“今晚给小明讲一个关于太空的故事,好吗?”</div>
<button class="btn btn--secondary" style="margin-top: 12px;">发送测试推送</button>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,73 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>故事详情</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<header class="nav">
<div class="container nav__inner">
<div class="nav__left">
<div class="nav__logo">
<span class="nav__logo-badge"></span>
DreamWeaver
</div>
<span class="badge">Web 原型</span>
</div>
<div class="nav__center">
<a class="nav__item active" href="home.html">生成故事</a>
<a class="nav__item" href="my-stories.html">我的故事</a>
<a class="nav__item" href="child-profiles.html">孩子档案</a>
<a class="nav__item" href="universes.html">故事宇宙</a>
<a class="nav__item" href="push-settings.html">推送设置</a>
<a class="nav__item" href="account-settings.html">账户设置</a>
<a class="nav__item" href="admin-providers.html">管理后台</a>
</div>
<div class="nav__right">
<input class="input" style="width: 200px;" placeholder="搜索故事" />
<div class="avatar"></div>
</div>
</div>
</header>
<div class="container" style="padding: 28px 0 60px;">
<div class="section">
<div class="cover-hero"></div>
<h2 style="margin-top: 16px;">星际冒险 · 勇气的种子</h2>
<div class="card-meta">小明 · 星际冒险宇宙 · 2025/01/12</div>
<div class="hero-actions section">
<button class="btn btn--secondary">重新生成封面</button>
<button class="btn btn--primary">生成语音</button>
<button class="btn btn--ghost">分享</button>
</div>
</div>
<div class="split section">
<div class="card">
<h3>故事正文</h3>
<p>夜空像一条温柔的河流,小明驾驶着飞船穿过星光……</p>
<p>他握紧操纵杆,鼓起勇气,向未知的星球靠近。</p>
<p>最终,小明发现了新的朋友,也学会了如何面对黑暗。</p>
</div>
<div class="card">
<h3>成就</h3>
<div class="chips section">
<span class="chip selected">勇气</span>
<span class="chip selected">友谊</span>
</div>
<div class="callout section">“克服了黑暗的恐惧”</div>
<div class="callout">“帮助了迷路的小伙伴”</div>
</div>
</div>
<div class="section audio-player">
<button class="btn btn--ghost">播放</button>
<div class="audio-bar"><div class="audio-progress"></div></div>
<button class="btn btn--ghost">1.0x</button>
</div>
<div class="footer-note">语音未生成时,显示“生成语音”按钮作为主操作。</div>
</div>
</div>
</body>
</html>

View File

@@ -1,217 +0,0 @@
:root {
--primary-600: #6C5CE7;
--primary-500: #7C69FF;
--primary-100: #EAE7FF;
--accent-pink: #FF8FB1;
--accent-sky: #65C3FF;
--success: #34C759;
--warning: #F6A609;
--error: #FF5A5F;
--neutral-900: #1F2430;
--neutral-700: #4B5563;
--neutral-500: #9AA3B2;
--neutral-200: #E5E7EB;
--neutral-100: #F5F7FB;
--hero-gradient: linear-gradient(135deg, #EAE7FF 0%, #FDF6FF 40%, #EAF6FF 100%);
}
* { box-sizing: border-box; }
:root {
--container-width: 1200px;
--gutter: 24px;
--radius-card: 12px;
--radius-input: 10px;
--radius-button: 8px;
--radius-pill: 24px;
--shadow-s: 0 4px 16px rgba(31,36,48,0.08);
--shadow-m: 0 10px 30px rgba(31,36,48,0.12);
}
body {
margin: 0;
font-family: "PingFang SC", "Noto Sans SC", Inter, system-ui, -apple-system, sans-serif;
color: var(--neutral-900);
background: var(--neutral-100);
}
a { color: var(--primary-600); text-decoration: none; }
.page { min-height: 100vh; }
.container {
width: min(var(--container-width), 100% - 48px);
margin: 0 auto;
}
.nav {
background: #fff;
border-bottom: 1px solid var(--neutral-200);
position: sticky;
top: 0;
z-index: 10;
}
.nav__inner {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 0;
gap: 16px;
}
.nav__left, .nav__center, .nav__right { display: flex; align-items: center; gap: 16px; }
.nav__logo {
display: flex;
align-items: center;
gap: 10px;
font-weight: 700;
color: var(--neutral-900);
}
.nav__logo-badge {
width: 28px;
height: 28px;
border-radius: 8px;
background: linear-gradient(135deg, var(--primary-500), var(--accent-sky));
}
.nav__item { color: var(--neutral-700); font-weight: 500; }
.nav__item.active { color: var(--primary-600); }
.hero {
background: var(--hero-gradient);
border-radius: 16px;
padding: 32px;
box-shadow: var(--shadow-s);
}
.section { margin: 28px 0; }
.section-title { font-size: 20px; font-weight: 600; margin-bottom: 12px; }
.grid { display: grid; gap: 16px; }
.grid-2 { grid-template-columns: repeat(2, minmax(0,1fr)); }
.grid-3 { grid-template-columns: repeat(3, minmax(0,1fr)); }
.card {
background: #fff;
border-radius: var(--radius-card);
padding: 16px;
box-shadow: var(--shadow-s);
}
.card--flat { box-shadow: none; border: 1px solid var(--neutral-200); }
.card-cover {
width: 100%;
aspect-ratio: 21 / 9;
border-radius: 10px;
background: linear-gradient(135deg, var(--primary-100), #fff 60%, var(--accent-sky));
margin-bottom: 12px;
}
.card-title { font-weight: 600; margin: 6px 0; }
.card-meta { color: var(--neutral-500); font-size: 12px; }
.badge {
display: inline-flex;
align-items: center;
padding: 4px 10px;
border-radius: var(--radius-pill);
background: var(--primary-100);
color: var(--primary-600);
font-size: 12px;
}
.chips { display: flex; flex-wrap: wrap; gap: 8px; }
.chip {
padding: 6px 12px;
border-radius: var(--radius-pill);
border: 1px solid var(--neutral-200);
background: #fff;
font-size: 12px;
}
.chip.selected { background: var(--primary-100); border-color: var(--primary-500); color: var(--primary-600); }
.btn {
height: 40px;
padding: 0 16px;
border-radius: var(--radius-button);
border: 1px solid transparent;
cursor: pointer;
font-weight: 600;
}
.btn--primary { background: var(--primary-600); color: #fff; }
.btn--secondary { background: #fff; border-color: var(--primary-600); color: var(--primary-600); }
.btn--ghost { background: transparent; color: var(--neutral-700); }
.btn--danger { background: #fff; border-color: var(--error); color: var(--error); }
.input, select, textarea {
width: 100%;
padding: 10px 12px;
border-radius: var(--radius-input);
border: 1px solid var(--neutral-200);
background: #fff;
font-size: 14px;
}
.row { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 12px; }
.stepper { display: flex; gap: 10px; align-items: center; }
.step {
padding: 6px 12px;
border-radius: var(--radius-pill);
background: var(--neutral-100);
color: var(--neutral-700);
font-size: 12px;
}
.step.active { background: var(--primary-100); color: var(--primary-600); }
.toolbar { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
.table { width: 100%; border-collapse: collapse; }
.table th, .table td { border-bottom: 1px solid var(--neutral-200); padding: 12px 8px; text-align: left; font-size: 14px; }
.avatar {
width: 40px; height: 40px; border-radius: 50%;
background: var(--primary-100);
display: inline-flex; align-items: center; justify-content: center;
font-weight: 700; color: var(--primary-600);
}
.cover-hero {
aspect-ratio: 16 / 9;
border-radius: 14px;
background: linear-gradient(135deg, var(--primary-100), #fff 55%, var(--accent-pink));
}
.audio-player {
display: flex; align-items: center; gap: 12px; padding: 12px 16px;
border-radius: 12px; border: 1px solid var(--neutral-200); background: #fff;
}
.audio-bar { height: 6px; background: var(--neutral-200); border-radius: 999px; flex: 1; }
.audio-progress { width: 35%; height: 100%; background: var(--primary-600); border-radius: 999px; }
.tabs { display: flex; gap: 8px; border-bottom: 1px solid var(--neutral-200); }
.tab { padding: 10px 12px; color: var(--neutral-700); }
.tab.active { color: var(--primary-600); border-bottom: 2px solid var(--primary-600); }
.callout {
background: #fff;
border: 1px dashed var(--neutral-200);
border-radius: 12px;
padding: 12px;
color: var(--neutral-700);
font-size: 12px;
}
.footer-note { color: var(--neutral-500); font-size: 12px; margin-top: 12px; }
.hero-actions { display: flex; gap: 12px; flex-wrap: wrap; }
.split {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 24px;
}
@media (max-width: 1024px) {
.grid-3 { grid-template-columns: repeat(2, minmax(0,1fr)); }
.split { grid-template-columns: 1fr; }
}
@media (max-width: 768px) {
.grid-2 { grid-template-columns: 1fr; }
.grid-3 { grid-template-columns: 1fr; }
.row { grid-template-columns: 1fr; }
}

View File

@@ -1,64 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>宇宙详情</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<header class="nav">
<div class="container nav__inner">
<div class="nav__left">
<div class="nav__logo">
<span class="nav__logo-badge"></span>
DreamWeaver
</div>
<span class="badge">Web 原型</span>
</div>
<div class="nav__center">
<a class="nav__item active" href="home.html">生成故事</a>
<a class="nav__item" href="my-stories.html">我的故事</a>
<a class="nav__item" href="child-profiles.html">孩子档案</a>
<a class="nav__item" href="universes.html">故事宇宙</a>
<a class="nav__item" href="push-settings.html">推送设置</a>
<a class="nav__item" href="account-settings.html">账户设置</a>
<a class="nav__item" href="admin-providers.html">管理后台</a>
</div>
<div class="nav__right">
<input class="input" style="width: 200px;" placeholder="搜索故事" />
<div class="avatar"></div>
</div>
</div>
</header>
<div class="container" style="padding: 28px 0 60px;">
<div class="card section">
<h2>星际冒险</h2>
<div class="card-meta">主角:小明船长 · 更新于 2025/01/12</div>
</div>
<div class="grid grid-2 section">
<div class="card">
<h3>主角设定</h3>
<div class="callout">小明是来自地球的探险家,勇敢且好奇。</div>
</div>
<div class="card">
<h3>常驻角色</h3>
<div class="callout">机器人小七、外星猫咪星星</div>
</div>
<div class="card">
<h3>世界观</h3>
<div class="callout">星际学院、彩虹星云、飞船港湾</div>
</div>
<div class="card">
<h3>成就</h3>
<div class="callout">克服恐惧 · 结交朋友 · 学会独立</div>
</div>
</div>
<div class="section">
<button class="btn btn--secondary">编辑宇宙</button>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,64 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>故事宇宙</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<header class="nav">
<div class="container nav__inner">
<div class="nav__left">
<div class="nav__logo">
<span class="nav__logo-badge"></span>
DreamWeaver
</div>
<span class="badge">Web 原型</span>
</div>
<div class="nav__center">
<a class="nav__item active" href="home.html">生成故事</a>
<a class="nav__item" href="my-stories.html">我的故事</a>
<a class="nav__item" href="child-profiles.html">孩子档案</a>
<a class="nav__item" href="universes.html">故事宇宙</a>
<a class="nav__item" href="push-settings.html">推送设置</a>
<a class="nav__item" href="account-settings.html">账户设置</a>
<a class="nav__item" href="admin-providers.html">管理后台</a>
</div>
<div class="nav__right">
<input class="input" style="width: 200px;" placeholder="搜索故事" />
<div class="avatar"></div>
</div>
</div>
</header>
<div class="container" style="padding: 28px 0 60px;">
<div class="toolbar section">
<h2>故事宇宙</h2>
<button class="btn btn--primary">新建宇宙</button>
</div>
<div class="grid grid-3 section">
<div class="card">
<div class="card-title">星际冒险</div>
<div class="card-meta">主角:小明船长</div>
<div class="chips section">
<span class="chip">伙伴:机器人小七</span>
<span class="chip">成就3</span>
</div>
</div>
<div class="card">
<div class="card-title">梦幻森林</div>
<div class="card-meta">主角:森林守护者</div>
<div class="chips section">
<span class="chip">伙伴:魔法猫咪</span>
<span class="chip">成就1</span>
</div>
</div>
<div class="card card--flat">
<div class="callout">空态示例:创建第一个宇宙</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,59 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>账户设置</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<header class="nav">
<div class="container nav__inner">
<div class="nav__left">
<div class="nav__logo">
<span class="nav__logo-badge"></span>
DreamWeaver
</div>
<span class="badge">Web 原型</span>
</div>
<div class="nav__center">
<a class="nav__item active" href="home.html">生成故事</a>
<a class="nav__item" href="my-stories.html">我的故事</a>
<a class="nav__item" href="child-profiles.html">孩子档案</a>
<a class="nav__item" href="universes.html">故事宇宙</a>
<a class="nav__item" href="push-settings.html">推送设置</a>
<a class="nav__item" href="account-settings.html">账户设置</a>
<a class="nav__item" href="admin-providers.html">管理后台</a>
</div>
<div class="nav__right">
<input class="input" style="width: 200px;" placeholder="搜索故事" />
<div class="avatar"></div>
</div>
</div>
</header>
<div class="container" style="padding: 28px 0 60px;">
<div class="grid grid-2 section">
<div class="card">
<h3>个人信息</h3>
<div class="row section">
<input class="input" placeholder="昵称" value="Dream Parent" />
<input class="input" placeholder="邮箱" value="parent@example.com" />
</div>
<button class="btn btn--primary">保存</button>
</div>
<div class="card">
<h3>账号安全</h3>
<div class="callout">已绑定 GitHub、Google</div>
<button class="btn btn--secondary" style="margin-top: 12px;">管理绑定</button>
</div>
<div class="card">
<h3>数据隐私</h3>
<button class="btn btn--secondary">导出数据</button>
<button class="btn btn--danger" style="margin-top: 12px;">删除账户</button>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,74 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>管理后台 - Providers</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<header class="nav">
<div class="container nav__inner">
<div class="nav__left">
<div class="nav__logo">
<span class="nav__logo-badge"></span>
DreamWeaver
</div>
<span class="badge">Web 原型</span>
</div>
<div class="nav__center">
<a class="nav__item active" href="home.html">生成故事</a>
<a class="nav__item" href="my-stories.html">我的故事</a>
<a class="nav__item" href="child-profiles.html">孩子档案</a>
<a class="nav__item" href="universes.html">故事宇宙</a>
<a class="nav__item" href="push-settings.html">推送设置</a>
<a class="nav__item" href="account-settings.html">账户设置</a>
<a class="nav__item" href="admin-providers.html">管理后台</a>
</div>
<div class="nav__right">
<input class="input" style="width: 200px;" placeholder="搜索故事" />
<div class="avatar"></div>
</div>
</div>
</header>
<div class="container" style="padding: 28px 0 60px;">
<div class="toolbar section">
<h2>Providers 管理</h2>
<button class="btn btn--primary">新增 Provider</button>
</div>
<table class="table">
<thead>
<tr>
<th>名称</th>
<th>类型</th>
<th>状态</th>
<th>延迟</th>
<th>最近检查</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr>
<td>text_primary</td>
<td>Text</td>
<td><span class="badge">健康</span></td>
<td>420ms</td>
<td>2 分钟前</td>
<td><a href="#">编辑</a> · <a href="#">禁用</a> · <a href="#">重载</a></td>
</tr>
<tr>
<td>image_primary</td>
<td>Image</td>
<td><span class="badge">健康</span></td>
<td>860ms</td>
<td>5 分钟前</td>
<td><a href="#">编辑</a> · <a href="#">禁用</a> · <a href="#">重载</a></td>
</tr>
</tbody>
</table>
<div class="footer-note">点击编辑后弹出 JSON 配置编辑器。</div>
</div>
</div>
</body>
</html>

View File

@@ -1,88 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>孩子档案详情</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<header class="nav">
<div class="container nav__inner">
<div class="nav__left">
<div class="nav__logo">
<span class="nav__logo-badge"></span>
DreamWeaver
</div>
<span class="badge">Web 原型</span>
</div>
<div class="nav__center">
<a class="nav__item active" href="home.html">生成故事</a>
<a class="nav__item" href="my-stories.html">我的故事</a>
<a class="nav__item" href="child-profiles.html">孩子档案</a>
<a class="nav__item" href="universes.html">故事宇宙</a>
<a class="nav__item" href="push-settings.html">推送设置</a>
<a class="nav__item" href="account-settings.html">账户设置</a>
<a class="nav__item" href="admin-providers.html">管理后台</a>
</div>
<div class="nav__right">
<input class="input" style="width: 200px;" placeholder="搜索故事" />
<div class="avatar"></div>
</div>
</div>
</header>
<div class="container" style="padding: 28px 0 60px;">
<div class="card section" style="display:flex; align-items:center; justify-content: space-between;">
<div style="display:flex; gap:12px; align-items:center;">
<div class="avatar"></div>
<div>
<div class="card-title">小明 · 5岁</div>
<div class="card-meta">男 · 生日 2020/05/12</div>
</div>
</div>
<button class="btn btn--secondary">编辑档案</button>
</div>
<div class="tabs section">
<div class="tab active">基础信息</div>
<div class="tab">兴趣与成长</div>
<div class="tab">故事宇宙</div>
<div class="tab">阅读记录</div>
</div>
<div class="grid grid-2 section">
<div class="card">
<h3>兴趣标签</h3>
<div class="chips">
<span class="chip selected">太空</span>
<span class="chip selected">机器人</span>
<span class="chip">冒险</span>
</div>
</div>
<div class="card">
<h3>成长主题</h3>
<div class="chips">
<span class="chip selected">勇气</span>
<span class="chip">分享</span>
</div>
</div>
</div>
<div class="section">
<h3>故事宇宙</h3>
<div class="grid grid-2">
<div class="card">
<div class="card-title">星际冒险</div>
<div class="card-meta">主角:小明船长 · 成就 3 个</div>
</div>
<div class="card">
<div class="card-title">梦幻森林</div>
<div class="card-meta">主角:森林守护者 · 成就 1 个</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,58 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>孩子档案</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<header class="nav">
<div class="container nav__inner">
<div class="nav__left">
<div class="nav__logo">
<span class="nav__logo-badge"></span>
DreamWeaver
</div>
<span class="badge">Web 原型</span>
</div>
<div class="nav__center">
<a class="nav__item active" href="home.html">生成故事</a>
<a class="nav__item" href="my-stories.html">我的故事</a>
<a class="nav__item" href="child-profiles.html">孩子档案</a>
<a class="nav__item" href="universes.html">故事宇宙</a>
<a class="nav__item" href="push-settings.html">推送设置</a>
<a class="nav__item" href="account-settings.html">账户设置</a>
<a class="nav__item" href="admin-providers.html">管理后台</a>
</div>
<div class="nav__right">
<input class="input" style="width: 200px;" placeholder="搜索故事" />
<div class="avatar"></div>
</div>
</div>
</header>
<div class="container" style="padding: 28px 0 60px;">
<div class="toolbar section">
<h2>我的宝贝</h2>
<button class="btn btn--primary">添加档案</button>
</div>
<div class="grid grid-3 section">
<div class="card">
<div class="avatar"></div>
<div class="card-title">小明 · 5岁</div>
<div class="chips"><span class="chip">太空</span><span class="chip">机器人</span></div>
</div>
<div class="card">
<div class="avatar"></div>
<div class="card-title">小红 · 3岁</div>
<div class="chips"><span class="chip">公主</span><span class="chip">动物</span></div>
</div>
<div class="card card--flat">
<div class="callout">空态示例:添加一个孩子档案</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,111 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>生成故事</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<header class="nav">
<div class="container nav__inner">
<div class="nav__left">
<div class="nav__logo">
<span class="nav__logo-badge"></span>
DreamWeaver
</div>
<span class="badge">Web 原型</span>
</div>
<div class="nav__center">
<a class="nav__item active" href="home.html">生成故事</a>
<a class="nav__item" href="my-stories.html">我的故事</a>
<a class="nav__item" href="child-profiles.html">孩子档案</a>
<a class="nav__item" href="universes.html">故事宇宙</a>
<a class="nav__item" href="push-settings.html">推送设置</a>
<a class="nav__item" href="account-settings.html">账户设置</a>
<a class="nav__item" href="admin-providers.html">管理后台</a>
</div>
<div class="nav__right">
<input class="input" style="width: 200px;" placeholder="搜索故事" />
<div class="avatar"></div>
</div>
</div>
</header>
<div class="container" style="padding: 28px 0 60px;">
<div class="stepper section">
<span class="step active">档案</span>
<span class="step">宇宙</span>
<span class="step">关键词</span>
<span class="step">生成</span>
</div>
<div class="split section">
<div class="card">
<h3>为谁创作故事</h3>
<div class="row section">
<div>
<label>孩子档案</label>
<select>
<option>小明 · 5岁</option>
<option>小红 · 3岁</option>
</select>
</div>
<div>
<label>故事宇宙</label>
<select>
<option>延续上一次(星际冒险)</option>
<option>新建宇宙</option>
</select>
</div>
</div>
<div class="section">
<label>关键词</label>
<div class="chips section">
<span class="chip selected">太空</span>
<span class="chip selected">勇气</span>
<span class="chip">机器人</span>
<span class="chip">探索</span>
</div>
<input class="input" placeholder="输入更多关键词" />
</div>
<div class="row section">
<div>
<label>成长主题</label>
<select>
<option>勇气</option>
<option>分享</option>
<option>独立</option>
</select>
</div>
<div>
<label>故事长度</label>
<div class="chips">
<span class="chip selected"></span>
<span class="chip"></span>
<span class="chip"></span>
</div>
</div>
</div>
<div class="section">
<button class="btn btn--primary" style="width: 100%;">生成故事</button>
</div>
</div>
<div class="card">
<h3>生成预览</h3>
<div class="card-cover"></div>
<div class="card-title">故事标题占位</div>
<p class="card-meta">故事摘要将显示在这里,支持 2-3 行预览。</p>
<div class="section">
<div class="callout">生成中:文本 → 封面 → 语音</div>
</div>
<div class="section">
<div class="callout" style="border-color: var(--error); color: var(--error);">封面生成失败,稍后重试</div>
<button class="btn btn--secondary" style="margin-top: 8px;">重新生成封面</button>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,33 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DreamWeaver 原型入口</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<div class="container" style="padding: 48px 0;">
<div class="hero">
<h1>DreamWeaver HTML 原型入口</h1>
<p>请选择页面进行导入或预览HTML to Figma</p>
<div class="grid grid-3 section">
<div class="card"><a href="login.html">登录 / 授权</a></div>
<div class="card"><a href="home.html">生成故事Home</a></div>
<div class="card"><a href="my-stories.html">我的故事(列表)</a></div>
<div class="card"><a href="story-detail.html">故事详情</a></div>
<div class="card"><a href="child-profiles.html">孩子档案(列表)</a></div>
<div class="card"><a href="child-profile-detail.html">孩子档案(详情)</a></div>
<div class="card"><a href="universes.html">故事宇宙(列表)</a></div>
<div class="card"><a href="universe-detail.html">故事宇宙(详情)</a></div>
<div class="card"><a href="push-settings.html">推送设置</a></div>
<div class="card"><a href="account-settings.html">账户设置</a></div>
<div class="card"><a href="admin-providers.html">管理后台Providers</a></div>
<div class="card"><a href="not-found.html">404 / 错误</a></div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,28 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>登录 / 授权</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<div class="container" style="padding: 80px 0;">
<div class="hero" style="max-width: 420px; margin: 0 auto; text-align: center;">
<div class="nav__logo" style="justify-content: center;">
<span class="nav__logo-badge"></span>
DreamWeaver
</div>
<h2 style="margin-top: 16px;">欢迎来到 DreamWeaver</h2>
<p>为孩子生成独一无二的故事</p>
<div class="section" style="display: grid; gap: 12px;">
<button class="btn btn--primary">使用 GitHub 登录</button>
<button class="btn btn--secondary">使用 Google 登录</button>
</div>
<div class="footer-note">我们仅使用公开信息创建账户</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,84 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>我的故事</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<header class="nav">
<div class="container nav__inner">
<div class="nav__left">
<div class="nav__logo">
<span class="nav__logo-badge"></span>
DreamWeaver
</div>
<span class="badge">Web 原型</span>
</div>
<div class="nav__center">
<a class="nav__item active" href="home.html">生成故事</a>
<a class="nav__item" href="my-stories.html">我的故事</a>
<a class="nav__item" href="child-profiles.html">孩子档案</a>
<a class="nav__item" href="universes.html">故事宇宙</a>
<a class="nav__item" href="push-settings.html">推送设置</a>
<a class="nav__item" href="account-settings.html">账户设置</a>
<a class="nav__item" href="admin-providers.html">管理后台</a>
</div>
<div class="nav__right">
<input class="input" style="width: 200px;" placeholder="搜索故事" />
<div class="avatar"></div>
</div>
</div>
</header>
<div class="container" style="padding: 28px 0 60px;">
<div class="toolbar section">
<input class="input" style="width: 260px;" placeholder="搜索标题或关键词" />
<select style="width: 160px;">
<option>孩子:全部</option>
<option>小明</option>
<option>小红</option>
</select>
<select style="width: 160px;">
<option>排序:最新</option>
<option>最早</option>
</select>
<button class="btn btn--ghost">网格</button>
<button class="btn btn--ghost">列表</button>
</div>
<div class="grid grid-3 section">
<div class="card">
<div class="card-cover"></div>
<div class="card-title">星际冒险 · 第三章</div>
<div class="chips">
<span class="chip">太空</span><span class="chip">勇气</span>
</div>
<div class="card-meta">小明 · 更新于 2 天前</div>
<div class="section hero-actions">
<button class="btn btn--primary">继续阅读</button>
<button class="btn btn--secondary">重生成封面</button>
</div>
</div>
<div class="card">
<div class="card-cover"></div>
<div class="card-title">梦幻森林 · 朋友篇</div>
<div class="chips">
<span class="chip">友谊</span><span class="chip">动物</span>
</div>
<div class="card-meta">小红 · 更新于 5 天前</div>
<div class="section hero-actions">
<button class="btn btn--primary">继续阅读</button>
<button class="btn btn--danger">删除</button>
</div>
</div>
<div class="card card--flat">
<div class="callout">空态示例:开始生成第一个故事</div>
<button class="btn btn--primary" style="margin-top: 12px;">生成故事</button>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,20 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>404</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<div class="container" style="padding: 80px 0; text-align:center;">
<div class="hero">
<h1>404</h1>
<p>页面走丢了,回到生成故事开始吧。</p>
<button class="btn btn--primary">返回首页</button>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,78 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>推送设置</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<header class="nav">
<div class="container nav__inner">
<div class="nav__left">
<div class="nav__logo">
<span class="nav__logo-badge"></span>
DreamWeaver
</div>
<span class="badge">Web 原型</span>
</div>
<div class="nav__center">
<a class="nav__item active" href="home.html">生成故事</a>
<a class="nav__item" href="my-stories.html">我的故事</a>
<a class="nav__item" href="child-profiles.html">孩子档案</a>
<a class="nav__item" href="universes.html">故事宇宙</a>
<a class="nav__item" href="push-settings.html">推送设置</a>
<a class="nav__item" href="account-settings.html">账户设置</a>
<a class="nav__item" href="admin-providers.html">管理后台</a>
</div>
<div class="nav__right">
<input class="input" style="width: 200px;" placeholder="搜索故事" />
<div class="avatar"></div>
</div>
</div>
</header>
<div class="container" style="padding: 28px 0 60px;">
<div class="grid grid-2 section">
<div class="card">
<h3>主动推送</h3>
<div class="row section">
<div>
<label>主开关</label>
<select>
<option>开启</option>
<option>关闭</option>
</select>
</div>
<div>
<label>推送时间</label>
<input class="input" placeholder="20:00" />
</div>
</div>
<div class="section">
<label>触发类型</label>
<div class="chips">
<span class="chip selected">时间触发</span>
<span class="chip selected">事件触发</span>
<span class="chip">行为触发</span>
<span class="chip">成长触发</span>
</div>
</div>
<div class="section">
<label>免打扰</label>
<div class="row">
<input class="input" placeholder="21:00" />
<input class="input" placeholder="09:00" />
</div>
</div>
</div>
<div class="card">
<h3>推送预览</h3>
<div class="callout">“今晚给小明讲一个关于太空的故事,好吗?”</div>
<button class="btn btn--secondary" style="margin-top: 12px;">发送测试推送</button>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,73 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>故事详情</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<header class="nav">
<div class="container nav__inner">
<div class="nav__left">
<div class="nav__logo">
<span class="nav__logo-badge"></span>
DreamWeaver
</div>
<span class="badge">Web 原型</span>
</div>
<div class="nav__center">
<a class="nav__item active" href="home.html">生成故事</a>
<a class="nav__item" href="my-stories.html">我的故事</a>
<a class="nav__item" href="child-profiles.html">孩子档案</a>
<a class="nav__item" href="universes.html">故事宇宙</a>
<a class="nav__item" href="push-settings.html">推送设置</a>
<a class="nav__item" href="account-settings.html">账户设置</a>
<a class="nav__item" href="admin-providers.html">管理后台</a>
</div>
<div class="nav__right">
<input class="input" style="width: 200px;" placeholder="搜索故事" />
<div class="avatar"></div>
</div>
</div>
</header>
<div class="container" style="padding: 28px 0 60px;">
<div class="section">
<div class="cover-hero"></div>
<h2 style="margin-top: 16px;">星际冒险 · 勇气的种子</h2>
<div class="card-meta">小明 · 星际冒险宇宙 · 2025/01/12</div>
<div class="hero-actions section">
<button class="btn btn--secondary">重新生成封面</button>
<button class="btn btn--primary">生成语音</button>
<button class="btn btn--ghost">分享</button>
</div>
</div>
<div class="split section">
<div class="card">
<h3>故事正文</h3>
<p>夜空像一条温柔的河流,小明驾驶着飞船穿过星光……</p>
<p>他握紧操纵杆,鼓起勇气,向未知的星球靠近。</p>
<p>最终,小明发现了新的朋友,也学会了如何面对黑暗。</p>
</div>
<div class="card">
<h3>成就</h3>
<div class="chips section">
<span class="chip selected">勇气</span>
<span class="chip selected">友谊</span>
</div>
<div class="callout section">“克服了黑暗的恐惧”</div>
<div class="callout">“帮助了迷路的小伙伴”</div>
</div>
</div>
<div class="section audio-player">
<button class="btn btn--ghost">播放</button>
<div class="audio-bar"><div class="audio-progress"></div></div>
<button class="btn btn--ghost">1.0x</button>
</div>
<div class="footer-note">语音未生成时,显示“生成语音”按钮作为主操作。</div>
</div>
</div>
</body>
</html>

View File

@@ -1,217 +0,0 @@
:root {
--primary-600: #3B82F6;
--primary-500: #60A5FA;
--primary-100: #DBEAFE;
--accent-pink: #F5C542;
--accent-sky: #6EE7B7;
--success: #34C759;
--warning: #F6A609;
--error: #FF5A5F;
--neutral-900: #111827;
--neutral-700: #374151;
--neutral-500: #9CA3AF;
--neutral-200: #E5E7EB;
--neutral-100: #F9FAFB;
--hero-gradient: linear-gradient(180deg, #F9FAFB 0%, #EEF2FF 100%);
}
* { box-sizing: border-box; }
:root {
--container-width: 1200px;
--gutter: 24px;
--radius-card: 12px;
--radius-input: 10px;
--radius-button: 8px;
--radius-pill: 24px;
--shadow-s: 0 4px 16px rgba(31,36,48,0.08);
--shadow-m: 0 10px 30px rgba(31,36,48,0.12);
}
body {
margin: 0;
font-family: "PingFang SC", "Noto Sans SC", Inter, system-ui, -apple-system, sans-serif;
color: var(--neutral-900);
background: var(--neutral-100);
}
a { color: var(--primary-600); text-decoration: none; }
.page { min-height: 100vh; }
.container {
width: min(var(--container-width), 100% - 48px);
margin: 0 auto;
}
.nav {
background: #fff;
border-bottom: 1px solid var(--neutral-200);
position: sticky;
top: 0;
z-index: 10;
}
.nav__inner {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 0;
gap: 16px;
}
.nav__left, .nav__center, .nav__right { display: flex; align-items: center; gap: 16px; }
.nav__logo {
display: flex;
align-items: center;
gap: 10px;
font-weight: 700;
color: var(--neutral-900);
}
.nav__logo-badge {
width: 28px;
height: 28px;
border-radius: 8px;
background: linear-gradient(135deg, var(--primary-500), var(--accent-sky));
}
.nav__item { color: var(--neutral-700); font-weight: 500; }
.nav__item.active { color: var(--primary-600); }
.hero {
background: var(--hero-gradient);
border-radius: 16px;
padding: 32px;
box-shadow: var(--shadow-s);
}
.section { margin: 28px 0; }
.section-title { font-size: 20px; font-weight: 600; margin-bottom: 12px; }
.grid { display: grid; gap: 16px; }
.grid-2 { grid-template-columns: repeat(2, minmax(0,1fr)); }
.grid-3 { grid-template-columns: repeat(3, minmax(0,1fr)); }
.card {
background: #fff;
border-radius: var(--radius-card);
padding: 16px;
box-shadow: var(--shadow-s);
}
.card--flat { box-shadow: none; border: 1px solid var(--neutral-200); }
.card-cover {
width: 100%;
aspect-ratio: 21 / 9;
border-radius: 10px;
background: linear-gradient(135deg, var(--primary-100), #fff 60%, var(--accent-sky));
margin-bottom: 12px;
}
.card-title { font-weight: 600; margin: 6px 0; }
.card-meta { color: var(--neutral-500); font-size: 12px; }
.badge {
display: inline-flex;
align-items: center;
padding: 4px 10px;
border-radius: var(--radius-pill);
background: var(--primary-100);
color: var(--primary-600);
font-size: 12px;
}
.chips { display: flex; flex-wrap: wrap; gap: 8px; }
.chip {
padding: 6px 12px;
border-radius: var(--radius-pill);
border: 1px solid var(--neutral-200);
background: #fff;
font-size: 12px;
}
.chip.selected { background: var(--primary-100); border-color: var(--primary-500); color: var(--primary-600); }
.btn {
height: 40px;
padding: 0 16px;
border-radius: var(--radius-button);
border: 1px solid transparent;
cursor: pointer;
font-weight: 600;
}
.btn--primary { background: var(--primary-600); color: #fff; }
.btn--secondary { background: #fff; border-color: var(--primary-600); color: var(--primary-600); }
.btn--ghost { background: transparent; color: var(--neutral-700); }
.btn--danger { background: #fff; border-color: var(--error); color: var(--error); }
.input, select, textarea {
width: 100%;
padding: 10px 12px;
border-radius: var(--radius-input);
border: 1px solid var(--neutral-200);
background: #fff;
font-size: 14px;
}
.row { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 12px; }
.stepper { display: flex; gap: 10px; align-items: center; }
.step {
padding: 6px 12px;
border-radius: var(--radius-pill);
background: var(--neutral-100);
color: var(--neutral-700);
font-size: 12px;
}
.step.active { background: var(--primary-100); color: var(--primary-600); }
.toolbar { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
.table { width: 100%; border-collapse: collapse; }
.table th, .table td { border-bottom: 1px solid var(--neutral-200); padding: 12px 8px; text-align: left; font-size: 14px; }
.avatar {
width: 40px; height: 40px; border-radius: 50%;
background: var(--primary-100);
display: inline-flex; align-items: center; justify-content: center;
font-weight: 700; color: var(--primary-600);
}
.cover-hero {
aspect-ratio: 16 / 9;
border-radius: 14px;
background: linear-gradient(135deg, var(--primary-100), #fff 55%, var(--accent-pink));
}
.audio-player {
display: flex; align-items: center; gap: 12px; padding: 12px 16px;
border-radius: 12px; border: 1px solid var(--neutral-200); background: #fff;
}
.audio-bar { height: 6px; background: var(--neutral-200); border-radius: 999px; flex: 1; }
.audio-progress { width: 35%; height: 100%; background: var(--primary-600); border-radius: 999px; }
.tabs { display: flex; gap: 8px; border-bottom: 1px solid var(--neutral-200); }
.tab { padding: 10px 12px; color: var(--neutral-700); }
.tab.active { color: var(--primary-600); border-bottom: 2px solid var(--primary-600); }
.callout {
background: #fff;
border: 1px dashed var(--neutral-200);
border-radius: 12px;
padding: 12px;
color: var(--neutral-700);
font-size: 12px;
}
.footer-note { color: var(--neutral-500); font-size: 12px; margin-top: 12px; }
.hero-actions { display: flex; gap: 12px; flex-wrap: wrap; }
.split {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 24px;
}
@media (max-width: 1024px) {
.grid-3 { grid-template-columns: repeat(2, minmax(0,1fr)); }
.split { grid-template-columns: 1fr; }
}
@media (max-width: 768px) {
.grid-2 { grid-template-columns: 1fr; }
.grid-3 { grid-template-columns: 1fr; }
.row { grid-template-columns: 1fr; }
}

View File

@@ -1,64 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>宇宙详情</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<header class="nav">
<div class="container nav__inner">
<div class="nav__left">
<div class="nav__logo">
<span class="nav__logo-badge"></span>
DreamWeaver
</div>
<span class="badge">Web 原型</span>
</div>
<div class="nav__center">
<a class="nav__item active" href="home.html">生成故事</a>
<a class="nav__item" href="my-stories.html">我的故事</a>
<a class="nav__item" href="child-profiles.html">孩子档案</a>
<a class="nav__item" href="universes.html">故事宇宙</a>
<a class="nav__item" href="push-settings.html">推送设置</a>
<a class="nav__item" href="account-settings.html">账户设置</a>
<a class="nav__item" href="admin-providers.html">管理后台</a>
</div>
<div class="nav__right">
<input class="input" style="width: 200px;" placeholder="搜索故事" />
<div class="avatar"></div>
</div>
</div>
</header>
<div class="container" style="padding: 28px 0 60px;">
<div class="card section">
<h2>星际冒险</h2>
<div class="card-meta">主角:小明船长 · 更新于 2025/01/12</div>
</div>
<div class="grid grid-2 section">
<div class="card">
<h3>主角设定</h3>
<div class="callout">小明是来自地球的探险家,勇敢且好奇。</div>
</div>
<div class="card">
<h3>常驻角色</h3>
<div class="callout">机器人小七、外星猫咪星星</div>
</div>
<div class="card">
<h3>世界观</h3>
<div class="callout">星际学院、彩虹星云、飞船港湾</div>
</div>
<div class="card">
<h3>成就</h3>
<div class="callout">克服恐惧 · 结交朋友 · 学会独立</div>
</div>
</div>
<div class="section">
<button class="btn btn--secondary">编辑宇宙</button>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,64 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>故事宇宙</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<header class="nav">
<div class="container nav__inner">
<div class="nav__left">
<div class="nav__logo">
<span class="nav__logo-badge"></span>
DreamWeaver
</div>
<span class="badge">Web 原型</span>
</div>
<div class="nav__center">
<a class="nav__item active" href="home.html">生成故事</a>
<a class="nav__item" href="my-stories.html">我的故事</a>
<a class="nav__item" href="child-profiles.html">孩子档案</a>
<a class="nav__item" href="universes.html">故事宇宙</a>
<a class="nav__item" href="push-settings.html">推送设置</a>
<a class="nav__item" href="account-settings.html">账户设置</a>
<a class="nav__item" href="admin-providers.html">管理后台</a>
</div>
<div class="nav__right">
<input class="input" style="width: 200px;" placeholder="搜索故事" />
<div class="avatar"></div>
</div>
</div>
</header>
<div class="container" style="padding: 28px 0 60px;">
<div class="toolbar section">
<h2>故事宇宙</h2>
<button class="btn btn--primary">新建宇宙</button>
</div>
<div class="grid grid-3 section">
<div class="card">
<div class="card-title">星际冒险</div>
<div class="card-meta">主角:小明船长</div>
<div class="chips section">
<span class="chip">伙伴:机器人小七</span>
<span class="chip">成就3</span>
</div>
</div>
<div class="card">
<div class="card-title">梦幻森林</div>
<div class="card-meta">主角:森林守护者</div>
<div class="chips section">
<span class="chip">伙伴:魔法猫咪</span>
<span class="chip">成就1</span>
</div>
</div>
<div class="card card--flat">
<div class="callout">空态示例:创建第一个宇宙</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,59 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>账户设置</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<header class="nav">
<div class="container nav__inner">
<div class="nav__left">
<div class="nav__logo">
<span class="nav__logo-badge"></span>
DreamWeaver
</div>
<span class="badge">Web 原型</span>
</div>
<div class="nav__center">
<a class="nav__item active" href="home.html">生成故事</a>
<a class="nav__item" href="my-stories.html">我的故事</a>
<a class="nav__item" href="child-profiles.html">孩子档案</a>
<a class="nav__item" href="universes.html">故事宇宙</a>
<a class="nav__item" href="push-settings.html">推送设置</a>
<a class="nav__item" href="account-settings.html">账户设置</a>
<a class="nav__item" href="admin-providers.html">管理后台</a>
</div>
<div class="nav__right">
<input class="input" style="width: 200px;" placeholder="搜索故事" />
<div class="avatar"></div>
</div>
</div>
</header>
<div class="container" style="padding: 28px 0 60px;">
<div class="grid grid-2 section">
<div class="card">
<h3>个人信息</h3>
<div class="row section">
<input class="input" placeholder="昵称" value="Dream Parent" />
<input class="input" placeholder="邮箱" value="parent@example.com" />
</div>
<button class="btn btn--primary">保存</button>
</div>
<div class="card">
<h3>账号安全</h3>
<div class="callout">已绑定 GitHub、Google</div>
<button class="btn btn--secondary" style="margin-top: 12px;">管理绑定</button>
</div>
<div class="card">
<h3>数据隐私</h3>
<button class="btn btn--secondary">导出数据</button>
<button class="btn btn--danger" style="margin-top: 12px;">删除账户</button>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,74 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>管理后台 - Providers</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<header class="nav">
<div class="container nav__inner">
<div class="nav__left">
<div class="nav__logo">
<span class="nav__logo-badge"></span>
DreamWeaver
</div>
<span class="badge">Web 原型</span>
</div>
<div class="nav__center">
<a class="nav__item active" href="home.html">生成故事</a>
<a class="nav__item" href="my-stories.html">我的故事</a>
<a class="nav__item" href="child-profiles.html">孩子档案</a>
<a class="nav__item" href="universes.html">故事宇宙</a>
<a class="nav__item" href="push-settings.html">推送设置</a>
<a class="nav__item" href="account-settings.html">账户设置</a>
<a class="nav__item" href="admin-providers.html">管理后台</a>
</div>
<div class="nav__right">
<input class="input" style="width: 200px;" placeholder="搜索故事" />
<div class="avatar"></div>
</div>
</div>
</header>
<div class="container" style="padding: 28px 0 60px;">
<div class="toolbar section">
<h2>Providers 管理</h2>
<button class="btn btn--primary">新增 Provider</button>
</div>
<table class="table">
<thead>
<tr>
<th>名称</th>
<th>类型</th>
<th>状态</th>
<th>延迟</th>
<th>最近检查</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr>
<td>text_primary</td>
<td>Text</td>
<td><span class="badge">健康</span></td>
<td>420ms</td>
<td>2 分钟前</td>
<td><a href="#">编辑</a> · <a href="#">禁用</a> · <a href="#">重载</a></td>
</tr>
<tr>
<td>image_primary</td>
<td>Image</td>
<td><span class="badge">健康</span></td>
<td>860ms</td>
<td>5 分钟前</td>
<td><a href="#">编辑</a> · <a href="#">禁用</a> · <a href="#">重载</a></td>
</tr>
</tbody>
</table>
<div class="footer-note">点击编辑后弹出 JSON 配置编辑器。</div>
</div>
</div>
</body>
</html>

View File

@@ -1,88 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>孩子档案详情</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<header class="nav">
<div class="container nav__inner">
<div class="nav__left">
<div class="nav__logo">
<span class="nav__logo-badge"></span>
DreamWeaver
</div>
<span class="badge">Web 原型</span>
</div>
<div class="nav__center">
<a class="nav__item active" href="home.html">生成故事</a>
<a class="nav__item" href="my-stories.html">我的故事</a>
<a class="nav__item" href="child-profiles.html">孩子档案</a>
<a class="nav__item" href="universes.html">故事宇宙</a>
<a class="nav__item" href="push-settings.html">推送设置</a>
<a class="nav__item" href="account-settings.html">账户设置</a>
<a class="nav__item" href="admin-providers.html">管理后台</a>
</div>
<div class="nav__right">
<input class="input" style="width: 200px;" placeholder="搜索故事" />
<div class="avatar"></div>
</div>
</div>
</header>
<div class="container" style="padding: 28px 0 60px;">
<div class="card section" style="display:flex; align-items:center; justify-content: space-between;">
<div style="display:flex; gap:12px; align-items:center;">
<div class="avatar"></div>
<div>
<div class="card-title">小明 · 5岁</div>
<div class="card-meta">男 · 生日 2020/05/12</div>
</div>
</div>
<button class="btn btn--secondary">编辑档案</button>
</div>
<div class="tabs section">
<div class="tab active">基础信息</div>
<div class="tab">兴趣与成长</div>
<div class="tab">故事宇宙</div>
<div class="tab">阅读记录</div>
</div>
<div class="grid grid-2 section">
<div class="card">
<h3>兴趣标签</h3>
<div class="chips">
<span class="chip selected">太空</span>
<span class="chip selected">机器人</span>
<span class="chip">冒险</span>
</div>
</div>
<div class="card">
<h3>成长主题</h3>
<div class="chips">
<span class="chip selected">勇气</span>
<span class="chip">分享</span>
</div>
</div>
</div>
<div class="section">
<h3>故事宇宙</h3>
<div class="grid grid-2">
<div class="card">
<div class="card-title">星际冒险</div>
<div class="card-meta">主角:小明船长 · 成就 3 个</div>
</div>
<div class="card">
<div class="card-title">梦幻森林</div>
<div class="card-meta">主角:森林守护者 · 成就 1 个</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,58 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>孩子档案</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<header class="nav">
<div class="container nav__inner">
<div class="nav__left">
<div class="nav__logo">
<span class="nav__logo-badge"></span>
DreamWeaver
</div>
<span class="badge">Web 原型</span>
</div>
<div class="nav__center">
<a class="nav__item active" href="home.html">生成故事</a>
<a class="nav__item" href="my-stories.html">我的故事</a>
<a class="nav__item" href="child-profiles.html">孩子档案</a>
<a class="nav__item" href="universes.html">故事宇宙</a>
<a class="nav__item" href="push-settings.html">推送设置</a>
<a class="nav__item" href="account-settings.html">账户设置</a>
<a class="nav__item" href="admin-providers.html">管理后台</a>
</div>
<div class="nav__right">
<input class="input" style="width: 200px;" placeholder="搜索故事" />
<div class="avatar"></div>
</div>
</div>
</header>
<div class="container" style="padding: 28px 0 60px;">
<div class="toolbar section">
<h2>我的宝贝</h2>
<button class="btn btn--primary">添加档案</button>
</div>
<div class="grid grid-3 section">
<div class="card">
<div class="avatar"></div>
<div class="card-title">小明 · 5岁</div>
<div class="chips"><span class="chip">太空</span><span class="chip">机器人</span></div>
</div>
<div class="card">
<div class="avatar"></div>
<div class="card-title">小红 · 3岁</div>
<div class="chips"><span class="chip">公主</span><span class="chip">动物</span></div>
</div>
<div class="card card--flat">
<div class="callout">空态示例:添加一个孩子档案</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,111 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>生成故事</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<header class="nav">
<div class="container nav__inner">
<div class="nav__left">
<div class="nav__logo">
<span class="nav__logo-badge"></span>
DreamWeaver
</div>
<span class="badge">Web 原型</span>
</div>
<div class="nav__center">
<a class="nav__item active" href="home.html">生成故事</a>
<a class="nav__item" href="my-stories.html">我的故事</a>
<a class="nav__item" href="child-profiles.html">孩子档案</a>
<a class="nav__item" href="universes.html">故事宇宙</a>
<a class="nav__item" href="push-settings.html">推送设置</a>
<a class="nav__item" href="account-settings.html">账户设置</a>
<a class="nav__item" href="admin-providers.html">管理后台</a>
</div>
<div class="nav__right">
<input class="input" style="width: 200px;" placeholder="搜索故事" />
<div class="avatar"></div>
</div>
</div>
</header>
<div class="container" style="padding: 28px 0 60px;">
<div class="stepper section">
<span class="step active">档案</span>
<span class="step">宇宙</span>
<span class="step">关键词</span>
<span class="step">生成</span>
</div>
<div class="split section">
<div class="card">
<h3>为谁创作故事</h3>
<div class="row section">
<div>
<label>孩子档案</label>
<select>
<option>小明 · 5岁</option>
<option>小红 · 3岁</option>
</select>
</div>
<div>
<label>故事宇宙</label>
<select>
<option>延续上一次(星际冒险)</option>
<option>新建宇宙</option>
</select>
</div>
</div>
<div class="section">
<label>关键词</label>
<div class="chips section">
<span class="chip selected">太空</span>
<span class="chip selected">勇气</span>
<span class="chip">机器人</span>
<span class="chip">探索</span>
</div>
<input class="input" placeholder="输入更多关键词" />
</div>
<div class="row section">
<div>
<label>成长主题</label>
<select>
<option>勇气</option>
<option>分享</option>
<option>独立</option>
</select>
</div>
<div>
<label>故事长度</label>
<div class="chips">
<span class="chip selected"></span>
<span class="chip"></span>
<span class="chip"></span>
</div>
</div>
</div>
<div class="section">
<button class="btn btn--primary" style="width: 100%;">生成故事</button>
</div>
</div>
<div class="card">
<h3>生成预览</h3>
<div class="card-cover"></div>
<div class="card-title">故事标题占位</div>
<p class="card-meta">故事摘要将显示在这里,支持 2-3 行预览。</p>
<div class="section">
<div class="callout">生成中:文本 → 封面 → 语音</div>
</div>
<div class="section">
<div class="callout" style="border-color: var(--error); color: var(--error);">封面生成失败,稍后重试</div>
<button class="btn btn--secondary" style="margin-top: 8px;">重新生成封面</button>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,33 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DreamWeaver 原型入口</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<div class="container" style="padding: 48px 0;">
<div class="hero">
<h1>DreamWeaver HTML 原型入口</h1>
<p>请选择页面进行导入或预览HTML to Figma</p>
<div class="grid grid-3 section">
<div class="card"><a href="login.html">登录 / 授权</a></div>
<div class="card"><a href="home.html">生成故事Home</a></div>
<div class="card"><a href="my-stories.html">我的故事(列表)</a></div>
<div class="card"><a href="story-detail.html">故事详情</a></div>
<div class="card"><a href="child-profiles.html">孩子档案(列表)</a></div>
<div class="card"><a href="child-profile-detail.html">孩子档案(详情)</a></div>
<div class="card"><a href="universes.html">故事宇宙(列表)</a></div>
<div class="card"><a href="universe-detail.html">故事宇宙(详情)</a></div>
<div class="card"><a href="push-settings.html">推送设置</a></div>
<div class="card"><a href="account-settings.html">账户设置</a></div>
<div class="card"><a href="admin-providers.html">管理后台Providers</a></div>
<div class="card"><a href="not-found.html">404 / 错误</a></div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,28 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>登录 / 授权</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<div class="container" style="padding: 80px 0;">
<div class="hero" style="max-width: 420px; margin: 0 auto; text-align: center;">
<div class="nav__logo" style="justify-content: center;">
<span class="nav__logo-badge"></span>
DreamWeaver
</div>
<h2 style="margin-top: 16px;">欢迎来到 DreamWeaver</h2>
<p>为孩子生成独一无二的故事</p>
<div class="section" style="display: grid; gap: 12px;">
<button class="btn btn--primary">使用 GitHub 登录</button>
<button class="btn btn--secondary">使用 Google 登录</button>
</div>
<div class="footer-note">我们仅使用公开信息创建账户</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,84 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>我的故事</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<header class="nav">
<div class="container nav__inner">
<div class="nav__left">
<div class="nav__logo">
<span class="nav__logo-badge"></span>
DreamWeaver
</div>
<span class="badge">Web 原型</span>
</div>
<div class="nav__center">
<a class="nav__item active" href="home.html">生成故事</a>
<a class="nav__item" href="my-stories.html">我的故事</a>
<a class="nav__item" href="child-profiles.html">孩子档案</a>
<a class="nav__item" href="universes.html">故事宇宙</a>
<a class="nav__item" href="push-settings.html">推送设置</a>
<a class="nav__item" href="account-settings.html">账户设置</a>
<a class="nav__item" href="admin-providers.html">管理后台</a>
</div>
<div class="nav__right">
<input class="input" style="width: 200px;" placeholder="搜索故事" />
<div class="avatar"></div>
</div>
</div>
</header>
<div class="container" style="padding: 28px 0 60px;">
<div class="toolbar section">
<input class="input" style="width: 260px;" placeholder="搜索标题或关键词" />
<select style="width: 160px;">
<option>孩子:全部</option>
<option>小明</option>
<option>小红</option>
</select>
<select style="width: 160px;">
<option>排序:最新</option>
<option>最早</option>
</select>
<button class="btn btn--ghost">网格</button>
<button class="btn btn--ghost">列表</button>
</div>
<div class="grid grid-3 section">
<div class="card">
<div class="card-cover"></div>
<div class="card-title">星际冒险 · 第三章</div>
<div class="chips">
<span class="chip">太空</span><span class="chip">勇气</span>
</div>
<div class="card-meta">小明 · 更新于 2 天前</div>
<div class="section hero-actions">
<button class="btn btn--primary">继续阅读</button>
<button class="btn btn--secondary">重生成封面</button>
</div>
</div>
<div class="card">
<div class="card-cover"></div>
<div class="card-title">梦幻森林 · 朋友篇</div>
<div class="chips">
<span class="chip">友谊</span><span class="chip">动物</span>
</div>
<div class="card-meta">小红 · 更新于 5 天前</div>
<div class="section hero-actions">
<button class="btn btn--primary">继续阅读</button>
<button class="btn btn--danger">删除</button>
</div>
</div>
<div class="card card--flat">
<div class="callout">空态示例:开始生成第一个故事</div>
<button class="btn btn--primary" style="margin-top: 12px;">生成故事</button>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,20 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>404</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<div class="container" style="padding: 80px 0; text-align:center;">
<div class="hero">
<h1>404</h1>
<p>页面走丢了,回到生成故事开始吧。</p>
<button class="btn btn--primary">返回首页</button>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,78 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>推送设置</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<header class="nav">
<div class="container nav__inner">
<div class="nav__left">
<div class="nav__logo">
<span class="nav__logo-badge"></span>
DreamWeaver
</div>
<span class="badge">Web 原型</span>
</div>
<div class="nav__center">
<a class="nav__item active" href="home.html">生成故事</a>
<a class="nav__item" href="my-stories.html">我的故事</a>
<a class="nav__item" href="child-profiles.html">孩子档案</a>
<a class="nav__item" href="universes.html">故事宇宙</a>
<a class="nav__item" href="push-settings.html">推送设置</a>
<a class="nav__item" href="account-settings.html">账户设置</a>
<a class="nav__item" href="admin-providers.html">管理后台</a>
</div>
<div class="nav__right">
<input class="input" style="width: 200px;" placeholder="搜索故事" />
<div class="avatar"></div>
</div>
</div>
</header>
<div class="container" style="padding: 28px 0 60px;">
<div class="grid grid-2 section">
<div class="card">
<h3>主动推送</h3>
<div class="row section">
<div>
<label>主开关</label>
<select>
<option>开启</option>
<option>关闭</option>
</select>
</div>
<div>
<label>推送时间</label>
<input class="input" placeholder="20:00" />
</div>
</div>
<div class="section">
<label>触发类型</label>
<div class="chips">
<span class="chip selected">时间触发</span>
<span class="chip selected">事件触发</span>
<span class="chip">行为触发</span>
<span class="chip">成长触发</span>
</div>
</div>
<div class="section">
<label>免打扰</label>
<div class="row">
<input class="input" placeholder="21:00" />
<input class="input" placeholder="09:00" />
</div>
</div>
</div>
<div class="card">
<h3>推送预览</h3>
<div class="callout">“今晚给小明讲一个关于太空的故事,好吗?”</div>
<button class="btn btn--secondary" style="margin-top: 12px;">发送测试推送</button>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,73 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>故事详情</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<header class="nav">
<div class="container nav__inner">
<div class="nav__left">
<div class="nav__logo">
<span class="nav__logo-badge"></span>
DreamWeaver
</div>
<span class="badge">Web 原型</span>
</div>
<div class="nav__center">
<a class="nav__item active" href="home.html">生成故事</a>
<a class="nav__item" href="my-stories.html">我的故事</a>
<a class="nav__item" href="child-profiles.html">孩子档案</a>
<a class="nav__item" href="universes.html">故事宇宙</a>
<a class="nav__item" href="push-settings.html">推送设置</a>
<a class="nav__item" href="account-settings.html">账户设置</a>
<a class="nav__item" href="admin-providers.html">管理后台</a>
</div>
<div class="nav__right">
<input class="input" style="width: 200px;" placeholder="搜索故事" />
<div class="avatar"></div>
</div>
</div>
</header>
<div class="container" style="padding: 28px 0 60px;">
<div class="section">
<div class="cover-hero"></div>
<h2 style="margin-top: 16px;">星际冒险 · 勇气的种子</h2>
<div class="card-meta">小明 · 星际冒险宇宙 · 2025/01/12</div>
<div class="hero-actions section">
<button class="btn btn--secondary">重新生成封面</button>
<button class="btn btn--primary">生成语音</button>
<button class="btn btn--ghost">分享</button>
</div>
</div>
<div class="split section">
<div class="card">
<h3>故事正文</h3>
<p>夜空像一条温柔的河流,小明驾驶着飞船穿过星光……</p>
<p>他握紧操纵杆,鼓起勇气,向未知的星球靠近。</p>
<p>最终,小明发现了新的朋友,也学会了如何面对黑暗。</p>
</div>
<div class="card">
<h3>成就</h3>
<div class="chips section">
<span class="chip selected">勇气</span>
<span class="chip selected">友谊</span>
</div>
<div class="callout section">“克服了黑暗的恐惧”</div>
<div class="callout">“帮助了迷路的小伙伴”</div>
</div>
</div>
<div class="section audio-player">
<button class="btn btn--ghost">播放</button>
<div class="audio-bar"><div class="audio-progress"></div></div>
<button class="btn btn--ghost">1.0x</button>
</div>
<div class="footer-note">语音未生成时,显示“生成语音”按钮作为主操作。</div>
</div>
</div>
</body>
</html>

View File

@@ -1,217 +0,0 @@
:root {
--primary-600: #7C3AED;
--primary-500: #8B5CF6;
--primary-100: #EDE9FE;
--accent-pink: #FB7185;
--accent-sky: #22D3EE;
--success: #34C759;
--warning: #F6A609;
--error: #FF5A5F;
--neutral-900: #1F2937;
--neutral-700: #4B5563;
--neutral-500: #9CA3AF;
--neutral-200: #E5E7EB;
--neutral-100: #F5F5F7;
--hero-gradient: linear-gradient(135deg, #EDE9FE 0%, #FFE4F3 45%, #E0F7FF 100%);
}
* { box-sizing: border-box; }
:root {
--container-width: 1200px;
--gutter: 24px;
--radius-card: 12px;
--radius-input: 10px;
--radius-button: 8px;
--radius-pill: 24px;
--shadow-s: 0 4px 16px rgba(31,36,48,0.08);
--shadow-m: 0 10px 30px rgba(31,36,48,0.12);
}
body {
margin: 0;
font-family: "PingFang SC", "Noto Sans SC", Inter, system-ui, -apple-system, sans-serif;
color: var(--neutral-900);
background: var(--neutral-100);
}
a { color: var(--primary-600); text-decoration: none; }
.page { min-height: 100vh; }
.container {
width: min(var(--container-width), 100% - 48px);
margin: 0 auto;
}
.nav {
background: #fff;
border-bottom: 1px solid var(--neutral-200);
position: sticky;
top: 0;
z-index: 10;
}
.nav__inner {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 0;
gap: 16px;
}
.nav__left, .nav__center, .nav__right { display: flex; align-items: center; gap: 16px; }
.nav__logo {
display: flex;
align-items: center;
gap: 10px;
font-weight: 700;
color: var(--neutral-900);
}
.nav__logo-badge {
width: 28px;
height: 28px;
border-radius: 8px;
background: linear-gradient(135deg, var(--primary-500), var(--accent-sky));
}
.nav__item { color: var(--neutral-700); font-weight: 500; }
.nav__item.active { color: var(--primary-600); }
.hero {
background: var(--hero-gradient);
border-radius: 16px;
padding: 32px;
box-shadow: var(--shadow-s);
}
.section { margin: 28px 0; }
.section-title { font-size: 20px; font-weight: 600; margin-bottom: 12px; }
.grid { display: grid; gap: 16px; }
.grid-2 { grid-template-columns: repeat(2, minmax(0,1fr)); }
.grid-3 { grid-template-columns: repeat(3, minmax(0,1fr)); }
.card {
background: #fff;
border-radius: var(--radius-card);
padding: 16px;
box-shadow: var(--shadow-s);
}
.card--flat { box-shadow: none; border: 1px solid var(--neutral-200); }
.card-cover {
width: 100%;
aspect-ratio: 21 / 9;
border-radius: 10px;
background: linear-gradient(135deg, var(--primary-100), #fff 60%, var(--accent-sky));
margin-bottom: 12px;
}
.card-title { font-weight: 600; margin: 6px 0; }
.card-meta { color: var(--neutral-500); font-size: 12px; }
.badge {
display: inline-flex;
align-items: center;
padding: 4px 10px;
border-radius: var(--radius-pill);
background: var(--primary-100);
color: var(--primary-600);
font-size: 12px;
}
.chips { display: flex; flex-wrap: wrap; gap: 8px; }
.chip {
padding: 6px 12px;
border-radius: var(--radius-pill);
border: 1px solid var(--neutral-200);
background: #fff;
font-size: 12px;
}
.chip.selected { background: var(--primary-100); border-color: var(--primary-500); color: var(--primary-600); }
.btn {
height: 40px;
padding: 0 16px;
border-radius: var(--radius-button);
border: 1px solid transparent;
cursor: pointer;
font-weight: 600;
}
.btn--primary { background: var(--primary-600); color: #fff; }
.btn--secondary { background: #fff; border-color: var(--primary-600); color: var(--primary-600); }
.btn--ghost { background: transparent; color: var(--neutral-700); }
.btn--danger { background: #fff; border-color: var(--error); color: var(--error); }
.input, select, textarea {
width: 100%;
padding: 10px 12px;
border-radius: var(--radius-input);
border: 1px solid var(--neutral-200);
background: #fff;
font-size: 14px;
}
.row { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 12px; }
.stepper { display: flex; gap: 10px; align-items: center; }
.step {
padding: 6px 12px;
border-radius: var(--radius-pill);
background: var(--neutral-100);
color: var(--neutral-700);
font-size: 12px;
}
.step.active { background: var(--primary-100); color: var(--primary-600); }
.toolbar { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
.table { width: 100%; border-collapse: collapse; }
.table th, .table td { border-bottom: 1px solid var(--neutral-200); padding: 12px 8px; text-align: left; font-size: 14px; }
.avatar {
width: 40px; height: 40px; border-radius: 50%;
background: var(--primary-100);
display: inline-flex; align-items: center; justify-content: center;
font-weight: 700; color: var(--primary-600);
}
.cover-hero {
aspect-ratio: 16 / 9;
border-radius: 14px;
background: linear-gradient(135deg, var(--primary-100), #fff 55%, var(--accent-pink));
}
.audio-player {
display: flex; align-items: center; gap: 12px; padding: 12px 16px;
border-radius: 12px; border: 1px solid var(--neutral-200); background: #fff;
}
.audio-bar { height: 6px; background: var(--neutral-200); border-radius: 999px; flex: 1; }
.audio-progress { width: 35%; height: 100%; background: var(--primary-600); border-radius: 999px; }
.tabs { display: flex; gap: 8px; border-bottom: 1px solid var(--neutral-200); }
.tab { padding: 10px 12px; color: var(--neutral-700); }
.tab.active { color: var(--primary-600); border-bottom: 2px solid var(--primary-600); }
.callout {
background: #fff;
border: 1px dashed var(--neutral-200);
border-radius: 12px;
padding: 12px;
color: var(--neutral-700);
font-size: 12px;
}
.footer-note { color: var(--neutral-500); font-size: 12px; margin-top: 12px; }
.hero-actions { display: flex; gap: 12px; flex-wrap: wrap; }
.split {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 24px;
}
@media (max-width: 1024px) {
.grid-3 { grid-template-columns: repeat(2, minmax(0,1fr)); }
.split { grid-template-columns: 1fr; }
}
@media (max-width: 768px) {
.grid-2 { grid-template-columns: 1fr; }
.grid-3 { grid-template-columns: 1fr; }
.row { grid-template-columns: 1fr; }
}

View File

@@ -1,64 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>宇宙详情</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<header class="nav">
<div class="container nav__inner">
<div class="nav__left">
<div class="nav__logo">
<span class="nav__logo-badge"></span>
DreamWeaver
</div>
<span class="badge">Web 原型</span>
</div>
<div class="nav__center">
<a class="nav__item active" href="home.html">生成故事</a>
<a class="nav__item" href="my-stories.html">我的故事</a>
<a class="nav__item" href="child-profiles.html">孩子档案</a>
<a class="nav__item" href="universes.html">故事宇宙</a>
<a class="nav__item" href="push-settings.html">推送设置</a>
<a class="nav__item" href="account-settings.html">账户设置</a>
<a class="nav__item" href="admin-providers.html">管理后台</a>
</div>
<div class="nav__right">
<input class="input" style="width: 200px;" placeholder="搜索故事" />
<div class="avatar"></div>
</div>
</div>
</header>
<div class="container" style="padding: 28px 0 60px;">
<div class="card section">
<h2>星际冒险</h2>
<div class="card-meta">主角:小明船长 · 更新于 2025/01/12</div>
</div>
<div class="grid grid-2 section">
<div class="card">
<h3>主角设定</h3>
<div class="callout">小明是来自地球的探险家,勇敢且好奇。</div>
</div>
<div class="card">
<h3>常驻角色</h3>
<div class="callout">机器人小七、外星猫咪星星</div>
</div>
<div class="card">
<h3>世界观</h3>
<div class="callout">星际学院、彩虹星云、飞船港湾</div>
</div>
<div class="card">
<h3>成就</h3>
<div class="callout">克服恐惧 · 结交朋友 · 学会独立</div>
</div>
</div>
<div class="section">
<button class="btn btn--secondary">编辑宇宙</button>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,64 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>故事宇宙</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<header class="nav">
<div class="container nav__inner">
<div class="nav__left">
<div class="nav__logo">
<span class="nav__logo-badge"></span>
DreamWeaver
</div>
<span class="badge">Web 原型</span>
</div>
<div class="nav__center">
<a class="nav__item active" href="home.html">生成故事</a>
<a class="nav__item" href="my-stories.html">我的故事</a>
<a class="nav__item" href="child-profiles.html">孩子档案</a>
<a class="nav__item" href="universes.html">故事宇宙</a>
<a class="nav__item" href="push-settings.html">推送设置</a>
<a class="nav__item" href="account-settings.html">账户设置</a>
<a class="nav__item" href="admin-providers.html">管理后台</a>
</div>
<div class="nav__right">
<input class="input" style="width: 200px;" placeholder="搜索故事" />
<div class="avatar"></div>
</div>
</div>
</header>
<div class="container" style="padding: 28px 0 60px;">
<div class="toolbar section">
<h2>故事宇宙</h2>
<button class="btn btn--primary">新建宇宙</button>
</div>
<div class="grid grid-3 section">
<div class="card">
<div class="card-title">星际冒险</div>
<div class="card-meta">主角:小明船长</div>
<div class="chips section">
<span class="chip">伙伴:机器人小七</span>
<span class="chip">成就3</span>
</div>
</div>
<div class="card">
<div class="card-title">梦幻森林</div>
<div class="card-meta">主角:森林守护者</div>
<div class="chips section">
<span class="chip">伙伴:魔法猫咪</span>
<span class="chip">成就1</span>
</div>
</div>
<div class="card card--flat">
<div class="callout">空态示例:创建第一个宇宙</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,429 +0,0 @@
# 孩子档案数据模型
## 概述
孩子档案是记忆智能系统的核心,存储孩子的基础信息、兴趣偏好和阅读行为数据。
---
## 一、数据库模型
### 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 天后永久删除

View File

@@ -1,541 +0,0 @@
# 记忆智能系统 PRD
## 概述
**功能名称**: 记忆智能 (Memory Intelligence)
**版本**: v1.1
**优先级**: Phase 2.5 (体验增强后、社区化前)
**目标用户**: 家长 + 3-8 岁儿童
**更新记录**: 2025-01-22 合并 `backend/docs/memory_system_prd.md`
### 核心愿景
将当前的"数据存储"升级为有温度的**"情感连接系统"**。
我们不只是在记住数据,而是在**维护孩子与故事世界的关系**。让每一个故事不再是孤立的碎片,而是构建孩子专属"故事宇宙"的砖瓦。
### 核心价值
让 DreamWeaver 从"故事生成工具"进化为"懂孩子的故事伙伴"
- **记住孩子**: 偏好、成长阶段、兴趣变化
- **延续故事**: 角色、世界观跨故事延续
- **主动关怀**: 适时推送个性化故事建议
### 产品痛点与解决方案
| 用户角色 | 核心痛点 | 解决方案 | 预期价值 |
|---------|---------|---------|---------|
| **孩子** | "上次的小兔子怎么不认识我了?" 故事之间缺乏连续性。 | **角色一致性与记忆注入** 故事开头主动提及往事,角色性格延续。 | 建立情感依恋,提升沉浸感。 |
| **家长** | "这App除了生成故事还能干嘛" 无法感知产品的长期教育价值。 | **显性化成长轨迹** 词汇量统计、主题变化、成就徽章可视化。 | 提高付费意愿,提供社交货币。 |
| **平台** | 用户用完即走,缺乏留存壁垒。 | **沉没成本与情感资产** 积累的记忆越多,越舍不得离开。 | 提升长期留存率 (LTV)。 |
---
## 一、功能模块
### 1.0 记忆分层模型
#### 层级 1: 核心档案 (Identity Layer)
*性质:永久、静态、显性*
- **数据**: 姓名、年龄、性别
- **输入**: 家长在 Onboarding 阶段手动输入
- **作用**: 决定故事的基础适龄性和称呼
#### 层级 2: 故事宇宙 (Universe Layer)
*性质:长期、动态积累、半显性*
- **主角设定**: 姓名、性格特征(勇敢/害羞)、外貌特征(戴眼镜/卷发)
- **常驻配角**: 从随机故事中涌现出的固定伙伴(如"爱吃胡萝卜的松鼠奇奇"
- **世界观**: 故事发生的背景(魔法森林、未来城市、海底世界)
- **成就系统**: 孩子获得的虚拟奖励(勇气勋章、小小探险家)
#### 层级 3: 工作记忆 (Working Memory)
*性质:短期、自动衰减、隐性*
- **关键情节**: 最近 3 个故事的结局和核心冲突
- **情感标记**: 孩子对特定内容的反应(根据"重播"、"跳过"推断)
- **新学词汇**: 故事中出现的高级词汇
### 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 智能开场白 (Memory Injection)
在生成新故事时Prompt 必须包含一段"记忆唤醒"指令:
- **示例**: "小明,还记得上周我们帮小松鼠找回了松果吗?今天,小松鼠带来了一位新朋友..."
- **策略**: 提取权重最高的 Top 3 记忆注入 Prompt
### 5.3 成就提取 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.08-30 天 0.731-90 天 0.490 天后 0.2
- 读取时按 `score` 排序,选 Top N 进入 Prompt
**可选实现**:定期批处理降权
- 每日/每周批量更新 `base_weight`
- 适合数据量大、读多写少的场景
**RAG 场景的衰减用法**
- 语义相似度分数 × 时间衰减
- 可加时间窗口过滤(如仅取最近 90 天)
**删除策略(默认不删)**
- 默认只降权,不主动删除
- 可选:对低权重且 180 天未使用的条目执行 TTL 清理
---
## 八、关键功能特性
### 8.1 成长时间轴 (Growth Timeline)
一个可视化的 H5 页面或 App 模块,以时间轴形式展示里程碑:
- 🌟 **初次相遇**: 创建角色的第一天
- 📖 **阅读打卡**: 累计阅读 10/50/100 本
- 🏅 **获得成就**: 获得"诚实勋章"
- 🧠 **能力解锁**: 第一次阅读"科幻"题材
### 8.2 成就仪式感 (Achievement Ceremony)
- **触发**: 故事生成并分析后,如果获得新成就
- **表现**: 弹窗动画 + 音效 + "恭喜获得 [勇气] 徽章"
- **分享**: 允许生成带二维码的成就海报
---
## 九、记忆类型扩展
| 类型 Key | 描述 | 来源 | 过期策略 |
|---------|------|------|---------|
| `recent_story` | 最近读过的故事梗概 | 阅读事件 | 30天衰减 |
| `favorite_character` | 孩子喜欢的角色 | 重播/高评分 | 长期有效 |
| `scary_element` | 孩子害怕/不喜欢的元素 | 跳过/负反馈 | 长期有效 (避雷) |
| `vocabulary_growth` | 新掌握的词汇 | 故事分析 | 90天衰减 |
| `emotional_highlight` | 高光时刻 (如: 特别开心的情节) | 互动数据 | 60天衰减 |
---
## 十、里程碑
### Phase 1: 基础建设 (v0.3.0)
- [x] 数据库 `MemoryItem` 表 (已存在)
- [ ] 扩展 `MemoryItem` 类型字段,支持更多维度
- [ ] 优化 `_build_memory_context`,支持更自然的 Prompt 注入
- [ ] 前端:简单的"近期回忆"展示列表
### M1: 孩子档案基础
- [ ] 数据库模型
- [ ] CRUD API
- [ ] 前端档案管理页面
- [ ] 故事生成时选择档案
### M2: 故事宇宙
- [ ] 宇宙数据模型
- [ ] Prompt 集成
- [ ] 成就自动提取
- [ ] 前端宇宙管理
### M3: 主动推送
- [ ] 推送配置 API
- [ ] Celery Beat 调度
- [ ] 推送通知集成 (Web Push / 微信)
### M4: 隐式学习
- [ ] 行为埋点
- [ ] 偏好学习算法
- [ ] 推荐优化
### Phase 2: 可视化与成就 (v0.4.0)
- [ ] 实现"成就提取器" (Achievement Extractor) 的闭环通知
- [ ] 前端:开发"我的成就"和"成长时间轴"页面
- [ ] 增加故事开场白的动态生成逻辑
### Phase 3: 深度智能 (v0.5.0+)
- [ ] 引入向量数据库,实现基于语义的记忆检索 (不仅是时间最近)
- [ ] 情感分析模型:分析用户行为推断情感倾向
---
## 十一、风险与应对
| 风险 | 影响 | 应对 |
|------|------|------|
| 隐私合规 | 高 | 儿童数据加密存储,家长授权机制 |
| 推送骚扰 | 中 | 默认关闭,用户主动开启 |
| 记忆膨胀 | 低 | 定期清理旧数据,限制宇宙数量 |
---
## 十二、相关文档
- [孩子档案数据模型](./CHILD-PROFILE-MODEL.md)
- [故事宇宙记忆结构](./STORY-UNIVERSE-MODEL.md)
- [主动推送触发规则](./PUSH-TRIGGER-RULES.md)

View File

@@ -1,177 +0,0 @@
# 记忆与个性化技术方案建议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. 推荐实施路线
### v11-2 个月)
- DB 记忆为主RAG 只做轻量补充
- 引入时序衰减
- 记忆来源:用户显式输入 + 行为日志
### v22-3 个月)
- 引入 Agent 记忆抽取与置信度
- 记忆管理界面(家长可编辑)
- 更精细的个性化推荐
---
## 9. 需要确认的决定点
- 是否采用混合方案DB + RAG
- RAG 的检索范围(故事摘要 / 行为摘要 / 成就)
- 记忆分层与衰减规则
- Agent 记忆写入规则与阈值
- 家长可见/可控的记忆管理策略
---
如确认以上方向,我可以进一步输出:
- PRD 里的“记忆系统”完整章节
- 数据模型(含字段 + 时序衰减)
- 交互与界面草案
- 后端实现拆解(任务清单 + 里程碑)

View File

@@ -1,129 +0,0 @@
# 主动推送触发规则
## 概述
主动推送用于在合适的时间为家长提供个性化故事建议,提升使用频次与亲子阅读习惯。推送默认关闭,需家长开启并配置时间。
---
## 一、数据输入
- **孩子档案**: `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)

View File

@@ -1,231 +0,0 @@
# 故事宇宙记忆结构
## 概述
故事宇宙用于在多次故事生成中保持角色、世界观与成长成就的连续性。每个孩子档案可以拥有多个宇宙,故事生成时可选择“延续上一个故事”,系统自动带入宇宙设定。
---
## 一、数据库模型
### 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)

View File

@@ -1,130 +0,0 @@
# DreamWeaver 产品愿景与全流程规划
## 一、产品定位
### 1.1 愿景
**梦语织机** - 为 3-8 岁儿童打造的 AI 故事创作平台,让每个孩子都能拥有专属的成长故事。
### 1.2 核心价值
| 维度 | 价值主张 |
|------|----------|
| 个性化 | 基于关键词/主角定制,每个故事独一无二 |
| 教育性 | 融入成长主题(勇气、友谊、诚实等) |
| 沉浸感 | AI 封面 + 语音朗读,多感官体验 |
| 亲子互动 | 家长参与创作,增进亲子关系 |
### 1.3 目标用户
**主要用户家长25-40岁**
- 需求:为孩子找到有教育意义的睡前故事
- 痛点:市面故事千篇一律,缺乏个性化
- 场景:睡前、旅途、周末亲子时光
**次要用户:幼儿园/早教机构**
- 需求:批量生成教学故事素材
- 痛点:内容制作成本高
---
## 二、竞品分析
| 产品 | 优势 | 劣势 | 我们的差异化 |
|------|------|------|--------------|
| 凯叔讲故事 | 内容丰富、品牌知名 | 无个性化、订阅贵 | AI 个性化生成 |
| 喜马拉雅儿童 | 海量音频、多平台 | 内容同质化 | 用户参与创作 |
| ChatGPT | AI 能力强 | 非儿童专属、无配套 | 垂直场景优化 |
| Midjourney | 图像质量高 | 无故事整合 | 故事+图像+音频一体 |
---
## 三、产品路线图
### Phase 1: MVP 完善 ✅ 已完成
- [x] 关键词生成故事
- [x] 故事润色增强
- [x] AI 封面生成
- [x] 语音朗读
- [x] 故事收藏管理
- [x] OAuth 登录
- [x] 工程鲁棒性改进
### Phase 2: 体验增强
| 功能 | 优先级 | 用户价值 |
|------|--------|----------|
| 故事编辑 | P0 | 用户可修改 AI 生成内容 |
| 角色定制 | P0 | 孩子成为故事主角 |
| 故事续写 | P1 | 形成系列故事 |
| 多语言支持 | P1 | 英文故事学习 |
| 故事分享 | P1 | 社交传播 |
### Phase 3: 供应商平台化
| 功能 | 优先级 | 技术价值 |
|------|--------|----------|
| 供应商管理后台 | P0 | 可视化配置 AI 供应商 |
| 适配器插件化 | P0 | 新供应商零代码接入 |
| 供应商健康监控 | P1 | 自动故障转移 |
| A/B 测试框架 | P1 | 供应商效果对比 |
| 成本分析面板 | P2 | API 调用成本追踪 |
### Phase 4: 社区与增长
| 功能 | 优先级 | 增长价值 |
|------|--------|----------|
| 故事广场 | P0 | 内容发现 |
| 点赞/收藏 | P0 | 社区互动 |
| 创作者主页 | P1 | 用户留存 |
| 故事模板 | P1 | 降低创作门槛 |
### Phase 5: 商业化
| 功能 | 优先级 | 商业价值 |
|------|--------|----------|
| 会员订阅 | P0 | 核心收入 |
| 故事导出 | P0 | 增值服务 |
| 实体书打印 | P1 | 高客单价 |
| API 开放 | P2 | B 端收入 |
---
## 四、核心指标 (KPIs)
### 4.1 用户指标
| 指标 | 定义 | 目标 |
|------|------|------|
| DAU | 日活跃用户 | Phase 2: 1000+ |
| 留存率 | 次日/7日/30日 | 40%/25%/15% |
| 创作转化率 | 访问→创作 | 30%+ |
### 4.2 业务指标
| 指标 | 定义 | 目标 |
|------|------|------|
| 故事生成量 | 日均生成数 | 5000+ |
| 分享率 | 故事被分享比例 | 10%+ |
| 付费转化率 | 免费→付费 | 5%+ |
### 4.3 技术指标
| 指标 | 定义 | 目标 |
|------|------|------|
| API 成功率 | 供应商调用成功率 | 99%+ |
| 响应时间 | 故事生成 P95 | <30s |
| 成本/故事 | 单个故事 API 成本 | <$0.05 |
---
## 五、风险与应对
| 风险 | 影响 | 概率 | 应对策略 |
|------|------|------|----------|
| AI 生成内容不当 | 高 | 中 | 内容审核 + 家长控制 + 敏感词过滤 |
| API 成本过高 | 高 | 中 | 多供应商比价 + 缓存优化 + 分级限流 |
| 供应商服务中断 | 高 | 低 | 多供应商冗余 + 自动故障转移 |
| 用户增长缓慢 | 中 | 中 | 社区运营 + 分享裂变 + SEO |
| 竞品模仿 | 低 | 高 | 快速迭代 + 深耕垂直 + 数据壁垒 |
---
## 六、下一步讨论议题
1. **供应商平台化架构** - 如何设计插件化的适配器系统?
2. **Phase 2 功能优先级** - 先做哪个功能?
3. **技术选型** - nanobanana vs flux vs 其他图像供应商?
4. **商业模式** - 免费/付费边界在哪里?
请确认以上产品愿景是否符合预期,我们再深入讨论供应商平台化的技术架构。

View File

@@ -1,677 +0,0 @@
# RFC: 供应商平台化架构设计
## 背景
### 当前问题
1. **硬编码适配器**: `gemini`, `flux`, `minimax` 写死在代码中
2. **新供应商需改代码**: 接入 nanobanana 等新供应商需要修改 `provider_router.py`
3. **无法动态切换**: 供应商故障时需要重启服务
4. **缺乏监控**: 不知道哪个供应商更快、更便宜、更稳定
### 目标
- **零代码接入**: 通过后台配置即可接入新供应商
- **动态切换**: 运行时切换供应商,无需重启
- **智能路由**: 基于成本、延迟、成功率自动选择最优供应商
- **可观测性**: 供应商健康状态、成本、性能一目了然
---
## 架构设计
### 1. 整体架构
```
┌─────────────────────────────────────────────────────────────┐
│ Admin Dashboard │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ 供应商管理 │ │ 健康监控 │ │ 成本分析 │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Provider Router │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 路由策略: Priority → Weight → Health → Cost │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────┬───────────┬───────────┬───────────────────┐ │
│ │ Adapter │ Adapter │ Adapter │ Adapter │ │
│ │ Registry │ Factory │ Health │ Metrics │ │
│ └───────────┴───────────┴───────────┴───────────────────┘ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ Text Adapters │ │ Image Adapters│ │ TTS Adapters │
├───────────────┤ ├───────────────┤ ├───────────────┤
│ • Gemini │ │ • Flux │ │ • Minimax │
│ • OpenAI │ │ • Nanobanana │ │ • ElevenLabs │
│ • Claude │ │ • DALL-E │ │ • Azure TTS │
│ • Qwen │ │ • Midjourney │ │ • Google TTS │
└───────────────┘ └───────────────┘ └───────────────┘
```
### 2. 核心组件
#### 2.1 Adapter 接口定义
```python
# 统一适配器接口
from abc import ABC, abstractmethod
from typing import TypeVar, Generic
from pydantic import BaseModel
T = TypeVar("T")
class AdapterConfig(BaseModel):
"""适配器配置基类"""
api_key: str
api_base: str | None = None
model: str | None = None
timeout_ms: int = 60000
max_retries: int = 3
class BaseAdapter(ABC, Generic[T]):
"""适配器基类"""
# 适配器元信息
adapter_type: str # text / image / tts
adapter_name: str # gemini / flux / minimax
def __init__(self, config: AdapterConfig):
self.config = config
@abstractmethod
async def execute(self, **kwargs) -> T:
"""执行适配器逻辑"""
pass
@abstractmethod
async def health_check(self) -> bool:
"""健康检查"""
pass
@property
@abstractmethod
def estimated_cost(self) -> float:
"""预估单次调用成本 (USD)"""
pass
```
#### 2.2 适配器注册表
```python
# 适配器注册表 - 支持动态注册
class AdapterRegistry:
"""适配器注册表"""
_adapters: dict[str, type[BaseAdapter]] = {}
@classmethod
def register(cls, adapter_type: str, adapter_name: str):
"""装饰器: 注册适配器"""
def decorator(adapter_class: type[BaseAdapter]):
key = f"{adapter_type}:{adapter_name}"
cls._adapters[key] = adapter_class
return adapter_class
return decorator
@classmethod
def get(cls, adapter_type: str, adapter_name: str) -> type[BaseAdapter] | None:
key = f"{adapter_type}:{adapter_name}"
return cls._adapters.get(key)
@classmethod
def list_adapters(cls, adapter_type: str | None = None) -> list[str]:
"""列出所有已注册的适配器"""
if adapter_type:
return [k for k in cls._adapters if k.startswith(f"{adapter_type}:")]
return list(cls._adapters.keys())
```
#### 2.3 适配器实现示例
```python
# 图像适配器示例: Nanobanana
@AdapterRegistry.register("image", "nanobanana")
class NanobananapAdapter(BaseAdapter[str]):
adapter_type = "image"
adapter_name = "nanobanana"
async def execute(self, prompt: str, **kwargs) -> str:
"""生成图片,返回 URL"""
async with httpx.AsyncClient(timeout=self.config.timeout_ms / 1000) as client:
response = await client.post(
f"{self.config.api_base}/generate",
json={"prompt": prompt, "model": self.config.model},
headers={"Authorization": f"Bearer {self.config.api_key}"},
)
response.raise_for_status()
return response.json()["image_url"]
async def health_check(self) -> bool:
# 简单的健康检查
try:
async with httpx.AsyncClient(timeout=5) as client:
response = await client.get(f"{self.config.api_base}/health")
return response.status_code == 200
except Exception:
return False
@property
def estimated_cost(self) -> float:
return 0.02 # $0.02 per image
```
#### 2.4 智能路由器
```python
class ProviderRouter:
"""智能供应商路由器"""
def __init__(self, db: AsyncSession):
self.db = db
self._health_cache: dict[str, tuple[bool, float]] = {} # adapter_key -> (healthy, last_check)
async def route(
self,
provider_type: str,
strategy: str = "priority", # priority / cost / latency / round_robin
**kwargs
):
"""路由到最优供应商"""
providers = await self._get_enabled_providers(provider_type)
if not providers:
raise ValueError(f"No {provider_type} providers configured")
# 按策略排序
sorted_providers = self._sort_by_strategy(providers, strategy)
errors = []
for provider in sorted_providers:
# 检查健康状态
if not await self._is_healthy(provider):
continue
try:
adapter = self._create_adapter(provider)
result = await adapter.execute(**kwargs)
# 记录成功指标
await self._record_metrics(provider, success=True)
return result
except Exception as e:
errors.append(f"{provider.name}: {e}")
await self._record_metrics(provider, success=False, error=str(e))
continue
raise ValueError(f"All providers failed: {' | '.join(errors)}")
def _sort_by_strategy(self, providers: list[Provider], strategy: str) -> list[Provider]:
if strategy == "priority":
return sorted(providers, key=lambda p: (-p.priority, -p.weight))
elif strategy == "cost":
return sorted(providers, key=lambda p: self._get_estimated_cost(p))
elif strategy == "latency":
return sorted(providers, key=lambda p: self._get_avg_latency(p))
else:
return providers
```
### 3. 数据模型扩展
```sql
-- 供应商表 (已有,需扩展)
ALTER TABLE providers ADD COLUMN api_key_ref VARCHAR(100); -- 密钥引用 (从 secrets 表获取)
ALTER TABLE providers ADD COLUMN request_schema JSONB; -- 请求参数 schema
ALTER TABLE providers ADD COLUMN response_parser VARCHAR(200); -- 响应解析规则
-- 供应商指标表 (新增)
CREATE TABLE provider_metrics (
id SERIAL PRIMARY KEY,
provider_id VARCHAR(36) REFERENCES providers(id),
timestamp TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
success BOOLEAN,
latency_ms INTEGER,
cost_usd DECIMAL(10, 6),
error_message TEXT,
request_id VARCHAR(100)
);
-- 供应商健康状态表 (新增)
CREATE TABLE provider_health (
provider_id VARCHAR(36) PRIMARY KEY REFERENCES providers(id),
is_healthy BOOLEAN DEFAULT TRUE,
last_check TIMESTAMP WITH TIME ZONE,
consecutive_failures INTEGER DEFAULT 0,
last_error TEXT
);
-- 密钥管理表 (新增)
CREATE TABLE provider_secrets (
id VARCHAR(36) PRIMARY KEY,
name VARCHAR(100) UNIQUE NOT NULL,
encrypted_value TEXT NOT NULL, -- 加密存储
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
```
### 4. Admin Dashboard 功能
#### 4.1 供应商管理
- 供应商列表 (启用/禁用/删除)
- 新增供应商 (选择适配器类型 + 配置参数)
- 编辑供应商 (修改优先级/权重/超时等)
- 测试连接 (验证 API Key 有效性)
#### 4.2 健康监控
- 实时健康状态 (绿/黄/红)
- 成功率趋势图
- 延迟分布图
- 故障告警配置
#### 4.3 成本分析
- 按供应商统计调用量
- 按供应商统计成本
- 成本趋势图
- 预算告警
#### 4.4 A/B 测试
- 创建实验 (供应商 A vs B)
- 流量分配 (50/50 或自定义)
- 效果对比 (成功率/延迟/成本)
---
## 实现路径
### 阶段 1: 适配器抽象 (基础) - ✅ 已完成
| 任务 | 状态 | 文件 |
|------|------|------|
| 定义 `BaseAdapter` 接口 | ✅ | `services/adapters/base.py` |
| 实现 `AdapterRegistry` 注册表 | ✅ | `services/adapters/registry.py` |
| 重构 GeminiAdapter | ✅ | `services/adapters/text/gemini.py` |
| 重构 FluxAdapter | ✅ | `services/adapters/image/flux.py` |
| 重构 MinimaxAdapter | ✅ | `services/adapters/tts/minimax.py` |
| 重构 `ProviderRouter` 使用新接口 | ✅ | `services/provider_router.py` |
### 阶段 2: 新供应商接入 (扩展) - 待开始
1. 实现 Nanobanana 适配器
2. 实现 OpenAI/Claude 文本适配器
3. 实现 ElevenLabs TTS 适配器
4. 验证零代码接入流程
### 阶段 3: 监控与分析 (可观测) - 待开始
1. 实现指标收集
2. 实现健康检查
3. 实现成本追踪
4. Admin Dashboard 开发
### 阶段 4: 智能路由 (优化) - 待开始
1. 实现多种路由策略
2. 实现自动故障转移
3. 实现 A/B 测试框架
---
## 并行执行与容错设计
### 问题
当前串行流程存在两个问题:
1. **等待时间长**: 故事(3-5s) → 封面(5-10s) → 音频(3-5s) = 总计 11-20s
2. **单点失败**: 某一步502/超时导致整个流程失败
### 方案 1: 并行执行
```python
async def generate_story_full(keywords: list[str]) -> StoryResult:
# Step 1: 故事生成(必须先完成,后续依赖它)
story = await generate_story_content(keywords)
# Step 2: 图片和音频并行执行
image_task = asyncio.create_task(generate_image(story.summary))
audio_task = asyncio.create_task(text_to_speech(story.content))
# 等待两者完成,互不阻塞
image_result, audio_result = await asyncio.gather(
image_task, audio_task,
return_exceptions=True # 一个<E4B880><E4B8AA><EFBFBD>败不影响另一个
)
return StoryResult(
story=story,
image_url=image_result if not isinstance(image_result, Exception) else None,
audio_url=audio_result if not isinstance(audio_result, Exception) else None,
errors={
"image": str(image_result) if isinstance(image_result, Exception) else None,
"audio": str(audio_result) if isinstance(audio_result, Exception) else None,
}
)
```
**时间对比:**
```
串行: 3s + 8s + 4s = 15s
并行: 3s + max(8s, 4s) = 11s (节省 27%)
```
### 方案 2: 部分成功处理
**核心原则: 部分成功 > 全部失败**
```python
@dataclass
class StoryResult:
story: Story # 核心,必须成功
image_url: str | None = None # 增强,可降级
audio_url: str | None = None # 增强,可降级
errors: dict[str, str] = field(default_factory=dict)
@property
def is_complete(self) -> bool:
return self.image_url is not None and self.audio_url is not None
@property
def failed_components(self) -> list[str]:
return [k for k, v in self.errors.items() if v is not None]
```
**降级策略:**
| 组件 | 失败时降级方案 | 用户体验 |
|------|---------------|---------|
| 故事 | 无降级,整体失败 | 显示错误,提示重试 |
| 封面 | 使用默认封面图 | 显示占位图 + "重新生成"按钮 |
| 音频 | 不生成音频 | 隐藏播放按钮 + "生成语音"按钮 |
### 方案 3: 流式返回 (SSE)
**为什么用 SSE:**
- 用户无需等待全部完成
- 每完成一步立即展示
- 比 WebSocket 简单HTTP 兼容性好
**后端实现:**
```python
from fastapi import APIRouter
from sse_starlette.sse import EventSourceResponse
router = APIRouter()
@router.post("/api/generate/stream")
async def generate_story_stream(
request: GenerateRequest,
current_user: User = Depends(get_current_user),
):
async def event_generator():
# 1. 立即返回任务ID
story_id = str(uuid.uuid4())
yield {"event": "started", "data": json.dumps({"story_id": story_id})}
# 2. 生成故事
try:
story = await generate_story_content(request.keywords)
yield {"event": "story_ready", "data": json.dumps({
"title": story.title,
"content": story.content,
})}
except Exception as e:
yield {"event": "story_failed", "data": json.dumps({"error": str(e)})}
return
# 3. 并行生成图片和音频
async def gen_image():
try:
url = await generate_image(story.summary)
yield {"event": "image_ready", "data": json.dumps({"image_url": url})}
except Exception as e:
yield {"event": "image_failed", "data": json.dumps({"error": str(e)})}
async def gen_audio():
try:
url = await text_to_speech(story.content)
yield {"event": "audio_ready", "data": json.dumps({"audio_url": url})}
except Exception as e:
yield {"event": "audio_failed", "data": json.dumps({"error": str(e)})}
# 并行执行逐个yield结果
tasks = [gen_image(), gen_audio()]
for coro in asyncio.as_completed([t.__anext__() for t in tasks]):
result = await coro
yield result
yield {"event": "complete", "data": json.dumps({"story_id": story_id})}
return EventSourceResponse(event_generator())
```
**前端实现:**
```typescript
const eventSource = new EventSource('/api/generate/stream', {
method: 'POST',
body: JSON.stringify({ keywords }),
});
eventSource.addEventListener('started', (e) => {
const { story_id } = JSON.parse(e.data);
showLoading('正在创作故事...');
});
eventSource.addEventListener('story_ready', (e) => {
const { title, content } = JSON.parse(e.data);
renderStory(title, content);
showLoading('正在生成封面和语音...');
});
eventSource.addEventListener('image_ready', (e) => {
const { image_url } = JSON.parse(e.data);
renderCover(image_url);
});
eventSource.addEventListener('image_failed', (e) => {
showRetryButton('image');
});
eventSource.addEventListener('audio_ready', (e) => {
const { audio_url } = JSON.parse(e.data);
enablePlayButton(audio_url);
});
eventSource.addEventListener('complete', () => {
eventSource.close();
hideLoading();
});
```
**用户体验时间线:**
```
0s → 显示"正在创作..."
3s → 故事文本渲染,显示"正在生成封面和语音..."
3-7s → 音频就绪,播放按钮可用
3-11s → 封面就绪,图片显示
11s → 完成
```
### 方案 4: 断点续传 (可选)
适用于网络不稳定场景,支持刷新页面后继续:
```python
class StoryWorkflowState(Base):
__tablename__ = "story_workflow_states"
story_id: Mapped[str] = mapped_column(String(36), primary_key=True)
status: Mapped[str] = mapped_column(String(20)) # pending/story_done/image_done/audio_done/complete
story_content: Mapped[str | None] = mapped_column(Text)
image_url: Mapped[str | None] = mapped_column(String(500))
audio_url: Mapped[str | None] = mapped_column(String(500))
last_error: Mapped[str | None] = mapped_column(Text)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
updated_at: Mapped[datetime] = mapped_column(DateTime, onupdate=datetime.utcnow)
async def resume_workflow(story_id: str) -> StoryResult:
state = await get_workflow_state(story_id)
if state.status == "story_done":
# 从图片+音频生成继续
return await generate_image_and_audio(state)
elif state.status == "image_done":
# 只需要生成音频
return await generate_audio_only(state)
elif state.status == "audio_done":
# 只需要生成图片
return await generate_image_only(state)
else:
return StoryResult.from_state(state)
```
### 推荐实现顺序
| 优先级 | 方案 | 收益 | 复杂度 | 状态 |
|--------|------|------|--------|------|
| P0 | 并行执行 | 节省 27% 时间 | 低 | ✅ 已完成 |
| P0 | 部分成功 | 提升容错性 | 低 | ✅ 已完成 |
| P1 | SSE 流式返回 | 体验大幅提升 | 中 | 待开始 |
| P2 | 断点续传 | 极端场景保障 | 高 | 待开始 |
**P0 实现详情:**
- 新增 API: `POST /api/generate/full`
- 文件: `api/stories.py:113-189`
- 响应模型: `FullStoryResponse` (含 `errors` 字段标识失败组件)
---
## 待决策清单
> **使用说明**: 在每个决策的 `[ ]` 中填入你的选择(如 `[x]` 或 `[B]`),确认后删除未选中的选项。
---
### 决策 1: 适配器配置存储
**问题**: 适配器的配置信息API地址、模型名、超时等存在哪里
| 选项 | 方案 | 优点 | 缺点 |
|------|------|------|------|
| [ ] A | 全部存数据库 | 完全动态,运行时可改 | 需要管理界面,初始化复杂 |
| [ ] B | 代码定义 + DB配置 | 平衡,核心逻辑在代码,参数可调 | 新适配器仍需改代码 |
| [ ] C | 配置文件 (YAML/JSON) | 简单,版本控制友好 | 改配置需重启 |
**推荐**: B代码定义适配器类DB存储启用状态/优先级/API Key引用
---
### 决策 2: 密钥管理
**问题**: API Key 等敏感信息如何存储?
| 选项 | 方案 | 优点 | 缺点 |
|------|------|------|------|
| [ ] A | 环境变量 | 简单,当前方式 | 多供应商时env膨胀改key需重启 |
| [ ] B | 数据库加密存储 | 动态管理支持多key | 需要加密方案,安全风险 |
| [ ] C | 外部密钥服务 (Vault/AWS Secrets) | 企业级安全 | 复杂,增加依赖 |
**推荐**: A当前阶段后期可迁移到B
---
### 决策 3: 图像供应商优先级
**问题**: 接入多个图像供应商后,默认使用哪个?
| 选项 | 供应商 | 特点 | 预估成本 |
|------|--------|------|----------|
| [ ] 1 | Nanobanana | 新兴,据说效果好 | 待调研 |
| [ ] 2 | Flux (当前) | 稳定,已接入 | ~$0.03/张 |
| [ ] 3 | DALL-E 3 | OpenAI出品质量高 | ~$0.04/张 |
| [ ] 4 | Midjourney | 艺术风格强 | API受限 |
**推荐**: 先调研Nanobanana效果好则替换Flux
---
### 决策 4: 文本供应商优先级
**问题**: 故事生成使用哪个LLM
| 选项 | 供应商 | 特点 | 预估成本 |
|------|--------|------|----------|
| [ ] 1 | Gemini (当前) | 免费额度大,中文好 | 免费/低成本 |
| [ ] 2 | OpenAI GPT-4o | 质量稳定 | ~$0.01/1K tokens |
| [ ] 3 | Claude | 创意写作强 | ~$0.015/1K tokens |
| [ ] 4 | Qwen (通义千问) | 国内,中文优化 | 待调研 |
**推荐**: Gemini为主OpenAI备用
---
### 决策 5: TTS供应商优先级
**问题**: 语音合成使用哪个服务?
| 选项 | 供应商 | 特点 | 预估成本 |
|------|--------|------|----------|
| [ ] 1 | Minimax (当前) | 中文效果好,已接入 | ~$0.01/1K字符 |
| [ ] 2 | ElevenLabs | 英文最佳,多语言 | ~$0.03/1K字符 |
| [ ] 3 | Azure TTS | 稳定,多语言 | ~$0.016/1K字符 |
| [ ] 4 | Google TTS | 便宜 | ~$0.004/1K字符 |
**推荐**: Minimax为主中文场景
---
### 决策 6: Admin Dashboard 技术栈
**问题**: 供应商管理后台用什么技术?
| 选项 | 方案 | 优点 | 缺点 |
|------|------|------|------|
| [ ] A | 复用 Vue 前端 | 技术栈统一,复用组件 | 需要自己写UI |
| [ ] B | React Admin | 成熟的Admin框架 | 引入新技术栈 |
| [ ] C | 现成方案 (AdminJS/Retool) | 开发快 | 定制性差,可能收费 |
**推荐**: A在现有Vue项目中加 `/admin` 路由)
---
### 决策 7: Phase 2 功能优先级
**问题**: 体验增强阶段先做哪个功能?
| 选项 | 功能 | 用户价值 | 开发复杂度 |
|------|------|----------|------------|
| [ ] 1 | 故事编辑 | 高用户可修改AI内容 | 中 |
| [ ] 2 | 角色定制 | 高(孩子成为主角) | 低 |
| [ ] 3 | 故事分享 | 高(增长引擎) | 中 |
| [ ] 4 | 故事续写 | 中(延长使用时长) | 中 |
**推荐**: 2 → 1 → 3 → 4角色定制最快出效果
---
### 决策 8: 并行与容错实现顺序
**问题**: 并行执行、部分成功、SSE、断点续传先做哪些
| 选项 | 方案 | 说明 |
|------|------|------|
| [ ] A | P0先做 | 先实现并行+部分成功,快速见效 |
| [ ] B | P0+P1一起 | 并行+部分成功+SSE体验完整 |
| [ ] C | 只做SSE | 跳过简单方案,直接上流式 |
**推荐**: A先P0验证后再做SSE
---
## 确认后删除此区块
确认所有决策后,可以删除未选中的选项,保留最终方案作为实现依据。

View File

@@ -1,169 +0,0 @@
# DreamWeaver 产品路线图
## 产品愿景
**梦语织机** - 为 3-8 岁儿童打造的 AI 故事创作平台,让每个孩子都能拥有专属的成长故事。
### 核心价值主张
- **个性化**: 基于关键词生成独一无二的故事
- **教育性**: 融入成长主题(勇气、友谊、诚实等)
- **沉浸感**: AI 封面 + 语音朗读,多感官体验
- **亲子互动**: 家长参与创作,增进亲子关系
---
## 用户画像
### 主要用户家长25-40岁
- **需求**: 为孩子找到有教育意义的睡前故事
- **痛点**: 市面故事千篇一律,缺乏个性化
- **场景**: 睡前、旅途、周末亲子时光
### 次要用户:幼儿园/早教机构
- **需求**: 批量生成教学故事素材
- **痛点**: 内容制作成本高
- **场景**: 课堂教学、活动策划
---
## 功能规划
### Phase 1: MVP 完善(当前)
> 目标:核心体验闭环,用户可完整使用
| 功能 | 状态 | 说明 |
|------|------|------|
| 关键词生成故事 | ✅ 已完成 | 输入关键词AI 生成故事 |
| 故事润色增强 | ✅ 已完成 | 用户提供草稿AI 润色 |
| AI 封面生成 | ✅ 已完成 | 根据故事生成插画 |
| 语音朗读 | ✅ 已完成 | TTS 朗读故事 |
| 故事收藏管理 | ✅ 已完成 | 保存、查看、删除 |
| OAuth 登录 | ✅ 已完成 | GitHub/Google 登录 |
### Phase 2: 体验增强
> 目标:提升用户粘性,增加互动性
| 功能 | 优先级 | 说明 |
|------|--------|------|
| **故事编辑** | P0 | 用户可修改 AI 生成的故事内容 |
| **角色定制** | P0 | 输入孩子姓名/性别,成为故事主角 |
| **故事续写** | P1 | 基于已有故事继续创作下一章 |
| **多语言支持** | P1 | 英文故事生成(已有 i18n 基础) |
| **故事分享** | P1 | 生成分享图片/链接 |
| **收藏夹/标签** | P2 | 故事分类管理 |
### Phase 3: 社区与增长
> 目标:构建用户社区,实现自然增长
| 功能 | 优先级 | 说明 |
|------|--------|------|
| **故事广场** | P0 | 公开优质故事,用户可浏览 |
| **点赞/收藏** | P0 | 社区互动基础 |
| **故事模板** | P1 | 预设故事框架(冒险/友谊/成长) |
| **创作者主页** | P1 | 展示用户创作的故事集 |
| **评论系统** | P2 | 用户交流反馈 |
### Phase 4: 商业化
> 目标:建立可持续商业模式
| 功能 | 优先级 | 说明 |
|------|--------|------|
| **会员订阅** | P0 | 免费/基础/高级三档 |
| **故事导出** | P0 | PDF/电子书格式导出 |
| **实体书打印** | P1 | 对接印刷服务,生成实体绘本 |
| **API 开放** | P2 | 为 B 端客户提供 API |
| **企业版** | P2 | 幼儿园/早教机构定制 |
---
## 技术架构演进
### 当前架构 (Phase 1)
```
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Vue 3 │────▶│ FastAPI │────▶│ PostgreSQL │
│ Frontend │ │ Backend │ │ (Neon) │
└─────────────┘ └──────┬──────┘ └─────────────┘
┌────────────┼────────────┐
▼ ▼ ▼
┌────────┐ ┌─────────┐ ┌─────────┐
│ Gemini │ │ Minimax │ │ Flux │
│ (Text) │ │ (TTS) │ │ (Image) │
└────────┘ └─────────┘ └─────────┘
```
### Phase 2 架构演进
```
新增组件:
- Redis: 缓存 + 会话 + Rate Limit
- Celery: 异步任务队列(图片/音频生成)
- S3/OSS: 静态资源存储
```
### Phase 3 架构演进
```
新增组件:
- Elasticsearch: 故事全文搜索
- CDN: 静态资源加速
- 消息队列: 社区通知推送
```
---
## 里程碑规划
### M1: MVP 完善 ✅
- [x] 核心功能闭环
- [x] 工程鲁棒性改进
- [x] 测试覆盖
### M2: 体验增强
- [ ] 故事编辑功能
- [ ] 角色定制(孩子成为主角)
- [ ] 故事续写
- [ ] 多语言支持
- [ ] 分享功能
### M3: 社区上线
- [ ] 故事广场
- [ ] 用户互动(点赞/收藏)
- [ ] 创作者主页
### M4: 商业化
- [ ] 会员体系
- [ ] 故事导出
- [ ] 实体书打印
---
## 竞品分析
| 产品 | 优势 | 劣势 | 我们的差异化 |
|------|------|------|--------------|
| 凯叔讲故事 | 内容丰富、品牌知名 | 无个性化、订阅贵 | AI 个性化生成 |
| 喜马拉雅儿童 | 海量音频、多平台 | 内容同质化 | 用户参与创作 |
| ChatGPT | AI 能力强 | 非儿童专属、无配套 | 垂直场景优化 |
---
## 风险与应对
| 风险 | 影响 | 应对策略 |
|------|------|----------|
| AI 生成内容不当 | 高 | 内容审核 + 家长控制 |
| API 成本过高 | 中 | 缓存优化 + 分级限流 |
| 用户增长缓慢 | 中 | 社区运营 + 分享裂变 |
| 竞品模仿 | 低 | 快速迭代 + 深耕垂直 |
---
## 下一步行动
**Phase 2 优先实现功能:**
1. **故事编辑** - 用户体验核心痛点
2. **角色定制** - 差异化竞争力
3. **故事分享** - 自然增长引擎
是否需要我为这些功能生成详细的技术规格文档?

View File

@@ -1,49 +0,0 @@
# DreamWeaver 工程鲁棒性改进计划
## 概述
本计划旨在提升 DreamWeaver 项目的工程质量,包括测试覆盖、稳定性、可观测性等方面。
## 任务列表
### P0 - 关键问题修复
#### Task-1: 修复 Rate Limit 内存泄漏 ✅
- **文件**: `backend/app/api/stories.py`
- **方案**: 已迁移至 Redis 分布式限流,内存泄漏问题不再存在
#### Task-2: 添加核心 API 测试 ✅
- **文件**: `backend/tests/`
- **范围**: test_auth, test_stories, test_profiles, test_universes, test_push_configs, test_reading_events, test_provider_router
### P1 - 稳定性提升
#### Task-3: 添加 API 重试机制 ✅
- **方案**: 所有适配器已使用 `tenacity` 指数退避重试 (gemini, openai, cqtai, antigravity, minimax, elevenlabs)
#### Task-4: 添加结构化日志 ✅
- **文件**: `backend/app/core/logging.py`
- **方案**: structlog JSON/Console 双模式,所有适配器和 provider_router 已集成
### P2 - 代码优化
#### Task-5: 重构 Provider Router ✅
- **文件**: `backend/app/services/provider_router.py`
- **方案**: 已实现统一 `_route_with_failover` 函数
#### Task-6: 配置外部化 ✅
- **文件**: `backend/app/core/config.py`, `backend/app/services/provider_router.py`
- **方案**: 所有模型名已移至 Settings支持环境变量覆盖
#### Task-7: 修复脆弱的 URL 解析 ✅
- **状态**: `drawing.py` 已被适配器系统取代,不再存在
## 新增依赖 (已添加)
```toml
# pyproject.toml [project.dependencies]
cachetools>=5.0.0 # Task-1: TTL cache
tenacity>=8.0.0 # Task-3: 重试机制
structlog>=24.0.0 # Task-4: 结构化日志
# [project.optional-dependencies.dev]
pytest-cov>=4.0.0 # Task-2: 覆盖率报告
```

View File

@@ -1,303 +0,0 @@
DreamWeaver 前端 UI 重构任务列表
阶段一:基础设施(必须先完成)
TASK-001 [x]: 安装图标库
文件: frontend/package.json
操作: 安装 @heroicons/vue 图标库
命令: npm install @heroicons/vue
验收: 能在 Vue 组件中 import { SparklesIcon } from '@heroicons/vue/24/outline'
TASK-002 [x]: 扩展 Tailwind 配置
文件: frontend/tailwind.config.js
操作: 添加完整的设计系统配置
内容:
- 扩展 fontFamily 添加 sans: ['Noto Sans SC', ...]
- 扩展 borderRadius 添加 '2xl': '1rem', '3xl': '1.5rem'
- 扩展 boxShadow 添加 'glass': '0 8px 32px rgba(0,0,0,0.08)'
- 扩展 animation 添加 'float': 'float 3s ease-in-out infinite'
- 扩展 keyframes 添加 float 动画定义
验收: Tailwind 类 font-sans, rounded-3xl, shadow-glass, animate-float 可用
TASK-003 [x]: 精简全局样式
文件: frontend/src/style.css
操作:
1. 删除 .animate-float (移至 Tailwind)
2. 删除 .stars::before/after (移除 emoji 装饰)
3. 保留 .glass, .btn-magic, .input-magic, .card-hover, .gradient-text
4. 删除 --gradient-magic 变量(过于花哨)
验收: 文件行数减少约 30%,无 emoji 相关 CSS
---
阶段二:创建可复用组件
TASK-004 [x]: 创建 BaseButton 组件
文件: frontend/src/components/ui/BaseButton.vue
操作: 创建统一按钮组件
Props:
- variant: 'primary' | 'secondary' | 'danger' | 'ghost'
- size: 'sm' | 'md' | 'lg'
- loading: boolean
- disabled: boolean
- icon: Component (可选Heroicon 组件)
样式规范:
- primary: 使用 .btn-magic 渐变
- secondary: bg-white border border-gray-200
- danger: bg-red-500 text-white
- ghost: bg-transparent hover:bg-gray-100
验收: 导出组件,支持 slot 内容和所有 props
TASK-005 [x]: 创建 BaseCard 组件
文件: frontend/src/components/ui/BaseCard.vue
操作: 创建统一卡片组件
Props:
- hover: boolean (是否启用悬浮效果)
- padding: 'none' | 'sm' | 'md' | 'lg'
样式: 使用 .glass + rounded-2xl + 可选 .card-hover
验收: 导出组件,支持默认 slot
TASK-006 [x]: 创建 BaseInput 组件
文件: frontend/src/components/ui/BaseInput.vue
操作: 创建统一输入框组件
Props:
- modelValue: string
- type: 'text' | 'password' | 'email' | 'number'
- placeholder: string
- label: string (可选)
- error: string (可选)
- disabled: boolean
样式: 使用 .input-magic + 错误状态红色边框
验收: 支持 v-model显示 label 和 error
TASK-007 [x]: 创建 BaseSelect 组件
文件: frontend/src/components/ui/BaseSelect.vue
操作: 创建统一下拉选择组件
Props:
- modelValue: string | number
- options: Array<{ value: string | number, label: string }>
- label: string (可选)
- placeholder: string
- disabled: boolean
样式: 与 BaseInput 保持一致
验收: 支持 v-model正确渲染 options
TASK-008 [x]: 创建 BaseTextarea 组件
文件: frontend/src/components/ui/BaseTextarea.vue
操作: 创建统一文本域组件
Props:
- modelValue: string
- placeholder: string
- rows: number
- maxLength: number (可选,显示字数统计)
- label: string (可选)
样式: 使用 .input-magic右下角显示字数
验收: 支持 v-model字数统计正确
TASK-009 [x]: 创建 LoadingSpinner 组件
文件: frontend/src/components/ui/LoadingSpinner.vue
操作: 创建统一加载动画组件
Props:
- size: 'sm' | 'md' | 'lg'
- text: string (可选,加载提示文字)
样式: 紫色渐变圆环旋转动画,无 emoji
验收: 三种尺寸正确渲染
TASK-010 [x]: 创建 EmptyState 组件
文件: frontend/src/components/ui/EmptyState.vue
操作: 创建统一空状态组件
Props:
- icon: Component (Heroicon)
- title: string
- description: string
- actionText: string (可选)
- actionTo: string (可选,路由路径)
样式: 居中布局,图标使用 Heroicon 而非 emoji
验收: 点击按钮正确跳转
TASK-011 [x]: 创建 ConfirmModal 组件
文件: frontend/src/components/ui/ConfirmModal.vue
操作: 创建统一确认弹窗组件
Props:
- show: boolean
- title: string
- message: string
- confirmText: string
- cancelText: string
- variant: 'danger' | 'warning' | 'info'
Emits: confirm, cancel
样式: 使用 .glass 背景Transition 动画
验收: 显示/隐藏动画流畅,事件正确触发
TASK-012 [x]: 创建组件导出索引
文件: frontend/src/components/ui/index.ts
操作: 统一导出所有 UI 组件
内容:
export { default as BaseButton } from './BaseButton.vue'
export { default as BaseCard } from './BaseCard.vue'
// ... 其他组件
验收: 可以 import { BaseButton, BaseCard } from '@/components/ui'
---
阶段三:重构现有页面
TASK-013 [x]: 重构 NavBar 组件
文件: frontend/src/components/NavBar.vue
操作:
1. 将 emoji ✨🌟📚🛠️🚪 替换为 Heroicons (SparklesIcon, StarIcon, BookOpenIcon, Cog6ToothIcon, ArrowRightOnRectangleIcon)
2. 将 ?? 占位符替换为正确图标 (UserGroupIcon, GlobeAltIcon)
3. 使用 BaseButton 替换登录按钮
4. 移除 animate-float 和 animate-pulse 装饰动画
验收: 无 emoji图标统一为 Heroicons视觉更专业
TASK-014 [x]: 重构 Home.vue 页面
文件: frontend/src/views/Home.vue
操作:
1. 删除 Hero 区域的浮动 emoji 装饰 (🌙⭐✨🌟)
2. 将模式切换按钮的 emoji (✨📝) 替换为 Heroicons
3. 将教育主题按钮的 emoji 替换为 Heroicons 或移除
4. 使用 BaseButton 替换提交按钮
5. 使用 BaseTextarea 替换文本输入区
6. 使用 BaseSelect 替换档案/宇宙选择器
7. 将 Features 区域的 emoji (🎨🔊📚) 替换为 Heroicons
验收: 页面无 emoji使用统一组件视觉简洁专业
TASK-015 [x]: 重构 MyStories.vue 页面
文件: frontend/src/views/MyStories.vue
操作:
1. 使用 BaseButton 替换"创作新故事"按钮
2. 使用 LoadingSpinner 替换自定义加载动画
3. 使用 EmptyState 替换空状态区域(移除 📚✨🪄 emoji
4. 将错误状态的 😢 替换为 Heroicon ExclamationCircleIcon
5. 将统计区域的 📖 替换为 Heroicon
6. 使用 BaseCard 包装故事卡片
验收: 页面无 emoji组件统一
TASK-016 [x]: 重构 StoryDetail.vue 页面
文件: frontend/src/views/StoryDetail.vue
操作:
1. 使用 LoadingSpinner 替换加载动画
2. 将 🎨 替换为 Heroicon PhotoIcon
3. 将 🔊 替换为 Heroicon SpeakerWaveIcon
4. 将 ✨ 替换为 Heroicon SparklesIcon
5. 将 🗑️ 替换为 Heroicon TrashIcon
6. 将 ⚠️ 替换为 Heroicon ExclamationTriangleIcon
7. 使用 BaseButton 替换所有按钮
8. 使用 ConfirmModal 替换删除确认弹窗
验收: 页面无 emoji弹窗使用统一组件
TASK-017 [x]: 重构 AdminProviders.vue 页面
文件: frontend/src/views/AdminProviders.vue
操作:
1. 移除 <style scoped> 中的所有自定义样式
2. 登录表单使用 BaseCard + BaseInput + BaseButton
3. Provider 表单使用 BaseCard + BaseInput + BaseSelect + BaseButton
4. 表格使用 Tailwind 样式divide-y divide-gray-200hover:bg-gray-50
5. 操作按钮使用 BaseButton variant="ghost"
6. 整体布局使用 .glass 背景
7. 添加页面标题使用 .gradient-text
验收: 与主应用风格一致,无原生 HTML 样式
TASK-018 [x]: 重构 ChildProfiles.vue 页面
文件: frontend/src/views/ChildProfiles.vue
操作:
1. 检查并替换所有 emoji 为 Heroicons
2. 使用 BaseButton, BaseCard, BaseInput 等统一组件
3. 使用 EmptyState 处理空状态
4. 使用 LoadingSpinner 处理加载状态
验收: 页面无 emoji组件统一
TASK-019 [x]: 重构 ChildProfileDetail.vue 页面
文件: frontend/src/views/ChildProfileDetail.vue
操作: 同 TASK-018
验收: 页面无 emoji组件统一
TASK-020 [x]: 重构 Universes.vue 页面
文件: frontend/src/views/Universes.vue
操作: 同 TASK-018
验收: 页面无 emoji组件统一
TASK-021 [x]: 重构 UniverseDetail.vue 页面
文件: frontend/src/views/UniverseDetail.vue
操作: 同 TASK-018
验收: 页面无 emoji组件统一
---
阶段四:优化与收尾
TASK-022 [x]: 添加深色模式支持(可选)
文件: frontend/tailwind.config.js, frontend/src/style.css
操作:
1. 在 tailwind.config.js 添加 darkMode: 'class'
2. 为 .glass, .btn-magic 等添加 dark: 变体
3. 在 NavBar 添加主题切换按钮
验收: 点击切换按钮,整体配色切换
TASK-023 [x]: 添加 prefers-reduced-motion 支持
文件: frontend/src/style.css
操作: 为所有动画添加媒体查询
@media (prefers-reduced-motion: reduce) {
.animate-float, .card-hover, .btn-magic { animation: none; transition: none; }
}
验收: 系统设置"减少动态效果"时,动画停止
TASK-024 [x]: 性能优化 - 减少 backdrop-filter
文件: frontend/src/style.css
操作:
1. 将 .glass 的 backdrop-filter: blur(20px) 改为 blur(10px)
2. 移除嵌套 .glass 元素的 backdrop-filter
验收: 页面滚动更流畅,尤其在移动端
---
执行顺序建议
阶段一 (TASK-001 ~ 003) → 阶段二 (TASK-004 ~ 012) → 阶段三 (TASK-013 ~ 021) → 阶段四 (TASK-022 ~ 024)
每个任务完成后运行 npm run build 确保无类型错误。

View File

@@ -1,189 +0,0 @@
# .github/workflows/build.yml
# 构建并推送 Docker 镜像到 GitHub Container Registry
#
# 触发条件:
# - push 到 main 分支
# - 手动触发 (workflow_dispatch)
# - 创建版本标签 (v*)
#
# 镜像命名:
# ghcr.io/<owner>/dreamweaver-backend:latest
# ghcr.io/<owner>/dreamweaver-frontend:latest
# ghcr.io/<owner>/dreamweaver-admin-frontend:latest
name: Build and Push Docker Images
on:
push:
branches: [main]
tags: ['v*']
paths:
- 'backend/**'
- 'frontend/**'
- 'admin-frontend/**'
- '.github/workflows/build.yml'
workflow_dispatch:
inputs:
force_build:
description: 'Force rebuild all images'
required: false
default: 'false'
env:
REGISTRY: ghcr.io
IMAGE_PREFIX: ${{ github.repository_owner }}/dreamweaver
jobs:
# ==============================================
# 检测变更的目录
# ==============================================
changes:
runs-on: ubuntu-latest
outputs:
backend: ${{ steps.filter.outputs.backend }}
frontend: ${{ steps.filter.outputs.frontend }}
admin-frontend: ${{ steps.filter.outputs.admin-frontend }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
backend:
- 'backend/**'
frontend:
- 'frontend/**'
admin-frontend:
- 'admin-frontend/**'
# ==============================================
# 构建后端镜像
# ==============================================
build-backend:
needs: changes
if: needs.changes.outputs.backend == 'true' || github.event.inputs.force_build == 'true' || startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}-backend
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=sha,prefix=
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: ./backend
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
# ==============================================
# 构建前端镜像
# ==============================================
build-frontend:
needs: changes
if: needs.changes.outputs.frontend == 'true' || github.event.inputs.force_build == 'true' || startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}-frontend
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=sha,prefix=
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: ./frontend
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
# ==============================================
# 构建管理后台前端镜像
# ==============================================
build-admin-frontend:
needs: changes
if: needs.changes.outputs.admin-frontend == 'true' || github.event.inputs.force_build == 'true' || startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}-admin-frontend
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=sha,prefix=
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: ./admin-frontend
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

4
.gitignore vendored
View File

@@ -16,8 +16,8 @@ dist/
.idea/
.vscode/
# Claude Code local settings
.claude/settings.local.json
# Agent scratch files
.claude/
*.swp
*.swo

View File

@@ -18,6 +18,11 @@ DreamWeaver (梦语织机) - AI-powered children's story generation app for ages
## Commands
```bash
# Docker demo stack
cp backend/.env.example backend/.env
docker compose up -d --build
docker compose ps
# Backend
cd backend
pip install -e . # Install dependencies
@@ -25,7 +30,8 @@ pip install -e ".[dev]" # With dev tools (pytest, ruff)
uvicorn app.main:app --reload --port 8000 # Start dev server
# Celery worker (requires Redis)
celery -A app.tasks worker --loglevel=info
celery -A app.core.celery_app worker --loglevel=info
celery -A app.core.celery_app beat --loglevel=info
# Database migrations
alembic upgrade head # Run migrations
@@ -45,6 +51,12 @@ cd frontend
npm install
npm run dev # Start dev server (port 5173)
npm run build # Type-check + build
# Admin frontend
cd admin-frontend
npm install
npm run dev
npm run build
```
## Architecture
@@ -97,11 +109,12 @@ frontend/src/
- **JWT auth:** Stored in httpOnly cookie, validated via `get_current_user` dependency
- **Provider routing:** `provider_router.py` tries providers in order, auto-failover on error
- **Background tasks:** Celery workers handle achievements and push notifications
- **Proxy in dev:** Vite proxies `/api`, `/auth`, `/admin` to backend (see `vite.config.ts`)
- **Docker proxy:** Frontend Nginx proxies `/api`, `/auth`, `/admin`, `/static` to backend services
- **Local demo:** `ENABLE_DEMO_PROVIDERS=true` enables deterministic text/image/storybook demo adapters
## Provider System
AI providers are configured via env vars (`TEXT_PROVIDERS`, `IMAGE_PROVIDERS`, `TTS_PROVIDERS`) as JSON arrays. The router tries each in order and fails over automatically.
AI providers are configured via env vars (`TEXT_PROVIDERS`, `IMAGE_PROVIDERS`, `TTS_PROVIDERS`, `STORYBOOK_PROVIDERS`) as JSON arrays. The router tries each in order and fails over automatically.
Admin console (disabled by default): Set `ENABLE_ADMIN_CONSOLE=true` to enable `/admin/providers` CRUD endpoints with Basic Auth (`ADMIN_USERNAME`/`ADMIN_PASSWORD`).
@@ -112,7 +125,8 @@ See `backend/.env.example` for required variables:
- `DATABASE_URL`, `SECRET_KEY` (required)
- OAuth: `GITHUB_CLIENT_ID/SECRET`, `GOOGLE_CLIENT_ID/SECRET`
- AI: `TEXT_API_KEY`, `TTS_API_BASE`, `TTS_API_KEY`, `IMAGE_API_KEY`
- Providers: `TEXT_PROVIDERS`, `IMAGE_PROVIDERS`, `TTS_PROVIDERS` (JSON arrays)
- Providers: `TEXT_PROVIDERS`, `IMAGE_PROVIDERS`, `TTS_PROVIDERS`, `STORYBOOK_PROVIDERS` (JSON arrays)
- Demo mode: `ENABLE_DEMO_PROVIDERS`
- Celery: `CELERY_BROKER_URL`, `CELERY_RESULT_BACKEND` (Redis URLs)
- Admin: `ENABLE_ADMIN_CONSOLE`, `ADMIN_USERNAME`, `ADMIN_PASSWORD`
@@ -121,10 +135,11 @@ See `backend/.env.example` for required variables:
| Method | Route | Description |
| ------------------- | -------------------------- | --------------------------- |
| GET | `/auth/{provider}/signin` | OAuth login |
| GET | `/auth/dev/signin` | Local dev login |
| GET | `/auth/session` | Get current user |
| POST | `/api/generate` | Generate/enhance story |
| POST | `/api/image/generate/{id}` | Generate cover image |
| GET | `/api/audio/{id}` | Get TTS audio |
| POST | `/api/stories/generate/full` | Generate story + assets |
| POST | `/api/storybook/generate` | Generate storybook |
| POST | `/api/stories/{id}/assets/retry` | Retry cover/audio assets |
| GET | `/api/stories` | List stories (paginated) |
| GET/DELETE | `/api/stories/{id}` | Story CRUD |
| CRUD | `/api/profiles` | User profiles |

134
CLAUDE.md
View File

@@ -1,134 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
DreamWeaver (梦语织机) - AI-powered children's story generation app for ages 3-8. Features story generation from keywords, story enhancement, AI-generated cover images, and text-to-speech narration.
**Language:** Chinese (Simplified) for UI and comments.
## Tech Stack
- **Backend:** FastAPI + PostgreSQL (Neon) + SQLAlchemy (async) + Celery/Redis | **Python 3.11+**
- **Frontend:** Vue 3 + TypeScript + Pinia + Tailwind CSS + vue-i18n
- **Auth:** OAuth 2.0 (GitHub/Google) with JWT in httpOnly cookie
- **AI Services:** Text generation, image generation, TTS (pluggable adapters)
## Commands
```bash
# Backend
cd backend
pip install -e . # Install dependencies
pip install -e ".[dev]" # With dev tools (pytest, ruff)
uvicorn app.main:app --reload --port 8000 # Start dev server
# Celery worker (requires Redis)
celery -A app.tasks worker --loglevel=info
# Database migrations
alembic upgrade head # Run migrations
alembic revision -m "message" --autogenerate # Generate new migration
# Linting
ruff check app/ # Check code
ruff check app/ --fix # Auto-fix
# Testing
pytest # Run all tests
pytest tests/test_auth.py -v # Single test file
pytest -k "test_name" # Match pattern
# Frontend
cd frontend
npm install
npm run dev # Start dev server (port 5173)
npm run build # Type-check + build
```
## Architecture
```
backend/app/
├── main.py # FastAPI app entry, routes registration
├── api/
│ ├── auth.py # OAuth routes (GitHub/Google)
│ ├── stories.py # Story CRUD and AI generation endpoints
│ ├── profiles.py # User profile management
│ ├── universes.py # Story universe/world management
│ ├── reading_events.py # Reading progress tracking
│ ├── push_configs.py # Push notification settings
│ ├── admin_providers.py # Provider CRUD (admin)
│ └── admin_reload.py # Hot-reload providers (admin)
├── core/
│ ├── config.py # Pydantic settings from env
│ ├── deps.py # Dependency injection (auth, db session)
│ ├── security.py # JWT token create/verify
│ ├── prompts.py # AI prompt templates
│ └── admin_auth.py # Basic Auth for admin routes
├── db/
│ ├── database.py # SQLAlchemy async engine + session
│ ├── models.py # User, Story models
│ └── admin_models.py # Provider model
├── services/
│ ├── adapters/ # Capability adapters (text, image, tts)
│ ├── provider_router.py # Failover routing across providers
│ ├── provider_cache.py # In-memory provider config cache
│ ├── provider_metrics.py # Provider performance metrics
│ └── achievement_extractor.py # Extract achievements from stories
└── tasks/
├── achievements.py # Celery task: achievement processing
└── push_notifications.py # Celery task: push notifications
frontend/src/
├── api/client.ts # Axios wrapper with auth interceptors
├── stores/user.ts # Pinia user state
├── router.ts # Vue Router config
├── i18n.ts + locales/ # vue-i18n setup
├── components/ # Reusable Vue components
└── views/ # Page components
```
## Key Patterns
- **Async everywhere:** All database and API calls use async/await
- **Dependency injection:** FastAPI `Depends()` for auth and db session
- **JWT auth:** Stored in httpOnly cookie, validated via `get_current_user` dependency
- **Provider routing:** `provider_router.py` tries providers in order, auto-failover on error
- **Background tasks:** Celery workers handle achievements and push notifications
- **Proxy in dev:** Vite proxies `/api`, `/auth`, `/admin` to backend (see `vite.config.ts`)
## Provider System
AI providers are configured via env vars (`TEXT_PROVIDERS`, `IMAGE_PROVIDERS`, `TTS_PROVIDERS`) as JSON arrays. The router tries each in order and fails over automatically.
Admin console (disabled by default): Set `ENABLE_ADMIN_CONSOLE=true` to enable `/admin/providers` CRUD endpoints with Basic Auth (`ADMIN_USERNAME`/`ADMIN_PASSWORD`).
## Environment Variables
See `backend/.env.example` for required variables:
- `DATABASE_URL`, `SECRET_KEY` (required)
- OAuth: `GITHUB_CLIENT_ID/SECRET`, `GOOGLE_CLIENT_ID/SECRET`
- AI: `TEXT_API_KEY`, `TTS_API_BASE`, `TTS_API_KEY`, `IMAGE_API_KEY`
- Providers: `TEXT_PROVIDERS`, `IMAGE_PROVIDERS`, `TTS_PROVIDERS` (JSON arrays)
- Celery: `CELERY_BROKER_URL`, `CELERY_RESULT_BACKEND` (Redis URLs)
- Admin: `ENABLE_ADMIN_CONSOLE`, `ADMIN_USERNAME`, `ADMIN_PASSWORD`
## API Endpoints
| Method | Route | Description |
| ------------------- | -------------------------- | --------------------------- |
| GET | `/auth/{provider}/signin` | OAuth login |
| GET | `/auth/session` | Get current user |
| POST | `/api/generate` | Generate/enhance story |
| POST | `/api/image/generate/{id}` | Generate cover image |
| GET | `/api/audio/{id}` | Get TTS audio |
| GET | `/api/stories` | List stories (paginated) |
| GET/DELETE | `/api/stories/{id}` | Story CRUD |
| CRUD | `/api/profiles` | User profiles |
| CRUD | `/api/universes` | Story universes |
| CRUD | `/api/reading-events` | Reading progress |
| CRUD | `/api/push-configs` | Push notification settings |
| GET/POST/PUT/DELETE | `/admin/providers` | Provider management (admin) |

288
README.md
View File

@@ -1,208 +1,144 @@
# DreamWeaver
# DreamWeaver 梦语织机
AI 驱动的儿童故事生成应用,面向 3-8 岁儿童提供个性化童话创作
AI 儿童故事生成应用,面向 3-8 岁儿童与家长,支持关键词生成故事、绘本页生成、封面图生成、语音朗读、孩子档案、故事宇宙与 Provider 管理
当前仓库优先服务一个目标:本地 Docker 环境中稳定跑通完整产品闭环,便于求职演示和后续迭代。
## 技术栈
- 后端FastAPI、SQLAlchemy (async)、PostgreSQLOAuth (GitHub/Google)
- AI文本生成、图像生成、语音合成可替换适配器
- 前端Vue 3、TypeScript、Pinia、Vue Router、Vite
- 后端FastAPI、SQLAlchemy async、PostgreSQL、Celery、Redis
- 前端Vue 3、TypeScript、Pinia、Vue Router、Tailwind CSS、Vite
- 认证OAuth 2.0 + JWT httpOnly cookie本地演示支持 dev login
- AI文本、图片、TTS、绘本结构生成均通过 adapter/provider router 接入
## 仓库结构
```
backend/
app/ # 后端代码
alembic/ # 数据库迁移
pyproject.toml
.env.example
frontend/
src/ # 前端代码
package.json
```text
backend/ FastAPI 后端、Celery 任务、数据库迁移
frontend/ 用户端 Vue 应用
admin-frontend/ 管理端 Vue 应用
docs/ 当前产品、规划与技术文档
docker-compose.yml
```
## 快速开始
### 后端
## 本地 Docker 演示
1. 准备环境文件:
```bash
cp backend/.env.example backend/.env
```
本地求职演示建议在 `backend/.env` 中使用:
```env
DEBUG=true
ENABLE_DEMO_PROVIDERS=true
TEXT_PROVIDERS=["demo", "gemini", "openai"]
IMAGE_PROVIDERS=["demo", "cqtai"]
TTS_PROVIDERS=["edge_tts", "minimax", "elevenlabs"]
STORYBOOK_PROVIDERS=["demo", "storybook_primary"]
```
`SECRET_KEY` 必须设置为强随机值。`backend/.env` 已被 git 忽略,不要提交真实密钥。
2. 启动完整本地栈:
```bash
docker compose up -d --build
```
3. 访问入口:
- 用户端http://localhost:52080
- 本地开发登录http://localhost:52080/auth/dev/signin
- 管理端http://localhost:52888
- 后端健康检查http://localhost:52000/health
- 管理后端健康检查http://localhost:52800/health
4. 常用命令:
```bash
docker compose ps
docker compose logs -f backend
docker compose down
docker compose down -v
```
## 手动开发
后端:
```bash
cd backend
python -m venv .venv
.\.venv\Scripts\activate # Linux/Mac: source .venv/bin/activate
pip install -e .
cp .env.example .env # 填写 SECRET_KEY、DATABASE_URL、各 API Key
# 运行数据库迁移(先配置好 DATABASE_URL
pip install -e ".[dev]"
alembic upgrade head
# 启动
uvicorn app.main:app --reload --port 8000
```
### 前端
Celery
```bash
cd backend
celery -A app.core.celery_app worker --loglevel=info
celery -A app.core.celery_app beat --loglevel=info
```
用户前端:
```bash
cd frontend
npm install
npm run dev
```
前端常用环境变量(可放 `.env.development`
- `VITE_API_BASE`:后端地址,例如 `http://localhost:8000`
- `VITE_ADMIN_USER` / `VITE_ADMIN_PASS`:管理后台 Basic Auth 账号(仅后台开启时需要)
### 访问
- 前端http://localhost:5173
- 后端 APIhttp://localhost:8000
- Swagger 文档http://localhost:8000/docs
管理前端:
## Docker Compose 使用说明
本项目包含 3 个 compose 文件:
- `docker-compose.yml`:开发基线,包含本地构建(`build`)配置,适合日常开发调试。
- `docker-compose.prod.yml`:生产基线,使用预构建镜像(不本地构建),适合部署环境。
- `docker-compose.ha.yml`HA 覆盖层,提供 PostgreSQL 主从、Redis 主从 + Sentinel、备份任务。
### 使用选择
- 本地开发:使用 `docker-compose.yml`
- 生产部署:使用 `docker-compose.prod.yml`
- 需要高可用:在上面任一基线上叠加 `docker-compose.ha.yml`
> 注意:`docker-compose.ha.yml` 是覆盖文件,不能单独使用。
### 常用命令
#### 开发模式(单机)
```bash
docker compose -f docker-compose.yml up -d
cd admin-frontend
npm install
npm run dev
```
#### 开发 + HA主从/哨兵演练)
## 质量检查
```bash
docker compose -f docker-compose.yml -f docker-compose.ha.yml up -d
cd backend
pytest
ruff check app tests
cd ../frontend
npm run build
cd ../admin-frontend
npm run build
```
#### 生产模式(预构建镜像)
```bash
docker compose -f docker-compose.prod.yml up -d
```
当前已知情况:完整后端测试可通过;全量 ruff 仍有少量历史 lint 债,优先处理核心演示链路与新增代码。
#### 生产 + HA
```bash
docker compose -f docker-compose.prod.yml -f docker-compose.ha.yml up -d
```
## 核心接口
#### 查看状态 / 日志
```bash
docker compose -f docker-compose.yml -f docker-compose.ha.yml ps
docker compose -f docker-compose.yml -f docker-compose.ha.yml logs -f backend
```
#### 停止并清理(含卷)
```bash
docker compose -f docker-compose.yml -f docker-compose.ha.yml down -v
```
### `docker-compose.prod.yml` 镜像标签
`docker-compose.prod.yml` 使用以下镜像格式:
- `${REGISTRY:-}dreamweaver-backend:${TAG:-latest}`
- `${REGISTRY:-}dreamweaver-frontend:${TAG:-latest}`
- `${REGISTRY:-}dreamweaver-admin-frontend:${TAG:-latest}`
Linux 部署示例(推荐):
```bash
export REGISTRY=my-registry.example.com/
export TAG=2026.02.12
docker compose -f docker-compose.prod.yml up -d
```
Windows PowerShell 示例:
```powershell
$env:REGISTRY="my-registry.example.com/"
$env:TAG="2026.02.12"
docker compose -f docker-compose.prod.yml up -d
```
### Linux 服务器部署流程(推荐)
以下流程适用于 Ubuntu/CentOS 等 Linux 服务器:
#### 1) 准备配置
```bash
cp backend/.env.example backend/.env
# 编辑 backend/.env至少配置 SECRET_KEY、OAuth/API Key 等
```
#### 2) 启动(非 HA
```bash
export REGISTRY=my-registry.example.com/
export TAG=2026.02.12
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d
```
#### 3) 启动HA
```bash
export REGISTRY=my-registry.example.com/
export TAG=2026.02.12
docker compose -f docker-compose.prod.yml -f docker-compose.ha.yml pull
docker compose -f docker-compose.prod.yml -f docker-compose.ha.yml up -d
```
#### 4) 运行状态检查
```bash
docker compose -f docker-compose.prod.yml ps
docker compose -f docker-compose.prod.yml logs -f backend
```
HA 场景使用:
```bash
docker compose -f docker-compose.prod.yml -f docker-compose.ha.yml ps
docker compose -f docker-compose.prod.yml -f docker-compose.ha.yml logs -f backend
```
#### 5) 版本升级
```bash
export TAG=2026.02.13
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d
```
HA 场景同理,在命令中额外叠加 `-f docker-compose.ha.yml`
#### 6) 版本回滚
```bash
export TAG=2026.02.12
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d
```
## 供应商路由与管理后台
- 路由按配置顺序尝试:`TEXT_PROVIDERS`(默认 `text_primary`)、`IMAGE_PROVIDERS`(默认 `image_primary`)、`TTS_PROVIDERS`(默认 `tts_primary`)。失败会自动切换下一个。
- 管理后台(默认关闭):`ENABLE_ADMIN_CONSOLE=true` 时启用,接口在 `/admin/providers`CRUD`/admin/providers/reload`。鉴权使用 Basic Auth账号密码由 `ADMIN_USERNAME`/`ADMIN_PASSWORD` 设置(请覆盖默认值)。
- 建议后台放在受保护子域并在反代层加 Basic Auth/IP 白名单;生产环境默认关闭。
- 前端最小管理页:`/admin/providers`,仅后台开启时可用,使用上面的 Basic Auth 调用。
## 数据库迁移Alembic
- 运行迁移:`alembic upgrade head`
- 生成新迁移:`alembic revision -m "message" --autogenerate`
迁移脚本位于 `backend/alembic/versions/`,包含 `providers` 表和 `stories.mode` 字段。
## 主要 API
| 方法 | 路径 | 说明 |
| ---- | ---- | ---- |
| GET | `/auth/github/signin` | GitHub 登录 |
| GET | `/auth/google/signin` | Google 登录 |
| --- | --- | --- |
| GET | `/auth/dev/signin` | 本地开发登录,仅 `DEBUG=true` 可用 |
| GET | `/auth/github/signin` | GitHub OAuth 登录 |
| GET | `/auth/google/signin` | Google OAuth 登录 |
| GET | `/auth/session` | 当前会话 |
| POST | `/api/generate` | 生成/润色故事 |
| POST | `/api/image/generate/{id}` | 生成封面图 |
| GET | `/api/audio/{id}` | 获取 TTS 音频 |
| GET | `/api/stories` | 获取故事列表(分页) |
| GET | `/api/stories/{id}` | 获取故事详情 |
| DELETE | `/api/stories/{id}` | 删除故事 |
| GET | `/admin/providers` | Provider 列表(需后台开启 + 管理员) |
| POST | `/api/stories/generate/full` | 生成故事并尝试生成封面 |
| POST | `/api/storybook/generate` | 生成绘本 |
| POST | `/api/stories/{story_id}/assets/retry` | 统一重试封面/语音资源 |
| GET | `/api/stories` | 故事列表 |
| GET | `/api/stories/{story_id}` | 故事详情 |
| DELETE | `/api/stories/{story_id}` | 删除故事 |
| GET/POST/PUT/DELETE | `/admin/providers` | Provider 管理,需开启管理后台 |
## 环境变量
常用项(详见 `backend/.env.example`
- `SECRET_KEY`必填JWT 签名密钥
- `DATABASE_URL`必填PostgreSQL 连接串
- OAuth`GITHUB_CLIENT_ID/SECRET``GOOGLE_CLIENT_ID/SECRET`
- AI Keys`TEXT_API_KEY``TTS_API_BASE``TTS_API_KEY``IMAGE_API_KEY`
- Provider 列表:`TEXT_PROVIDERS``IMAGE_PROVIDERS``TTS_PROVIDERS`
- 管理后台:`ENABLE_ADMIN_CONSOLE``ADMIN_USERNAME``ADMIN_PASSWORD`
## 文档入口
## 注意
- 生产务必使用强随机 `SECRET_KEY`,并关闭 `ENABLE_ADMIN_CONSOLE`(或放在受保护子域/内网)。
- 路由会按配置/DB 顺序尝试供应商并失败切换,请确保各 Key 有效且配额充足。
- `docs/product/job-search-relaunch-prd.md`:求职版产品重启 PRD
- `docs/product/unified-generation-workflow-prd.md`:统一生成工作流 PRD
- `docs/planning/week-1-execution-backlog.md`:短期执行 backlog
- `docs/technical/memory-system-dev.md`:记忆系统技术说明
## 当前取舍
仓库只保留一个 Docker Compose 入口:`docker-compose.yml`。生产部署、HA 演练、旧 Claude 原型和历史归档已从主仓库移除,避免干扰当前求职演示主线。

View File

@@ -1,310 +0,0 @@
# docker-compose.ha.yml
# HA 覆盖配置(建议与 docker-compose.yml 叠加使用)
#
# 启动示例:
# docker compose -f docker-compose.yml -f docker-compose.ha.yml up -d
services:
# ==============================================
# 应用服务 Sentinel 配置覆盖
# ==============================================
backend:
environment:
- DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-dreamweaver}:${POSTGRES_PASSWORD:-dreamweaver_password}@db:5432/${POSTGRES_DB:-dreamweaver_db}
- REDIS_SENTINEL_ENABLED=true
- REDIS_SENTINEL_NODES=redis-sentinel-1:26379,redis-sentinel-2:26379,redis-sentinel-3:26379
- REDIS_SENTINEL_MASTER_NAME=mymaster
- REDIS_SENTINEL_DB=0
- REDIS_SENTINEL_SOCKET_TIMEOUT=0.5
backend-admin:
environment:
- DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-dreamweaver}:${POSTGRES_PASSWORD:-dreamweaver_password}@db:5432/${POSTGRES_DB:-dreamweaver_db}
- REDIS_SENTINEL_ENABLED=true
- REDIS_SENTINEL_NODES=redis-sentinel-1:26379,redis-sentinel-2:26379,redis-sentinel-3:26379
- REDIS_SENTINEL_MASTER_NAME=mymaster
- REDIS_SENTINEL_DB=0
- REDIS_SENTINEL_SOCKET_TIMEOUT=0.5
worker:
environment:
- DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-dreamweaver}:${POSTGRES_PASSWORD:-dreamweaver_password}@db:5432/${POSTGRES_DB:-dreamweaver_db}
- REDIS_SENTINEL_ENABLED=true
- REDIS_SENTINEL_NODES=redis-sentinel-1:26379,redis-sentinel-2:26379,redis-sentinel-3:26379
- REDIS_SENTINEL_MASTER_NAME=mymaster
- REDIS_SENTINEL_DB=0
- REDIS_SENTINEL_SOCKET_TIMEOUT=0.5
celery-beat:
environment:
- DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-dreamweaver}:${POSTGRES_PASSWORD:-dreamweaver_password}@db:5432/${POSTGRES_DB:-dreamweaver_db}
- REDIS_SENTINEL_ENABLED=true
- REDIS_SENTINEL_NODES=redis-sentinel-1:26379,redis-sentinel-2:26379,redis-sentinel-3:26379
- REDIS_SENTINEL_MASTER_NAME=mymaster
- REDIS_SENTINEL_DB=0
- REDIS_SENTINEL_SOCKET_TIMEOUT=0.5
# ==============================================
# PostgreSQL 主库(覆盖默认 db
# ==============================================
db:
image: postgres:15-alpine
container_name: dreamweaver_db_primary
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-dreamweaver}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-dreamweaver_password}
POSTGRES_DB: ${POSTGRES_DB:-dreamweaver_db}
command:
- postgres
- -c
- wal_level=replica
- -c
- max_wal_senders=10
- -c
- max_replication_slots=10
- -c
- hot_standby=on
- -c
- hba_file=/etc/postgresql/pg_hba.conf
ports:
- "52432:5432"
volumes:
- postgres_primary_data:/var/lib/postgresql/data
- ./ops/postgres-ha/pg_hba.conf:/etc/postgresql/pg_hba.conf:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-dreamweaver} -d ${POSTGRES_DB:-dreamweaver_db}"]
interval: 10s
timeout: 5s
retries: 10
# ==============================================
# PostgreSQL 从库(基于 pg_basebackup 初始化)
# ==============================================
db-replica:
image: postgres:15-alpine
container_name: dreamweaver_db_replica
restart: unless-stopped
user: "postgres"
environment:
POSTGRES_USER: ${POSTGRES_USER:-dreamweaver}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-dreamweaver_password}
POSTGRES_DB: ${POSTGRES_DB:-dreamweaver_db}
PGDATA: /var/lib/postgresql/data
depends_on:
db:
condition: service_healthy
volumes:
- postgres_replica_data:/var/lib/postgresql/data
command:
- /bin/sh
- -ec
- |
if [ ! -s "$$PGDATA/PG_VERSION" ]; then
echo "Initializing replica from primary..."
until pg_isready -h db -U "$$POSTGRES_USER" -d "$$POSTGRES_DB"; do sleep 2; done
export PGPASSWORD="$$POSTGRES_PASSWORD"
rm -rf "$$PGDATA"/*
pg_basebackup -h db -D "$$PGDATA" -U "$$POSTGRES_USER" -Fp -Xs -P -R
fi
chmod 700 "$$PGDATA"
exec postgres -c hot_standby=on
healthcheck:
test:
[
"CMD-SHELL",
"pg_isready -U ${POSTGRES_USER:-dreamweaver} -d ${POSTGRES_DB:-dreamweaver_db} && psql -U ${POSTGRES_USER:-dreamweaver} -d ${POSTGRES_DB:-dreamweaver_db} -tAc 'select pg_is_in_recovery();' | grep -q t",
]
interval: 10s
timeout: 5s
retries: 10
# ==============================================
# PostgreSQL 备份任务(每日一次,保留 7 天)
# ==============================================
postgres-backup:
image: postgres:15-alpine
container_name: dreamweaver_postgres_backup
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-dreamweaver}
POSTGRES_DB: ${POSTGRES_DB:-dreamweaver_db}
PGPASSWORD: ${POSTGRES_PASSWORD:-dreamweaver_password}
BACKUP_INTERVAL_SECONDS: ${BACKUP_INTERVAL_SECONDS:-86400}
depends_on:
db:
condition: service_healthy
volumes:
- postgres_backups:/backups
command:
- /bin/sh
- -ec
- |
while true; do
ts=$$(date +%Y%m%d_%H%M%S);
pg_dump -h db -U "$$POSTGRES_USER" -d "$$POSTGRES_DB" -F c -f "/backups/dreamweaver_$${ts}.dump";
find /backups -type f -name '*.dump' -mtime +7 -delete;
sleep "$$BACKUP_INTERVAL_SECONDS";
done
# ==============================================
# Redis 主库(覆盖默认 redis
# ==============================================
redis:
image: redis:7-alpine
container_name: dreamweaver_redis_master
restart: unless-stopped
ports:
- "52379:6379"
volumes:
- redis_master_data:/data
command: ["redis-server", "--appendonly", "yes", "--protected-mode", "no"]
networks:
default:
ipv4_address: 172.29.0.10
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 10
# ==============================================
# Redis 从库
# ==============================================
redis-replica:
image: redis:7-alpine
container_name: dreamweaver_redis_replica
restart: unless-stopped
depends_on:
redis:
condition: service_healthy
volumes:
- redis_replica_data:/data
command:
[
"redis-server",
"--appendonly",
"yes",
"--protected-mode",
"no",
"--replicaof",
"172.29.0.10",
"6379",
]
networks:
default:
ipv4_address: 172.29.0.11
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 10
# ==============================================
# Redis Sentinel (3 节点)
# ==============================================
redis-sentinel-1:
image: redis:7-alpine
container_name: dreamweaver_redis_sentinel_1
restart: unless-stopped
ports:
- "52631:26379"
depends_on:
redis:
condition: service_healthy
redis-replica:
condition: service_healthy
networks:
default:
ipv4_address: 172.29.0.21
command:
- /bin/sh
- -ec
- |
cat > /tmp/sentinel.conf <<EOF
port 26379
sentinel resolve-hostnames yes
sentinel announce-hostnames yes
sentinel monitor mymaster 172.29.0.10 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 10000
sentinel parallel-syncs mymaster 1
protected-mode no
dir /tmp
EOF
exec redis-sentinel /tmp/sentinel.conf
redis-sentinel-2:
image: redis:7-alpine
container_name: dreamweaver_redis_sentinel_2
restart: unless-stopped
ports:
- "52632:26379"
depends_on:
redis:
condition: service_healthy
redis-replica:
condition: service_healthy
networks:
default:
ipv4_address: 172.29.0.22
command:
- /bin/sh
- -ec
- |
cat > /tmp/sentinel.conf <<EOF
port 26379
sentinel resolve-hostnames yes
sentinel announce-hostnames yes
sentinel monitor mymaster 172.29.0.10 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 10000
sentinel parallel-syncs mymaster 1
protected-mode no
dir /tmp
EOF
exec redis-sentinel /tmp/sentinel.conf
redis-sentinel-3:
image: redis:7-alpine
container_name: dreamweaver_redis_sentinel_3
restart: unless-stopped
ports:
- "52633:26379"
depends_on:
redis:
condition: service_healthy
redis-replica:
condition: service_healthy
networks:
default:
ipv4_address: 172.29.0.23
command:
- /bin/sh
- -ec
- |
cat > /tmp/sentinel.conf <<EOF
port 26379
sentinel resolve-hostnames yes
sentinel announce-hostnames yes
sentinel monitor mymaster 172.29.0.10 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 10000
sentinel parallel-syncs mymaster 1
protected-mode no
dir /tmp
EOF
exec redis-sentinel /tmp/sentinel.conf
volumes:
postgres_primary_data:
postgres_replica_data:
postgres_backups:
redis_master_data:
redis_replica_data:
networks:
default:
ipam:
config:
- subnet: 172.29.0.0/24

View File

@@ -1,158 +0,0 @@
# docker-compose.prod.yml
# 生产部署配置 - 使用预构建镜像,不包含 build 指令
# 镜像通过 GitHub Actions 或本地 docker build 预先构建
#
# 使用方式:
# docker compose -f docker-compose.prod.yml up -d
#
# 镜像构建 (手动):
# docker build -t dreamweaver-backend:latest ./backend
# docker build -t dreamweaver-frontend:latest ./frontend
# docker build -t dreamweaver-admin-frontend:latest ./admin-frontend
services:
# ==============================================
# 前端服务 (C端用户 App)
# ==============================================
frontend:
image: ${REGISTRY:-}dreamweaver-frontend:${TAG:-latest}
container_name: dreamweaver_frontend
restart: always
ports:
- "52080:80"
depends_on:
- backend
# ==============================================
# 管理后台前端 (Admin Console)
# ==============================================
frontend-admin:
image: ${REGISTRY:-}dreamweaver-admin-frontend:${TAG:-latest}
container_name: dreamweaver_frontend_admin
restart: always
ports:
- "52888:80"
depends_on:
- backend-admin
# ==============================================
# 后端服务 (FastAPI)
# ==============================================
backend:
image: ${REGISTRY:-}dreamweaver-backend:${TAG:-latest}
container_name: dreamweaver_backend
restart: always
ports:
- "52000:8000"
env_file:
- ./backend/.env
environment:
- DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-dreamweaver}:${POSTGRES_PASSWORD:-dreamweaver_password}@db:5432/${POSTGRES_DB:-dreamweaver_db}
- CELERY_BROKER_URL=redis://redis:6379/0
- CELERY_RESULT_BACKEND=redis://redis:6379/0
volumes:
- backend_static:/app/static
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
# ==============================================
# 管理后台后端 (Admin Backend)
# ==============================================
backend-admin:
image: ${REGISTRY:-}dreamweaver-backend:${TAG:-latest}
container_name: dreamweaver_backend_admin
restart: always
ports:
- "52800:8001"
command: ["uvicorn", "app.admin_main:app", "--host", "0.0.0.0", "--port", "8001"]
env_file:
- ./backend/.env
environment:
- DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-dreamweaver}:${POSTGRES_PASSWORD:-dreamweaver_password}@db:5432/${POSTGRES_DB:-dreamweaver_db}
- CELERY_BROKER_URL=redis://redis:6379/0
- CELERY_RESULT_BACKEND=redis://redis:6379/0
volumes:
- backend_static:/app/static
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
# ==============================================
# 工作节点 (Celery Worker)
# ==============================================
worker:
image: ${REGISTRY:-}dreamweaver-backend:${TAG:-latest}
container_name: dreamweaver_worker
command: celery -A app.core.celery_app worker --loglevel=info
restart: always
env_file: ./backend/.env
environment:
- DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-dreamweaver}:${POSTGRES_PASSWORD:-dreamweaver_password}@db:5432/${POSTGRES_DB:-dreamweaver_db}
- CELERY_BROKER_URL=redis://redis:6379/0
- CELERY_RESULT_BACKEND=redis://redis:6379/0
depends_on:
redis:
condition: service_started
db:
condition: service_healthy
# ==============================================
# 调度节点 (Celery Beat)
# ==============================================
celery-beat:
image: ${REGISTRY:-}dreamweaver-backend:${TAG:-latest}
container_name: dreamweaver_beat
command: celery -A app.core.celery_app beat --loglevel=info
restart: always
env_file: ./backend/.env
environment:
- DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-dreamweaver}:${POSTGRES_PASSWORD:-dreamweaver_password}@db:5432/${POSTGRES_DB:-dreamweaver_db}
- CELERY_BROKER_URL=redis://redis:6379/0
- CELERY_RESULT_BACKEND=redis://redis:6379/0
depends_on:
redis:
condition: service_started
# ==============================================
# 数据库 (PostgreSQL)
# ==============================================
db:
image: postgres:15-alpine
container_name: dreamweaver_db
restart: always
environment:
POSTGRES_USER: ${POSTGRES_USER:-dreamweaver}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-dreamweaver_password}
POSTGRES_DB: ${POSTGRES_DB:-dreamweaver_db}
ports:
- "52432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-dreamweaver}"]
interval: 10s
timeout: 5s
retries: 5
# ==============================================
# 缓存 (Redis)
# ==============================================
redis:
image: redis:7-alpine
container_name: dreamweaver_redis
restart: always
ports:
- "52379:6379"
volumes:
- redis_data:/data
command: redis-server --appendonly yes
volumes:
postgres_data:
redis_data:
backend_static:

View File

@@ -1,183 +1,127 @@
# docker-compose.yml
# 开发环境配置 - 支持本地构建和快速迭代
#
# 使用方式:
# docker compose up -d # 启动所有服务
# docker compose up -d --build # 重新构建并启动
# docker compose logs -f backend # 查看日志
#
# 生产部署请使用: docker-compose.prod.yml
services:
# ==============================================
# 前端服务 (C端用户 App)
# ==============================================
frontend:
build: ./frontend
image: dreamweaver-frontend:dev
container_name: dreamweaver_frontend
restart: unless-stopped
ports:
- "52080:80"
depends_on:
- backend
# ==============================================
# 管理后台前端 (Admin Console)
# ==============================================
frontend-admin:
build: ./admin-frontend
image: dreamweaver-admin-frontend:dev
container_name: dreamweaver_frontend_admin
restart: unless-stopped
ports:
- "52888:80"
depends_on:
- backend-admin
# ==============================================
# 后端服务 (FastAPI)
# ==============================================
backend:
build: ./backend
image: dreamweaver-backend:dev
container_name: dreamweaver_backend
restart: unless-stopped
ports:
- "52000:8000"
env_file:
- ./backend/.env
environment:
- DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-dreamweaver}:${POSTGRES_PASSWORD:-dreamweaver_password}@db:5432/${POSTGRES_DB:-dreamweaver_db}
- CELERY_BROKER_URL=redis://redis:6379/0
- CELERY_RESULT_BACKEND=redis://redis:6379/0
- REDIS_URL=redis://redis:6379/0
volumes:
- backend_static:/app/static
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
# ==============================================
# 管理后台后端 (Admin Backend) - 复用 backend 镜像
# ==============================================
backend-admin:
image: dreamweaver-backend:dev
container_name: dreamweaver_backend_admin
restart: unless-stopped
ports:
- "52800:8001"
command: ["uvicorn", "app.admin_main:app", "--host", "0.0.0.0", "--port", "8001"]
env_file:
- ./backend/.env
environment:
- DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-dreamweaver}:${POSTGRES_PASSWORD:-dreamweaver_password}@db:5432/${POSTGRES_DB:-dreamweaver_db}
- CELERY_BROKER_URL=redis://redis:6379/0
- CELERY_RESULT_BACKEND=redis://redis:6379/0
- REDIS_URL=redis://redis:6379/0
volumes:
- backend_static:/app/static
depends_on:
backend:
condition: service_started
db:
condition: service_healthy
redis:
condition: service_started
# ==============================================
# 工作节点 (Celery Worker) - 复用 backend 镜像
# ==============================================
worker:
image: dreamweaver-backend:dev
container_name: dreamweaver_worker
command: celery -A app.core.celery_app worker --loglevel=info
restart: unless-stopped
env_file: ./backend/.env
environment:
- DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-dreamweaver}:${POSTGRES_PASSWORD:-dreamweaver_password}@db:5432/${POSTGRES_DB:-dreamweaver_db}
- CELERY_BROKER_URL=redis://redis:6379/0
- CELERY_RESULT_BACKEND=redis://redis:6379/0
- REDIS_URL=redis://redis:6379/0
depends_on:
backend:
condition: service_started
redis:
condition: service_started
db:
condition: service_healthy
# ==============================================
# 调度节点 (Celery Beat) - 复用 backend 镜像
# ==============================================
celery-beat:
image: dreamweaver-backend:dev
container_name: dreamweaver_beat
command: celery -A app.core.celery_app beat --loglevel=info
restart: unless-stopped
env_file: ./backend/.env
environment:
- DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-dreamweaver}:${POSTGRES_PASSWORD:-dreamweaver_password}@db:5432/${POSTGRES_DB:-dreamweaver_db}
- CELERY_BROKER_URL=redis://redis:6379/0
- CELERY_RESULT_BACKEND=redis://redis:6379/0
- REDIS_URL=redis://redis:6379/0
depends_on:
backend:
condition: service_started
redis:
condition: service_started
# ==============================================
# 数据库 (PostgreSQL)
# ==============================================
db:
image: postgres:15-alpine
container_name: dreamweaver_db
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-dreamweaver}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-dreamweaver_password}
POSTGRES_DB: ${POSTGRES_DB:-dreamweaver_db}
ports:
- "52432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-dreamweaver}"]
interval: 10s
timeout: 5s
retries: 5
# ==============================================
# 缓存 (Redis)
# ==============================================
redis:
image: redis:7-alpine
container_name: dreamweaver_redis
restart: unless-stopped
ports:
- "52379:6379"
volumes:
- redis_data:/data
command: redis-server --appendonly yes
# ==============================================
# 数据库管理 (Adminer) - 仅开发环境
# ==============================================
adminer:
image: adminer
container_name: dreamweaver_adminer
restart: unless-stopped
ports:
- "52999:8080"
depends_on:
- db
profiles:
- dev # 仅在 --profile dev 时启动
volumes:
postgres_data:
redis_data:
backend_static:
name: dreamweaver
x-backend-env: &backend-env
DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-dreamweaver}:${POSTGRES_PASSWORD:-dreamweaver_password}@db:5432/${POSTGRES_DB:-dreamweaver_db}
CELERY_BROKER_URL: redis://redis:6379/0
CELERY_RESULT_BACKEND: redis://redis:6379/0
REDIS_URL: redis://redis:6379/0
services:
frontend:
build: ./frontend
image: dreamweaver-frontend:dev
container_name: dreamweaver_frontend
restart: unless-stopped
ports:
- "52080:80"
depends_on:
backend:
condition: service_started
frontend-admin:
build: ./admin-frontend
image: dreamweaver-admin-frontend:dev
container_name: dreamweaver_frontend_admin
restart: unless-stopped
ports:
- "52888:80"
depends_on:
backend-admin:
condition: service_started
backend:
build: ./backend
image: dreamweaver-backend:dev
container_name: dreamweaver_backend
restart: unless-stopped
ports:
- "52000:8000"
env_file: ./backend/.env
environment: *backend-env
volumes:
- backend_static:/app/static
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
backend-admin:
image: dreamweaver-backend:dev
container_name: dreamweaver_backend_admin
restart: unless-stopped
command: ["uvicorn", "app.admin_main:app", "--host", "0.0.0.0", "--port", "8001"]
ports:
- "52800:8001"
env_file: ./backend/.env
environment: *backend-env
volumes:
- backend_static:/app/static
depends_on:
backend:
condition: service_started
db:
condition: service_healthy
redis:
condition: service_started
worker:
image: dreamweaver-backend:dev
container_name: dreamweaver_worker
restart: unless-stopped
command: celery -A app.core.celery_app worker --loglevel=info
env_file: ./backend/.env
environment: *backend-env
depends_on:
backend:
condition: service_started
db:
condition: service_healthy
redis:
condition: service_started
celery-beat:
image: dreamweaver-backend:dev
container_name: dreamweaver_beat
restart: unless-stopped
command: celery -A app.core.celery_app beat --loglevel=info
env_file: ./backend/.env
environment: *backend-env
depends_on:
backend:
condition: service_started
redis:
condition: service_started
db:
image: postgres:15-alpine
container_name: dreamweaver_db
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-dreamweaver}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-dreamweaver_password}
POSTGRES_DB: ${POSTGRES_DB:-dreamweaver_db}
ports:
- "52432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-dreamweaver} -d ${POSTGRES_DB:-dreamweaver_db}"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
container_name: dreamweaver_redis
restart: unless-stopped
command: redis-server --appendonly yes
ports:
- "52379:6379"
volumes:
- redis_data:/data
volumes:
backend_static:
postgres_data:
redis_data:

View File

@@ -1,166 +1,29 @@
# Documentation Index
# 文档索引
This repository now uses a simple documentation taxonomy so you can quickly tell:
当前文档只保留对求职演示和短期迭代有用的内容,避免旧方案、临时交接和原型素材混在主线里。
- what is current and actionable
- what is product-facing vs. technical
- what is historical and should not be treated as source of truth
## Product
---
- `product/job-search-relaunch-prd.md`
求职版产品重启 PRD。用于说明产品定位、用户价值、MVP 范围和面试表达主线。
## Folder Structure
- `product/unified-generation-workflow-prd.md`
统一生成工作流 PRD。用于说明故事、绘本、封面、语音如何收敛成一条清晰体验。
### `docs/product/`
## Planning
Current product documents. These are the best starting point when you want to understand:
- `planning/week-1-execution-backlog.md`
当前短期执行 backlog。用于决定下一步先做什么以及如何拆成可交付任务。
- product positioning
- MVP scope
- feature requirements
- portfolio/presentation story
## Technical
Files:
- `technical/memory-system-dev.md`
记忆系统技术说明。用于后续继续做孩子档案、故事宇宙和个性化生成。
- `job-search-relaunch-prd.md`
Status: Active
Type: Product strategy / reboot PRD
Use when: you want the current product direction and prioritization.
## 维护规则
- `unified-generation-workflow-prd.md`
Status: Active
Type: Feature-level PRD
Use when: you want the target design for the core generation workflow.
### `docs/planning/`
Execution-oriented documents. These are for sprint planning, backlog breakdown, and short-term delivery.
Files:
- `document-status-inventory.md`
Status: Active
Type: Documentation audit / implementation mapping
Use when: you want to know which docs are current, which capabilities are really implemented, and where coding should restart.
- `week-1-execution-backlog.md`
Status: Active
Type: Sprint / execution planning
Use when: you want to know what to do first and how to break work into tasks.
- `weekend-handoff-2026-04-17.md`
Status: Active
Type: Progress handoff / execution snapshot
Use when: you want to continue work from another machine without reconstructing the latest checkpoint from chat history.
### `docs/technical/`
Technical reference documents. These are implementation-oriented and may include design guidance or development notes.
Files:
- `memory-system-dev.md`
Status: Reference
Type: Technical design / development guide
Use when: you work on the memory system or want to study one style of technical design note.
Note: parts of this document are forward-looking and should be validated against the current codebase before implementation.
### `docs/operations/`
Runbooks and environment/operations documentation.
Files:
- `ha-runbook.md`
Status: Reference
Type: Operations runbook
Use when: you work on Docker-based HA deployment, Redis Sentinel, PostgreSQL replication, or backup verification.
### `docs/archive/`
Historical documents. Keep these for learning or project history, but do not treat them as the current source of truth.
Files:
- `provider-system-legacy.md`
Status: Archived
Type: Historical technical plan
Why archived: partially outdated; references earlier provider architecture and older app entry naming.
- `refactoring-plan-legacy.md`
Status: Archived
Type: Historical implementation plan
Why archived: reflects an earlier refactor phase; some items are completed, some are no longer current priorities.
- `stories-split-analysis-legacy.md`
Status: Archived
Type: Historical code analysis
Why archived: tied to a past `stories.py` split effort and no longer represents the current structure.
---
## Deleted Document
The following document was removed instead of archived:
- `backend/docs/code_review_report.md`
Reason: it was a one-off review artifact, not a durable project document, and its main issue was already resolved by the later `0002_add_api_key_to_providers.py` migration.
---
## Recommended Reading Order
If you want to understand the project as a product manager:
1. `docs/product/job-search-relaunch-prd.md`
2. `docs/product/unified-generation-workflow-prd.md`
3. `docs/planning/document-status-inventory.md`
4. `docs/planning/week-1-execution-backlog.md`
5. `docs/planning/weekend-handoff-2026-04-17.md`
6. `docs/technical/memory-system-dev.md`
7. `docs/operations/ha-runbook.md`
If you want to understand the project as an engineer:
1. `docs/planning/document-status-inventory.md`
2. `docs/product/unified-generation-workflow-prd.md`
3. `docs/technical/memory-system-dev.md`
4. `docs/operations/ha-runbook.md`
5. `docs/archive/*` only when you need historical context
---
## Documentation Rules Going Forward
When adding a new document, place it using these rules:
- Put it in `docs/product/` if it explains what should be built and why.
- Put it in `docs/planning/` if it explains when or in what order work should happen.
- Put it in `docs/technical/` if it explains how something works or should be implemented.
- Put it in `docs/operations/` if it is about deployment, environments, runbooks, or maintenance.
- Put it in `docs/archive/` if it is historically useful but no longer current.
Delete a document only when all three are true:
- it is a one-off artifact
- it is not a reusable reference
- its key information is either obsolete or already captured elsewhere
Archive instead of deleting when:
- the document shows project history
- the document may still help future debugging or learning
- you are not fully sure whether it is still valuable
---
## PM Learning Note
A good documentation system helps you think clearly:
- `product` tells you what problem you are solving
- `planning` tells you what to do next
- `technical` tells you how the system works
- `operations` tells you how to run it
- `archive` tells you what used to be true
That separation is useful not only for this repo, but also as a general PM habit. Many product documents become confusing because they mix all five at once.
- 新 PRD 放到 `docs/product/`
- 新执行计划放到 `docs/planning/`
- 新技术说明放到 `docs/technical/`
- 一次性记录、过期交接、旧原型、未验证部署实验不再放进主仓库
- 如果某份文档不能帮助“演示产品、解释决策、指导下一步编码”,优先删除而不是继续堆叠

View File

@@ -1,246 +0,0 @@
# Provider 系统开发文档
## 当前版本功能 (v0.2.0)
### 已完成功能
1. **CQTAI nano 图像适配器** (`app/services/adapters/image/cqtai.py`)
- 异步生成 + 轮询获取结果
- 支持 nano-banana / nano-banana-pro 模型
- 支持多种分辨率和画面比例
- 支持图生图 (filesUrl)
2. **密钥加密存储** (`app/services/secret_service.py`)
- Fernet 对称加密,密钥从 SECRET_KEY 派生
- Provider API Key 自动加密存储
- 密钥管理 API (CRUD)
3. **指标收集系统** (`app/services/provider_metrics.py`)
- 调用成功率、延迟、成本统计
- 时间窗口聚合查询
- 已集成到 provider_router
4. **熔断器功能** (`app/services/provider_metrics.py`)
- 连续失败 3 次触发熔断
- 60 秒后自动恢复尝试
- 健康状态持久化到数据库
5. **管理后台前端** (`app/admin_app.py`)
- 独立端口部署 (8001)
- Vue 3 + Tailwind CSS 单页应用
- Provider CRUD 管理
- 密钥管理界面
- Basic Auth 认证
### 配置说明
```bash
# 启动主应用
uvicorn app.main:app --port 8000
# 启动管理后台 (独立端口)
uvicorn app.admin_app:app --port 8001
```
环境变量:
```
CQTAI_API_KEY=your-cqtai-api-key
ENABLE_ADMIN_CONSOLE=true
ADMIN_USERNAME=admin
ADMIN_PASSWORD=your-secure-password
```
---
## 下一版本优化计划 (v0.3.0)
### 高优先级
#### 1. 智能负载分流 (方案 B)
**目标**: 主渠道压力大时自动分流到后备渠道
**实现方案**:
- 监控指标: 并发数、响应延迟、错误率
- 分流阈值配置:
```python
class LoadBalanceConfig:
max_concurrent: int = 10 # 并发超过此值时分流
max_latency_ms: int = 5000 # 延迟超过此值时分流
max_error_rate: float = 0.1 # 错误率超过 10% 时分流
```
- 分流策略: 加权轮询,根据健康度动态调整权重
**涉及文件**:
- `app/services/provider_router.py` - 添加负载均衡逻辑
- `app/services/provider_metrics.py` - 添加并发计数器
- `app/db/admin_models.py` - 添加 LoadBalanceConfig 模型
#### 2. Storybook 适配器
**目标**: 生成可翻页的分页故事书
**实现方案**:
- 参考 Gemini AI Story Generator 格式
- 输出结构:
```python
class StorybookPage:
page_number: int
text: str
image_prompt: str
image_url: str | None
class Storybook:
title: str
pages: list[StorybookPage]
cover_url: str | None
```
- 集成文本 + 图像生成流水线
**涉及文件**:
- `app/services/adapters/storybook/` - 新建目录
- `app/api/stories.py` - 添加 storybook 生成端点
### 中优先级
#### 3. 成本追踪系统
**目标**: 记录实际消费,支持预算控制
**实现方案**:
- 成本记录表:
```python
class CostRecord:
user_id: str
provider_id: str
capability: str # text/image/tts
estimated_cost: Decimal
actual_cost: Decimal | None
timestamp: datetime
```
- 预算配置:
```python
class BudgetConfig:
user_id: str
daily_limit: Decimal
monthly_limit: Decimal
alert_threshold: float = 0.8 # 80% 时告警
```
- 超预算处理: 拒绝请求 / 降级到低成本 provider
**涉及文件**:
- `app/db/admin_models.py` - 添加 CostRecord, BudgetConfig
- `app/services/cost_tracker.py` - 新建
- `app/api/admin_providers.py` - 添加成本查询 API
#### 4. 指标可视化
**目标**: 管理后台展示供应商指标图表
**实现方案**:
- 添加指标查询 API:
- GET /admin/metrics/summary - 汇总统计
- GET /admin/metrics/timeline - 时间线数据
- GET /admin/metrics/providers/{id} - 单个供应商详情
- 前端使用 Chart.js 或 ECharts 展示
### 低优先级
#### 5. 多租户 Provider 配置
**目标**: 每个租户可配置独立 provider 列表和 API Key
**实现方案**:
- 租户配置表:
```python
class TenantProviderConfig:
tenant_id: str
provider_type: str
provider_ids: list[str] # 按优先级排序
api_key_override: str | None # 加密存储
```
- 路由时优先使用租户配置,回退到全局配置
#### 6. Provider 健康检查调度器
**目标**: 定期主动检查 provider 健康状态
**实现方案**:
- Celery Beat 定时任务
- 每 5 分钟检查一次所有启用的 provider
- 更新 ProviderHealth 表
#### 7. 适配器热加载
**目标**: 支持运行时动态加载新适配器
**实现方案**:
- 适配器插件目录: `app/services/adapters/plugins/`
- 启动时扫描并注册
- 提供 API 触发重新扫描
---
## API 变更记录
### v0.2.0 新增
| Method | Route | Description |
|--------|-------|-------------|
| GET | `/admin/secrets` | 列出所有密钥名称 |
| POST | `/admin/secrets` | 创建/更新密钥 |
| DELETE | `/admin/secrets/{name}` | 删除密钥 |
| GET | `/admin/secrets/{name}/verify` | 验证密钥有效性 |
### 计划中 (v0.3.0)
| Method | Route | Description |
|--------|-------|-------------|
| GET | `/admin/metrics/summary` | 指标汇总 |
| GET | `/admin/metrics/timeline` | 时间线数据 |
| POST | `/api/storybook/generate` | 生成分页故事书 |
| GET | `/admin/costs` | 成本统计 |
| POST | `/admin/budgets` | 设置预算 |
---
## 适配器开发指南
### 添加新适配器
1. 创建适配器文件:
```python
# app/services/adapters/image/new_provider.py
from app.services.adapters.base import AdapterConfig, BaseAdapter
from app.services.adapters.registry import AdapterRegistry
@AdapterRegistry.register("image", "new_provider")
class NewProviderAdapter(BaseAdapter[str]):
adapter_type = "image"
adapter_name = "new_provider"
async def execute(self, prompt: str, **kwargs) -> str:
# 实现生成逻辑
pass
async def health_check(self) -> bool:
# 实现健康检查
pass
@property
def estimated_cost(self) -> float:
return 0.01 # USD
```
2. 在 `__init__.py` 中导入:
```python
# app/services/adapters/__init__.py
from app.services.adapters.image import new_provider as _new_provider # noqa: F401
```
3. 添加配置:
```python
# app/core/config.py
new_provider_api_key: str = ""
# app/services/provider_router.py
API_KEY_MAP["new_provider"] = "new_provider_api_key"
```
4. 更新 `.env.example`:
```
NEW_PROVIDER_API_KEY=
```

View File

@@ -1,109 +0,0 @@
# DreamWeaver 重构实施计划
## 1. 概述
本文档基于对当前架构的深入分析,制定了从稳定性、可维护性到可扩展性的分阶段重构计划。
**目标**
- **短期**:解决单点故障风险,优化开发体验,清理关键技术债。
- **中期**:提升系统高可用能力,增强监控与可观测性。
- **长期**:架构演进,支持大规模并发与复杂业务场景。
---
## 2. 短期优化计划 (1-2周)
**重点**:消除即时风险,提升部署效率。
### 2.1 统一镜像构建 (High Priority)
目前 `backend`, `backend-admin`, `worker`, `celery-beat` 重复构建 4 次,浪费资源且镜像版本可能不一致。
- **Action Items**:
- [x] 修改 `backend/Dockerfile` 为通用基础镜像。
- [x] 更新 `docker-compose.yml`,定义 `backend-base` 服务或使用 `image` 标签共享镜像。
- [x] 确保所有 Python 服务共用同一构建产物,仅启动命令不同。
### 2.2 修复 Provider 缓存与限流 (High Priority)
内存缓存 (`TTLCache`, `_latency_cache`) 在多进程/多实例下失效。
- **Action Items**:
- [x] 引入 Redis 作为共享缓存后端。
- [x] 重构 `_load_provider_cache`,将 Provider 配置缓存至 Redis。
- [x] 重构 `stories.py` 中的限流逻辑,使用 `redis-cell` 或简单的 Redis 计数器替代 `TTLCache`
### 2.3 拆分 `stories.py` (Medium Priority)
`app/api/stories.py` 超过 600 行,包含 API 定义、业务逻辑、验证逻辑,维护困难。
- **Action Items**:
- [x] 创建 `app/services/story_service.py`迁移生成、润色、PDF生成等核心逻辑。
- [x] 创建 `app/schemas/story_schemas.py`,迁移 Pydantic 模型(`GenerateRequest`, `StoryResponse` 等)。
- [x] API 层 `stories.py` 仅保留路由定义和依赖注入,调用 Service 层。
---
## 3. 中期优化计划 (1-2月)
**重点**:高可用 (HA) 与系统韧性。
### 3.1 数据库高可用 (Critical)
当前 PostgreSQL 为单点,且 Admin/User 混合使用。
- **Action Items**:
- [ ] 部署 PostgreSQL 主从复制 (Master-Slave)。
- [ ] 配置 `PgBouncer` 或 SQLAlchemy 读写分离,减轻主库压力。
- [ ] 实施数据库自动备份策略 (如 `pg_dump` 定时上传 S3)。
### 3.2 消息队列高可用 (Critical)
Redis 单点故障将导致 Celery 任务全盘停摆。
- **Action Items**:
- [ ] 迁移至 Redis Sentinel 或 Redis Cluster 模式。
- [ ] 更新 Celery 配置以支持 Sentinel/Cluster 连接串。
### 3.3 增强可观测性 (Important)
目前仅有简单的日志,缺乏系统级指标。
- **Action Items**:
- [ ] 集成 Prometheus Client暴露 `/metrics` 端点。
- [ ] 部署 Grafana + Prometheus监控 API 延迟、QPS、Celery 队列积压情况。
- [ ] 完善 `ProviderMetrics`,增加可视化大盘,实时监控 AI 供应商的成本与成功率。
### 3.4 Phase 3 最小可执行任务清单 (MVP)
目标:在不大改业务代码的前提下,于一个迭代内完成高可用基础设施闭环。
- [x] PostgreSQL 主从:新增 `docker-compose.ha.yml`,包含 1 主 1 从与健康检查。
- [x] PostgreSQL 备份:新增每日备份任务(`pg_dump`)与 7 天保留策略。
- [x] Redis Sentinel新增 1 主 2 哨兵最小拓扑,并验证故障切换。
- [x] Celery 连接:更新 Celery broker/result backend 配置,支持 Sentinel 连接串。
- [x] 回归验证:执行一次故事生成 + 异步任务链路worker/beat冒烟测试。
- [x] 运行手册补充故障切换与恢复步骤文档PostgreSQL/Redis/Celery
---
## 4. 长期架构演进 (季度规划)
**重点**:业务解耦与规模化。
### 4.1 统一 API 网关
- **当前**前端直连后端端口CORS 配置分散。
- **演进**:引入 Traefik 或 Nginx 作为统一网关管理路由、SSL、全局限流、统一鉴权。
### 4.2 前端工程合并
- **当前**User App 和 Admin Console 是完全独立的两个项目,但在组件和工具链上高度重复。
- **演进**:使用一种 Monorepo 策略或基于路由的单一应用策略,共享组件库和类型定义,减少维护成本。
### 4.3 事件驱动架构完善
- **当前**:部分业务逻辑耦合在 API 中。
- **演进**:扩展事件总线,将“阅读记录”、“成就解锁”、“通知推送”等非核心链路完全异步化,通过 Domain Events 解耦。
---
## 5. 实施路线图
| 阶段 | 时间估算 | 关键里程碑 |
| :--- | :--- | :--- |
| **Phase 1: 基础夯实** | Week 1-2 | Docker 构建优化上线Redis 替代内存缓存。 |
| **Phase 2: 代码重构** | Week 3-4 | `stories.py` 拆分完成Service 层建立。 |
| **Phase 3: 高可用建设** | Month 2 | 数据库与 Redis 实现主备/集群模式。 |
| **Phase 4: 监控体系** | Month 2 | Grafana 监控大盘上线,关键指标报警配置完毕。 |

View File

@@ -1,52 +0,0 @@
# `stories.py` 拆分分析 (Phase 2 准备)
## 当前职责
`app/api/stories.py` (591 行) 承担了以下职责:
| 职责 | 行数 | 描述 |
|---|---|---|
| Pydantic 模型 | ~50 行 | `GenerateRequest`, `StoryResponse`, `FullStoryResponse` 等 |
| 验证逻辑 | ~40 行 | `_validate_profile_and_universe` |
| 路由 + 业务 | ~300 行 | `generate_story`, `generate_story_full`, `generate_story_stream` |
| 绘本逻辑 | ~170 行 | `generate_storybook_api` (含并行图片生成) |
| 成就查询 | ~30 行 | `get_story_achievements` |
## 缺失端点
测试中引用但 **未实现** 的端点(这些应在拆分时一并补充):
- `GET /api/stories` — 故事列表 (分页)
- `GET /api/stories/{id}` — 故事详情
- `DELETE /api/stories/{id}` — 故事删除
- `POST /api/image/generate/{id}` — 封面图片生成
- `GET /api/audio/{id}` — 语音朗读
## 建议拆分结构
```
app/
├── schemas/
│ └── story_schemas.py # [NEW] Pydantic 模型
├── services/
│ └── story_service.py # [NEW] 核心业务逻辑
└── api/
├── stories.py # [SLIM] 路由定义 + 依赖注入
└── stories_storybook.py # [NEW] 绘本相关端点 (可选)
```
### `story_schemas.py`
- 迁移所有 Pydantic 模型
- 包括 `GenerateRequest`, `StoryResponse`, `FullStoryResponse`, `StorybookRequest`, `StorybookResponse`
### `story_service.py`
- `validate_profile_and_universe()` — 验证逻辑
- `create_story()` — 故事入库
- `generate_and_save_story()` — 生成 + 保存联合操作
- `generate_storybook_with_images()` — 绘本并行图片生成
- 补充: `list_stories()`, `get_story()`, `delete_story()`
### `stories.py` (瘦路由层)
- 仅保留 `@router` 装饰器和依赖注入
- 调用 service 层完成业务逻辑
- 预计 150-200 行

View File

@@ -1,89 +0,0 @@
# HA 部署与验证 RunbookPhase 3 MVP
本文档对应 `docker-compose.ha.yml`,用于本地/测试环境验证高可用基础能力。
## 1. 启动方式
```bash
docker compose -f docker-compose.yml -f docker-compose.ha.yml up -d
```
说明:
- 基础业务服务仍来自 `docker-compose.yml`
- `docker-compose.ha.yml` 覆盖了 `db``redis`,并新增 `db-replica``postgres-backup``redis-replica``redis-sentinel-*`
## 2. 核心环境变量建议
`backend/.env`(或 shell 环境)中至少配置:
```env
# PostgreSQL
POSTGRES_USER=dreamweaver
POSTGRES_PASSWORD=dreamweaver_password
POSTGRES_DB=dreamweaver_db
POSTGRES_REPMGR_PASSWORD=repmgr_password
# Redis Sentinel
REDIS_SENTINEL_ENABLED=true
REDIS_SENTINEL_NODES=redis-sentinel-1:26379,redis-sentinel-2:26379,redis-sentinel-3:26379
REDIS_SENTINEL_MASTER_NAME=mymaster
REDIS_SENTINEL_DB=0
REDIS_SENTINEL_SOCKET_TIMEOUT=0.5
# 可选:若 Sentinel/Redis 设置了密码
REDIS_SENTINEL_PASSWORD=
# 备份周期,默认 86400 秒1 天)
BACKUP_INTERVAL_SECONDS=86400
```
## 3. 健康检查
### 3.1 PostgreSQL 主从
```bash
docker compose -f docker-compose.yml -f docker-compose.ha.yml ps
docker exec -it dreamweaver_db_primary psql -U dreamweaver -d dreamweaver_db -c "select now();"
docker exec -it dreamweaver_db_replica psql -U dreamweaver -d dreamweaver_db -c "select pg_is_in_recovery();"
```
期望:
- 主库可读写;
- 从库 `pg_is_in_recovery()` 返回 `t`
### 3.2 Redis Sentinel
```bash
docker exec -it dreamweaver_redis_sentinel_1 redis-cli -p 26379 sentinel masters
docker exec -it dreamweaver_redis_sentinel_1 redis-cli -p 26379 sentinel replicas mymaster
```
期望:
- `mymaster` 存在;
- 至少 1 个 replica 被发现。
### 3.3 备份任务
```bash
docker exec -it dreamweaver_postgres_backup sh -c "ls -lh /backups"
```
期望:
- `/backups` 下出现 `.dump` 文件;
- 旧于 7 天的备份会被自动清理。
## 4. 故障切换演练(最小)
```bash
# 模拟 Redis 主节点故障
docker stop dreamweaver_redis_master
# 等待 Sentinel 选主后查看
docker exec -it dreamweaver_redis_sentinel_1 redis-cli -p 26379 sentinel get-master-addr-by-name mymaster
```
提示:应用与 Celery 已支持 Sentinel 配置。若未启用 Sentinel仍可回退到 `REDIS_URL` / `CELERY_BROKER_URL` / `CELERY_RESULT_BACKEND` 直连模式。
## 5. 当前已知限制(下一步)
- PostgreSQL 侧当前仅完成主从拓扑读写分离PgBouncer/路由)待后续迭代。

View File

@@ -1,450 +0,0 @@
# DreamWeaver 文档状态盘点表
**Version**: 1.0
**Date**: 2026-04-17
**Author**: Sarah (Product Owner) / Codex
**Document Type**: Documentation Audit / Source-of-Truth Inventory
---
## 1. 盘点目的
这份文档不是新的 PRD也不是新的技术方案而是一份“项目资产盘点文档”。它解决三个问题
1. 让团队快速分清楚 `docs/` 里哪些文件是当前有效文档,哪些只是历史材料。
2. 让产品文档与代码现实建立映射,避免“文档看起来很完整,但代码并没有落地”的错觉。
3. 在重新启动项目时,为后续改代码提供明确起点,减少无效重构和重复讨论。
对于求职版 DreamWeaver这份盘点文档的价值在于它帮助你把“会写需求文档”进一步提升为“会管理文档体系、会判断 source of truth、会做项目现状诊断”。
---
## 2. 盘点范围与判定口径
### 2.1 盘点范围
本次盘点覆盖以下对象:
- `docs/` 当前全部文档
- 后端核心实现:`backend/app/api/``backend/app/services/``backend/app/db/`
- 前端关键体验:`frontend/src/components/``frontend/src/views/``frontend/src/stores/`
- 运维相关配置:`docker-compose.ha.yml``backend/app/core/`
- 构建与验证结果:后端测试、后端 lint、主前端构建、管理端构建
### 2.2 文档状态定义
| 文档状态 | 含义 |
| --- | --- |
| Active | 当前有效,应作为近期工作的参考依据 |
| Reference | 有参考价值,但不能直接视为最新实现说明 |
| Archived | 保留历史价值,但不再作为现行 source of truth |
### 2.3 代码落地状态定义
| 落地状态 | 含义 |
| --- | --- |
| 非实现类文档 | 文档本身是产品/规划/治理文档,不直接对应“已实现/未实现” |
| 已实现 | 文档描述的主体能力已在代码中形成闭环,且验证结果基本通过 |
| 部分实现 | 已有主干能力,但关键路径、恢复能力、状态模型或工程质量仍未闭环 |
| 未实现 | 文档描述的主体仍是目标态,当前代码尚未形成有效落地 |
| 历史文档 | 文档描述对应的是过去的设计/阶段,部分内容已落地,但已不适合作为现行依据 |
### 2.4 本次验证快照
截至 2026-04-17 evening本次盘点同步得到以下验证结果
- 后端测试通过:`backend/``pytest -q` 结果为 `53 passed`
- 主前端类型检查通过:`frontend/``vue-tsc --noEmit` 成功
- 主前端完整构建在当前环境受 Rollup 可选原生包缺失影响,属于环境依赖问题,不是本轮状态模型改动直接引起
- 管理端范围仍未明确,不适合作为当前求职版稳定演示链路
- 后端 lint 仍有历史债务,尚未完成最后一轮收尾
这意味着:项目并不是“不能运行”,而是“核心主链路可用,但工程完备度和演示稳定性还没到求职成品状态”。
---
## 3. 文档状态总表
| 文档 | 分类 | 文档状态 | 代码落地状态 | 盘点结论 | 建议动作 |
| --- | --- | --- | --- | --- | --- |
| `docs/README.md` | 文档治理 | Active | 非实现类文档 | 当前 docs 分类规则清晰,已成为文档入口页 | 保留并持续维护 |
| `docs/product/job-search-relaunch-prd.md` | 产品 PRD | Active | 非实现类文档 | 是当前“求职版产品重启”的核心 source of truth | 保留,作为产品总纲 |
| `docs/product/unified-generation-workflow-prd.md` | 功能 PRD | Active | 部分实现 | 目标方向明确,但“统一工作流”目标态尚未真正落地 | 保留,作为改代码主依据 |
| `docs/planning/week-1-execution-backlog.md` | 执行规划 | Active | 非实现类文档 | 是执行计划,不应用它判断是否“已经做完” | 保留,并按完成情况更新 |
| `docs/planning/document-status-inventory.md` | 项目盘点 | Active | 非实现类文档 | 当前文档体系与代码现实的映射表 | 保留,后续按阶段更新 |
| `docs/technical/memory-system-dev.md` | 技术设计 | Reference | 部分实现 | 记忆系统主干已存在,但文档中不少内容仍是增强设计 | 保留,开发前逐条核验 |
| `docs/operations/ha-runbook.md` | 运维 Runbook | Reference | 部分实现 | Docker HA、Redis Sentinel、备份与 Celery Sentinel 支持已存在,但仍属基础版 | 保留,按真实环境演练继续校正 |
| `docs/archive/provider-system-legacy.md` | 历史技术文档 | Archived | 历史文档 | 部分设计已落地,但命名与架构描述已过时 | 继续归档,不再扩写 |
| `docs/archive/refactoring-plan-legacy.md` | 历史实施计划 | Archived | 历史文档 | 反映旧阶段重构过程,部分 checklist 已完成 | 继续归档,仅供回溯 |
| `docs/archive/stories-split-analysis-legacy.md` | 历史分析 | Archived | 历史文档 | 拆分分析对应的主要重构已发生 | 继续归档,仅供理解演进过程 |
---
## 4. 逐份文档判定说明
### 4.1 `docs/README.md`
**判定**
当前有效,属于“文档治理入口”。
**证据**
- 已明确区分 `product / planning / technical / operations / archive`
- 已写清删除与归档规则
- 已能帮助团队快速识别 source of truth
**结论**
这份文档不是实现说明,但它已经承担“文档信息架构”角色,应继续保留并作为 docs 首页。
### 4.2 `docs/product/job-search-relaunch-prd.md`
**判定**
当前有效,属于产品总纲文档。
**证据**
- 文档明确提出求职版产品定位、成功指标、P0/P1/P2 取舍
- 文档中的问题诊断与当前代码现实一致,包括:
- Storybook 恢复能力不足
- Provider 体系职责混杂
- Admin 构建问题影响演示
- 前端状态设计薄弱
**结论**
这份 PRD 不用于判断“是否已实现”,而用于回答“现在应该把项目做成什么样”。它是当前最重要的产品 source of truth。
### 4.3 `docs/product/unified-generation-workflow-prd.md`
**判定**
当前有效,但对应能力仅部分实现。
**证据**
- 当前后端仍保留多条生成路径:
- `POST /api/stories/generate`
- `POST /api/stories/generate/full`
- `POST /api/storybook/generate`
- 相关实现仍分别落在 `backend/app/api/stories.py``backend/app/services/story_service.py`
- 当前 `Story` 模型已具备统一主记录的基础字段:
- `story_text`
- `pages`
- `cover_prompt`
- `image_url`
- `mode`
- 当前已经落地的统一状态模型包括:
- `generation_status`
- `image_status`
- `audio_status`
- `last_error`
- `degraded_completed`
- 但更完整的工作流目标仍未完全实现,例如:
- `partial_ready`
- `retryable_assets`
- 统一资产重试入口
- 单一 generation service workflow
**结论**
这份文档对应的是“当前核心改造主线”。它已经不再只是方向性文档,因为统一状态模型和恢复能力已经开始落地;但它仍不是“已完成实现说明”,因为统一工作流入口和统一资产补全过程还未真正收束。
### 4.4 `docs/planning/week-1-execution-backlog.md`
**判定**
当前有效,属于执行计划文档。
**证据**
- 文档将工作拆成产品聚焦、工作流定义、Storybook 恢复、Admin 处理、Provider 边界梳理等任务
- 这些任务与盘点出的真实缺口一致
- 但多数事项仍未被代码完成,因此不能把这份文档当作“实现说明”
**结论**
这是一份“该做什么”的文档,不是“已经做了什么”的文档。后续应在每个任务完成后更新状态,而不是继续长期停留在初始 backlog。
### 4.5 `docs/technical/memory-system-dev.md`
**判定**
技术参考文档,部分实现。
**已落地部分证据**
- `backend/app/db/models.py` 中存在 `MemoryItem``ChildProfile``StoryUniverse`
- `backend/app/services/memory_service.py` 中已实现:
- 记忆类型定义
- 时效衰减评分
- Prompt 注入格式化
- TTL 清理
- recent story / favorite character / scary element 创建
- `backend/app/api/memories.py` 已提供记忆查询、创建、删除相关接口
- `backend/app/api/profiles.py` 已提供 `GET /profiles/{profile_id}/timeline`
- `backend/app/tasks/memory.py``backend/app/core/celery_app.py` 已接入每日清理任务
**未完全落地部分**
- 文档规划的反馈接口 `POST /api/memories/{id}/feedback` 当前不存在
- 更复杂的“长期印象总结”“通知机制”“更丰富的结构化 schema”尚未形成闭环
- 时间线目前主要由档案创建、故事记录、宇宙成就拼装而成,还不是完整的成长操作系统
**结论**
记忆系统不是“没做”,而是“已经有主干,但还停在可用原型阶段”。这份文档应该被保留为技术参考,但开发时必须逐条核验,不可直接按文档默认其已落地。
### 4.6 `docs/operations/ha-runbook.md`
**判定**
运维参考文档,部分实现。
**已落地部分证据**
- `docker-compose.ha.yml` 已提供:
- PostgreSQL 主库
- PostgreSQL 从库
- 定时备份容器
- Redis 主从
- 3 个 Sentinel 节点
- `backend/app/core/config.py` 已支持 Sentinel 相关配置解析
- `backend/app/core/redis.py` 已支持通过 Sentinel 获取 Redis master
- `backend/app/core/celery_app.py` 已支持 Celery broker/result backend 走 Sentinel
**未完全落地部分**
- 仍停留在 Docker Compose 层的基础 HA 演练,不是成熟生产级方案
- 尚未看到读写分离、连接池代理、监控告警等更完整设施
- 这份 runbook 更适合作为“基础 HA 实验手册”,而不是正式生产运维规范
**结论**
该文档不应删除,因为它对应的基础设施确实存在;但也不能对外表述成“完整 HA 能力已成熟上线”。
### 4.7 `docs/archive/provider-system-legacy.md`
**判定**
历史文档,部分内容已落地,但整体已过时。
**证据**
- 文档提到的 provider failover、metrics、secret management、admin console 等能力,在代码中能找到对应实现:
- `backend/app/services/provider_router.py`
- `backend/app/services/provider_metrics.py`
- `backend/app/services/secret_service.py`
- `backend/app/api/admin_providers.py`
- 但文档中的部分命名与现状不一致,例如仍提到 `app/admin_app.py`,而当前入口为 `backend/app/admin_main.py`
- 当前 provider router 同时承担默认配置、凭据映射、路由策略、熔断、成本记录等多项职责,说明体系已继续演化,不再等同于这份旧文档
**结论**
这份文档值得保留用于理解历史,但不能作为现行 provider 体系说明书。
### 4.8 `docs/archive/refactoring-plan-legacy.md`
**判定**
历史计划文档,部分任务已完成。
**证据**
- 文档中提到的 `stories.py` 拆分,目前已经有明显落地:
- `backend/app/services/story_service.py`
- `backend/app/schemas/story_schemas.py`
- `backend/app/api/stories.py`
- 文档中提到的 Redis / HA 方向也已有基础实现
- 但它描述的是更早阶段的改造路线,与当前“求职版重启”的产品目标已不是同一语境
**结论**
保留在 `archive/` 是合理的。它是“项目曾经怎么想”的材料,不是“现在应该怎么做”的材料。
### 4.9 `docs/archive/stories-split-analysis-legacy.md`
**判定**
历史分析文档,核心分析目的已经完成。
**证据**
- 文档聚焦 `stories.py` 过重的问题
- 当前已形成更合理的拆分:
- API 层保留路由
- schema 独立
- service 独立
- 说明它的主要使命已经完成
**结论**
应继续归档,用于未来解释“为什么会有现在的结构”,但不再参与当前需求决策。
---
## 5. 当前已落地的核心能力
以下能力已经具备“代码存在且主链路可验证”的基础:
### 5.1 内容生成基础能力
- 普通故事生成、完整故事生成、绘本生成均存在可调用接口
- `Story` 模型已能同时承载文本故事与分页绘本
- 封面图生成与成就提取已接入后处理链路
### 5.2 个性化上下文基础能力
- 孩子档案、故事宇宙、记忆系统、成长时间线均已有基础模型和接口
- Prompt 侧已接入记忆上下文构建
- 成就可回写到 `StoryUniverse.achievements`
### 5.3 Provider 管理基础能力
- Provider Router 已支持 failover
- Provider 管理、密钥管理、成本汇总等管理 API 已存在
- 默认 provider 列表与数据库 provider 配置可共存
### 5.4 运维与异步基础能力
- Celery + Redis 已接入
- Redis Sentinel 与 Celery Sentinel 配置已实现
- PostgreSQL 主从与备份的 Compose 级实验环境已存在
### 5.5 工程可运行性
- 后端测试通过:`53 passed`
- 主前端构建通过
---
## 6. 当前“部分实现 / 未实现”的关键缺口
这些缺口正是接下来改代码最应该优先处理的地方。
### 6.1 统一生成工作流尚未真正落地
虽然 PRD 已经明确目标,但当前系统仍是多入口、多响应模型、多处理路径并存。它们共享了一些底层能力,但还没有收束为统一 workflow。
### 6.2 Storybook 恢复能力不完整
当前前端仍依赖 `frontend/src/stores/storybook.ts` 暂存数据,`frontend/src/views/StorybookViewer.vue` 在刷新或直接访问时无法按 ID 恢复。这是最明显的“演示链路不稳”问题之一。
### 6.3 音频体验未形成闭环
当前 `GET /api/audio/{id}` 会在请求时即时生成音频,但没有持久化缓存与复用策略,既影响用户体验,也影响成本控制。
### 6.4 Provider 体系职责边界仍然混杂
当前 `provider_router.py` 既负责默认 provider、凭据映射、策略排序又承担 metrics、熔断、成本记录等职责。功能虽强但不利于后续持续演进也不利于你在面试中清晰讲解。
### 6.5 管理端尚未达到“可展示成品”标准
`admin-frontend` 当前构建失败,说明管理端虽然概念上存在,但还不适合作为稳定演示链路的一部分。
### 6.6 工程质量信号还不统一
后端测试是加分项,但 lint 未通过会削弱成熟度观感。对于求职版项目,测试通过但 lint 大量报错,会让项目显得“能跑,但还没收尾”。
---
## 7. 推荐下一步编码切入点
如果目标是“尽快把项目恢复到可演示、可讲清、可继续迭代”的状态,建议按以下顺序推进。
### 7.1 第一优先级:补齐 Storybook 按 ID 恢复
**为什么先做**
- 改动范围相对可控
- 用户价值直观
- 修完后演示稳定性立刻提升
- 很适合作为“重新启动项目后的第一场胜仗”
**目标**
- `StorybookViewer` 不再只依赖 Pinia
- 支持通过 `story_id` 拉取 `Story.pages`
- 刷新页面后仍能继续阅读
### 7.2 第二优先级:抽出统一生成状态模型
**为什么第二个做**
- 这是“统一工作流”真正开始落地的最小切口
- 它能先统一语言,再统一代码
- 前端状态设计、后端任务编排、部分完成/降级完成,都会以它为中心展开
**目标**
- 先在服务层定义统一状态
- 再决定是否扩展数据库字段
- 让故事、绘本、图片、音频都能共享一套状态表达
### 7.3 第三优先级:清理 Provider 边界并决定 Admin 范围
**为什么第三个做**
- 这是系统长期可解释性的关键
- 但它比 Storybook 恢复和状态模型更抽象,适合在主链路稳定后推进
**目标**
- 先梳理 Capability / Provider / Routing Policy 三层概念
- 再判断管理端是修复、降级,还是缩小到只保留必要接口
---
## 8. 建议保留、更新、删除动作汇总
### 8.1 建议保留
- `docs/README.md`
- `docs/product/job-search-relaunch-prd.md`
- `docs/product/unified-generation-workflow-prd.md`
- `docs/planning/week-1-execution-backlog.md`
- `docs/technical/memory-system-dev.md`
- `docs/operations/ha-runbook.md`
- `docs/archive/*`
### 8.2 建议更新
- `docs/planning/week-1-execution-backlog.md`
- 需要随着任务推进更新完成状态,不应长期停留在纯规划状态
- `docs/technical/memory-system-dev.md`
- 后续开发时应补充“已实现”和“待实现”标记,减少误读
- `docs/operations/ha-runbook.md`
- 后续若做真实演练,应把演练结果写回文档
### 8.3 当前不建议再删除
本轮分类整理后,`docs/` 目录中没有新的“应该直接删除”的文档。剩余历史文件都具备学习价值或项目演进价值,适合继续保留在 `archive/`
---
## 9. PM 学习笔记:为什么要写这种盘点文档
很多初级产品文档只会写“要做什么”,但不会回答:
- 现在手里的文档哪些是真的有效
- 哪些是目标态,哪些是现状
- 哪些能力已经能演示,哪些只是概念
- 哪些问题适合现在改,哪些问题应该晚一点改
“文档状态盘点表”就是用来解决这些问题的。它本质上是产品管理中的三项能力训练:
1. **Source of Truth 管理**
你要知道团队现在到底该信哪份文档。
2. **现状诊断能力**
你要把 PRD、代码、构建结果、运维配置放在一起看而不是只看其中一边。
3. **优先级判断能力**
你要判断什么是“现在最值得做的第一件事”。
以后你在写自己的项目盘点时,可以直接复用这套结构:
1. 盘点目的
2. 判定口径
3. 状态总表
4. 逐项证据
5. 已落地能力
6. 关键缺口
7. 下一步建议
---
## 10. 本次盘点结论
DreamWeaver 当前不是“半成品废案”,而是“有明显实现基础、但还缺一轮产品收束与关键链路补完”的项目。
更准确地说:
- 产品层面,方向已经比以前清楚,现有 PRD 可以继续作为重启依据。
- 技术层面后端主能力、记忆系统、Provider 管理、异步任务和基础 HA 都不是空白。
- 体验层面Storybook 恢复、音频闭环、前端状态设计已明显推进,但统一工作流与统一重试入口仍是关键缺口。
- 工程层面,主前端与后端可用,但 admin-frontend 与 lint 问题说明项目还没完成最后一轮收尾。
因此,文档已经足够清晰,可以进入下一阶段:按优先级开始改代码,而不是继续扩写更多概念文档。

View File

@@ -1,139 +0,0 @@
# DreamWeaver Weekend Handoff - 2026-04-17
## Purpose
这份文档用于周末在另一台电脑上继续推进 DreamWeaver 时快速接手,不需要先重新阅读大量聊天记录或从工作区猜测上下文。
---
## What Is Already On Remote
当前远端 `main` 已经包含两个连续 checkpoint
- Commit: `a97a2fe`
- Message: `feat: persist story generation states and cache audio`
这个 checkpoint 覆盖的主线如下:
- 新增并落地统一生成状态字段:
- `generation_status`
- `image_status`
- `audio_status`
- `last_error`
- Storybook 阅读页支持按 ID 恢复
- 故事列表页、故事详情页、绘本阅读页接入统一状态展示
- 音频首次生成后缓存落盘并可复用
- 统一状态语义中 `degraded_completed` 已和错误展示保持一致
- Commit: `b8d3cb4`
- Message: `wip: snapshot full local workspace state`
这个 checkpoint 已把 2026-04-17 晚间的工作区快照同步到远端,包括:
- 新增 `AGENTS.md`
- 整理 `docs/` 文档信息架构
- 新增求职版 PRD、统一生成工作流 PRD、Week 1 backlog 与文档盘点
- 归档旧 backend docs 到 `docs/archive/``docs/technical/``docs/operations/`
- 补齐 Storybook 带 ID 路由恢复相关前端改动
注意:`b8d3cb4` 是一次 WIP 快照提交,原始 diff 中包含大量行尾/格式噪音。继续开发时应以当前 `main` 代码与 `docs/` 中 Active 文档为准,不需要再回到 `a97a2fe` 重新整理。
---
## Current Local Status On 2026-04-18
2026-04-18 在个人电脑接手后已确认:
- 本地 `main``origin/main` 对齐到 `b8d3cb4`
- 工作树起始状态干净
- 已配置 Gitea HTTPS 推送凭据,并通过 `git push --dry-run origin HEAD:main`
- 后端本地虚拟环境可用:`backend/.venv`
- 主前端与管理端依赖已安装到各自 `node_modules`
---
## Recommended Reading Order
周末继续前,建议先阅读:
1. `docs/product/job-search-relaunch-prd.md`
2. `docs/product/unified-generation-workflow-prd.md`
3. `docs/planning/week-1-execution-backlog.md`
4. `docs/planning/document-status-inventory.md`
---
## Environment Setup On The Next Machine
建议接手后先完成以下动作:
1. `git pull`
2. `cd backend && alembic upgrade head`
3. `cd backend && .venv/bin/python -m pytest -q`macOS/Linux
4. `cd frontend && npm install`
5. `cd frontend && ./node_modules/.bin/vue-tsc --noEmit`
如果主前端完整构建失败,优先检查 Rollup 可选原生包是否正常安装,而不是先怀疑本轮代码逻辑。
---
## Current Product / Engineering Position
当前阶段不是“继续加功能”,而是把 DreamWeaver 收敛成可讲述、可演示、可恢复的求职版产品。
已经完成的关键支点:
- 状态模型已落地,不再只是文档概念
- Storybook 恢复能力已补上
- 音频体验开始形成闭环
还没完成的关键工作:
- 普通故事、完整生成、绘本生成仍是多条 service 路径
- 缺少统一资产重试入口
- 缺少更清晰的统一 workflow 编排边界
- admin-frontend 范围和 Provider 边界仍未收束
---
## Best Next Step
周末最值得继续做的第一优先级:
### P0: 统一资产补全过程
目标:
- 抽出封面生成和音频生成的共同步骤
- 让图片 / 音频共享一套资产状态回写逻辑
- 为后续“统一重试入口”打基础
为什么先做:
- 它直接承接已经落地的状态模型
- 它比继续加页面更能体现系统设计能力
- 它能把当前三条生成路径往统一 workflow 再推近一步
### P1: 统一重试入口
目标:
- 至少设计出一个清晰的 retry API 方向
- 即使不一次性重命名为 `/api/generations/...`,也先做到内部统一
### P1: 收敛 service workflow
目标:
- 梳理普通故事 / 完整生成 / 绘本生成的共同步骤
- 把“验证上下文 -> 生成主内容 -> 保存主记录 -> 补全资产 -> 状态回写”整理成更明确的共享流程
---
## Important Reminder
如果周末是在另一台电脑上继续,不要默认“今天下午所有本地修改”都已经上远端。当前最可靠的 source of truth 是:
- 远端代码:以 commit `b8d3cb4` 为准
- 产品目标:以 `docs/product/job-search-relaunch-prd.md` 为准
- 当前执行主线:以 `docs/product/unified-generation-workflow-prd.md``docs/planning/week-1-execution-backlog.md` 为准

View File

@@ -1,216 +0,0 @@
@echo off
setlocal enabledelayedexpansion
REM Claude Code Windows CMD Bootstrap Script
REM Installs Claude Code for environments where PowerShell is not available
REM Parse command line argument
set "TARGET=%~1"
if "!TARGET!"=="" set "TARGET=latest"
REM Validate target parameter
if /i "!TARGET!"=="stable" goto :target_valid
if /i "!TARGET!"=="latest" goto :target_valid
echo !TARGET! | findstr /r "^[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*" >nul
if !ERRORLEVEL! equ 0 goto :target_valid
echo Usage: %0 [stable^|latest^|VERSION] >&2
echo Example: %0 1.0.58 >&2
exit /b 1
:target_valid
REM Check for 64-bit Windows
if /i "%PROCESSOR_ARCHITECTURE%"=="AMD64" goto :arch_valid
if /i "%PROCESSOR_ARCHITECTURE%"=="ARM64" goto :arch_valid
if /i "%PROCESSOR_ARCHITEW6432%"=="AMD64" goto :arch_valid
if /i "%PROCESSOR_ARCHITEW6432%"=="ARM64" goto :arch_valid
echo Claude Code does not support 32-bit Windows. Please use a 64-bit version of Windows. >&2
exit /b 1
:arch_valid
REM Set constants
set "GCS_BUCKET=https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases"
set "DOWNLOAD_DIR=%USERPROFILE%\.claude\downloads"
set "PLATFORM=win32-x64"
REM Create download directory
if not exist "!DOWNLOAD_DIR!" mkdir "!DOWNLOAD_DIR!"
REM Check for curl availability
curl --version >nul 2>&1
if !ERRORLEVEL! neq 0 (
echo curl is required but not available. Please install curl or use PowerShell installer. >&2
exit /b 1
)
REM Always download latest version (which has the most up-to-date installer)
call :download_file "!GCS_BUCKET!/latest" "!DOWNLOAD_DIR!\latest"
if !ERRORLEVEL! neq 0 (
echo Failed to get latest version >&2
exit /b 1
)
REM Read version from file
set /p VERSION=<"!DOWNLOAD_DIR!\latest"
del "!DOWNLOAD_DIR!\latest"
REM Download manifest
call :download_file "!GCS_BUCKET!/!VERSION!/manifest.json" "!DOWNLOAD_DIR!\manifest.json"
if !ERRORLEVEL! neq 0 (
echo Failed to get manifest >&2
exit /b 1
)
REM Extract checksum from manifest
call :parse_manifest "!DOWNLOAD_DIR!\manifest.json" "!PLATFORM!"
if !ERRORLEVEL! neq 0 (
echo Platform !PLATFORM! not found in manifest >&2
del "!DOWNLOAD_DIR!\manifest.json" 2>nul
exit /b 1
)
del "!DOWNLOAD_DIR!\manifest.json"
REM Download binary
set "BINARY_PATH=!DOWNLOAD_DIR!\claude-!VERSION!-!PLATFORM!.exe"
call :download_file "!GCS_BUCKET!/!VERSION!/!PLATFORM!/claude.exe" "!BINARY_PATH!"
if !ERRORLEVEL! neq 0 (
echo Failed to download binary >&2
if exist "!BINARY_PATH!" del "!BINARY_PATH!"
exit /b 1
)
REM Verify checksum
call :verify_checksum "!BINARY_PATH!" "!EXPECTED_CHECKSUM!"
if !ERRORLEVEL! neq 0 (
echo Checksum verification failed >&2
del "!BINARY_PATH!"
exit /b 1
)
REM Run claude install to set up launcher and shell integration
echo Setting up Claude Code...
"!BINARY_PATH!" install "!TARGET!"
set "INSTALL_RESULT=!ERRORLEVEL!"
REM Clean up downloaded file
REM Wait a moment for any file handles to be released
timeout /t 1 /nobreak >nul 2>&1
del /f "!BINARY_PATH!" >nul 2>&1
if exist "!BINARY_PATH!" (
echo Warning: Could not remove temporary file: !BINARY_PATH!
)
if !INSTALL_RESULT! neq 0 (
echo Installation failed >&2
exit /b 1
)
echo.
echo Installation complete^^!
echo.
exit /b 0
REM ============================================================================
REM SUBROUTINES
REM ============================================================================
:download_file
REM Downloads a file using curl
REM Args: %1=URL, %2=OutputPath
set "URL=%~1"
set "OUTPUT=%~2"
curl -fsSL "!URL!" -o "!OUTPUT!"
exit /b !ERRORLEVEL!
:parse_manifest
REM Parse JSON manifest to extract checksum for platform
REM Args: %1=ManifestPath, %2=Platform
set "MANIFEST_PATH=%~1"
set "PLATFORM_NAME=%~2"
set "EXPECTED_CHECKSUM="
REM Use findstr to find platform section, then look for checksum
set "FOUND_PLATFORM="
set "IN_PLATFORM_SECTION="
REM Read the manifest line by line
for /f "usebackq tokens=*" %%i in ("!MANIFEST_PATH!") do (
set "LINE=%%i"
REM Check if this line contains our platform
echo !LINE! | findstr /c:"\"%PLATFORM_NAME%\":" >nul
if !ERRORLEVEL! equ 0 (
set "IN_PLATFORM_SECTION=1"
)
REM If we're in the platform section, look for checksum
if defined IN_PLATFORM_SECTION (
echo !LINE! | findstr /c:"\"checksum\":" >nul
if !ERRORLEVEL! equ 0 (
REM Extract checksum value
for /f "tokens=2 delims=:" %%j in ("!LINE!") do (
set "CHECKSUM_PART=%%j"
REM Remove quotes, whitespace, and comma
set "CHECKSUM_PART=!CHECKSUM_PART: =!"
set "CHECKSUM_PART=!CHECKSUM_PART:"=!"
set "CHECKSUM_PART=!CHECKSUM_PART:,=!"
REM Check if it looks like a SHA256 (64 hex chars)
if not "!CHECKSUM_PART!"=="" (
call :check_length "!CHECKSUM_PART!" 64
if !ERRORLEVEL! equ 0 (
set "EXPECTED_CHECKSUM=!CHECKSUM_PART!"
exit /b 0
)
)
)
)
REM Check if we've left the platform section (closing brace)
echo !LINE! | findstr /c:"}" >nul
if !ERRORLEVEL! equ 0 set "IN_PLATFORM_SECTION="
)
)
if "!EXPECTED_CHECKSUM!"=="" exit /b 1
exit /b 0
:check_length
REM Check if string length equals expected length
REM Args: %1=String, %2=ExpectedLength
set "STR=%~1"
set "EXPECTED_LEN=%~2"
set "LEN=0"
:count_loop
if "!STR:~%LEN%,1!"=="" goto :count_done
set /a LEN+=1
goto :count_loop
:count_done
if %LEN%==%EXPECTED_LEN% exit /b 0
exit /b 1
:verify_checksum
REM Verify file checksum using certutil
REM Args: %1=FilePath, %2=ExpectedChecksum
set "FILE_PATH=%~1"
set "EXPECTED=%~2"
for /f "skip=1 tokens=*" %%i in ('certutil -hashfile "!FILE_PATH!" SHA256') do (
set "ACTUAL=%%i"
set "ACTUAL=!ACTUAL: =!"
if "!ACTUAL!"=="CertUtil:Thecommandcompletedsuccessfully." goto :verify_done
if "!ACTUAL!" neq "" (
if /i "!ACTUAL!"=="!EXPECTED!" (
exit /b 0
) else (
exit /b 1
)
)
)
:verify_done
exit /b 1

View File

@@ -1,10 +0,0 @@
# Allow local socket access
local all all trust
# Allow all IPv4/IPv6 client access in local docker network
host all all 0.0.0.0/0 trust
host all all ::/0 trust
# Allow streaming replication connections
host replication all 0.0.0.0/0 trust
host replication all ::/0 trust